Compare commits
8 Commits
648fae99b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 144a6c71a5 | |||
| 29e887cb14 | |||
| 947e119dba | |||
| 7d2f3c4679 | |||
| 332361c296 | |||
| 1e38c4ad57 | |||
| 60656d77e8 | |||
| c3a90f2816 |
51
.github/copilot-instructions.md
vendored
Normal file
51
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
## PlayMaker — guidance for AI coding assistants
|
||||||
|
|
||||||
|
This file gives focused, actionable knowledge to quickly make safe edits in the PlayMaker Flutter app.
|
||||||
|
|
||||||
|
Key facts (big picture)
|
||||||
|
- App: Flutter (mobile + desktop) using Supabase for backend and SharedPreferences for local persistence.
|
||||||
|
- Data ownership: teams are per-user (teams.user_id). Team records include fields like `id`, `name`, `image_url`, `wins`, `losses`, `draws`, `is_favorite`.
|
||||||
|
- Global selection: the app exposes a single global active team via `lib/controllers/active_team.dart` — a `ValueNotifier<ActiveTeam?>` named `globalActiveTeam`. Use `loadGlobalTeam()` and `saveGlobalTeam()` to read/update both local (SharedPreferences) and Supabase (`profiles.selected_team_id`).
|
||||||
|
|
||||||
|
Where to look (important files)
|
||||||
|
- `lib/controllers/active_team.dart` — central logic: load/save active team, prefers favorite team after our recent change.
|
||||||
|
- `lib/controllers/team_controller.dart` — creates teams, toggles favorites and exposes `teamsStream` for realtime listing.
|
||||||
|
- `lib/pages/home.dart` — main UI that shows the selected team, reads/writes last_team_* prefs and uses `TeamController` streams.
|
||||||
|
- `lib/pages/status_page.dart` — shows detailed stats and accepts initialTeam* props; will use the same persistence keys as `home.dart` as a fallback.
|
||||||
|
- `lib/widgets/*.dart` — UI building blocks (game, placar, team widgets) that assume team names/logos are strings and use `globalActiveTeam`/selectedTeam state.
|
||||||
|
|
||||||
|
Project-specific conventions & patterns
|
||||||
|
- Single active team: stored locally under prefs keys `last_team_id`, `last_team_name`, `last_team_logo`, `last_team_wins`, `last_team_losses`, `last_team_draws`.
|
||||||
|
- Realtime lists: controllers expose Streams from Supabase (e.g., `teamsStream`) and UI uses `StreamBuilder`/`snapshot.data` expecting List<Map<String,dynamic>>.
|
||||||
|
- Favoriting: `teams.is_favorite` is a boolean; code expects at most one favorite per user. When marking favorite, clear other favorites for that user and set the favorite as the global active team (we updated `team_controller.toggleFavorite` to do this).
|
||||||
|
- Global state: prefer using `globalActiveTeam` instead of ad-hoc SharedPreferences reads when changing the current team — it notifies Home and Status pages automatically.
|
||||||
|
- Naming: Portuguese UI strings are used throughout (e.g., "Selecionar Equipa"), so preserve that when editing visible text.
|
||||||
|
|
||||||
|
Examples of common edits
|
||||||
|
- Changing how the active team is chosen: edit `loadGlobalTeam()` in `lib/controllers/active_team.dart`. Example behaviour implemented: prefer `teams.is_favorite == true`, then fallback to `profiles.selected_team_id`, then local prefs.
|
||||||
|
- Adding a field to teams: update Supabase schema, then adapt `TeamController.getTeamsWithStats()` and UI widgets under `lib/widgets/` to read the new field.
|
||||||
|
- Making UI reactive to team changes: call `saveGlobalTeam(ActiveTeam(...))` to update memory, Supabase profile and notify UI.
|
||||||
|
|
||||||
|
Developer workflows (how to build/test/debug)
|
||||||
|
- Install dependencies: `flutter pub get`.
|
||||||
|
- Run on iOS simulator: `flutter run -d ios` (macOS only). Android: `flutter run -d android`.
|
||||||
|
- Quick analyzer: `flutter analyze` or rely on the project's editor diagnostics.
|
||||||
|
- Running tests: There is a `test/widget_test.dart` — run `flutter test` to execute.
|
||||||
|
- When debugging Supabase flows locally, ensure `google-services.json` (Android) and iOS config files are present under `android/app/` and `ios/` as appropriate.
|
||||||
|
|
||||||
|
Safe-edit checklist for AI agents
|
||||||
|
- Prefer small, focused patches. Keep existing structure and naming conventions (Portuguese strings, `prefs` keys and Supabase table names).
|
||||||
|
- When touching team selection: update both local prefs and Supabase `profiles.selected_team_id` (use `saveGlobalTeam`).
|
||||||
|
- When changing persistence keys, update both `home.dart` and `status_page.dart` and `active_team.dart`.
|
||||||
|
- Avoid altering app-wide themes or widget contracts unless necessary; many widgets rely on team names/logos being non-null strings.
|
||||||
|
- If adding Supabase queries, filter by `user_id` unless deliberately global.
|
||||||
|
|
||||||
|
If you can't find something
|
||||||
|
- Search for the pref keys `last_team_id` / `last_team_name` / `last_team_logo` to locate all usages.
|
||||||
|
- Look for `globalActiveTeam` to see places that react to the active team.
|
||||||
|
|
||||||
|
Contact / next steps
|
||||||
|
- After applying changes that affect team selection flow, run `flutter analyze` and (if possible) `flutter test`.
|
||||||
|
- Ask for confirmation before changing user-visible Portuguese copy or database schema.
|
||||||
|
|
||||||
|
— end of guidance —
|
||||||
157
SYNC_CHANGES_SUMMARY.md
Normal file
157
SYNC_CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Resumo das Mudanças - Sincronização de Jogo em Tempo Real
|
||||||
|
|
||||||
|
## 1. lib/controllers/placar_controller.dart
|
||||||
|
|
||||||
|
### Adicionado ao constructor:
|
||||||
|
```dart
|
||||||
|
final void Function(String actionType, Map<String, dynamic> actionData)? onSyncAction;
|
||||||
|
|
||||||
|
PlacarController({
|
||||||
|
required this.gameId,
|
||||||
|
required this.myTeam,
|
||||||
|
required this.opponentTeam,
|
||||||
|
this.onSyncAction, // ← NOVO
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adicionado método _dispatchSyncAction:
|
||||||
|
```dart
|
||||||
|
void _dispatchSyncAction(String actionType, Map<String, dynamic> actionData) {
|
||||||
|
if (onSyncAction != null) {
|
||||||
|
final enrichedActionData = Map<String, dynamic>.from(actionData)
|
||||||
|
..['remaining_seconds'] = durationNotifier.value.inSeconds
|
||||||
|
..['is_running'] = isRunning;
|
||||||
|
onSyncAction!(actionType, enrichedActionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adicionado em 5 métodos (chamada _dispatchSyncAction):
|
||||||
|
- `useTimeout()` → dispatch `'use_timeout'`
|
||||||
|
- `handleSubbing()` → dispatch `'subbing'`
|
||||||
|
- `swapCourtPlayers()` → dispatch `'swap_players'`
|
||||||
|
- `registerFoul()` → dispatch `'register_foul'`
|
||||||
|
- `commitStat()` → dispatch `'commit_stat'`
|
||||||
|
|
||||||
|
**Exemplo em commitStat:**
|
||||||
|
```dart
|
||||||
|
_dispatchSyncAction('commit_stat', {
|
||||||
|
'action': action,
|
||||||
|
'player_data': playerData,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. lib/pages/PlacarPage.dart
|
||||||
|
|
||||||
|
### Adicionado ao state:
|
||||||
|
```dart
|
||||||
|
String? _lastAppliedSyncEventId; // ← NOVO - deduplicação de eventos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constructor do controller:
|
||||||
|
```dart
|
||||||
|
_controller = PlacarController(
|
||||||
|
gameId: widget.gameId,
|
||||||
|
myTeam: widget.myTeam,
|
||||||
|
opponentTeam: widget.opponentTeam,
|
||||||
|
onSyncAction: _onLocalControllerSync, // ← CONECTADO
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adicionado novo método _onLocalControllerSync:
|
||||||
|
```dart
|
||||||
|
void _onLocalControllerSync(String actionType, Map<String, dynamic> actionData) {
|
||||||
|
if (_sessionId == null || _isApplyingRemoteSync) return;
|
||||||
|
print("📤 Enviando sync action local: $actionType -> $actionData");
|
||||||
|
_sharingController.sendSyncEvent(_sessionId!, actionType, actionData);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualizado _setupSyncListener (deduplicação):
|
||||||
|
```dart
|
||||||
|
_syncSubscription = _sharingController.listenToGameSyncOthers(_sessionId!).listen(
|
||||||
|
(dynamic event) {
|
||||||
|
Map<String, dynamic>? record;
|
||||||
|
if (event is List && event.isNotEmpty) {
|
||||||
|
for (final item in event) {
|
||||||
|
final row = item as Map<String, dynamic>?;
|
||||||
|
if (row == null) continue;
|
||||||
|
final rowId = row['id']?.toString();
|
||||||
|
if (rowId != null && rowId != _lastAppliedSyncEventId) {
|
||||||
|
record = row;
|
||||||
|
break; // ← para no primeiro evento novo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event is Map<String, dynamic>) {
|
||||||
|
record = Map<String, dynamic>.from(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record != null) {
|
||||||
|
final recordId = record['id']?.toString();
|
||||||
|
if (recordId != null && recordId == _lastAppliedSyncEventId) return;
|
||||||
|
if (recordId != null) _lastAppliedSyncEventId = recordId;
|
||||||
|
_applyRemoteSyncEvent(record);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualizado _applyRemoteSyncEvent (aplicar estado remoto):
|
||||||
|
```dart
|
||||||
|
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
|
||||||
|
final actionType = record['action_type']?.toString();
|
||||||
|
final actionData = Map<String, dynamic>.from(record['action_data'] ?? {});
|
||||||
|
|
||||||
|
// ← NOVO: aplicar timer remotamente em TODAS as ações
|
||||||
|
final remoteSeconds = int.tryParse(actionData['remaining_seconds']?.toString() ?? '');
|
||||||
|
final remoteIsRunning = actionData['is_running'] == true;
|
||||||
|
if (remoteSeconds != null) {
|
||||||
|
_controller.durationNotifier.value = Duration(seconds: remoteSeconds);
|
||||||
|
}
|
||||||
|
if (remoteIsRunning != _controller.isRunning) {
|
||||||
|
_isApplyingRemoteSync = true;
|
||||||
|
_controller.toggleTimer(context);
|
||||||
|
_isApplyingRemoteSync = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar ações específicas
|
||||||
|
if (actionType == 'toggle_timer') {
|
||||||
|
setState(() {});
|
||||||
|
} else if (actionType == 'commit_stat') {
|
||||||
|
// aplicar pontos/faltas
|
||||||
|
} else if (actionType == 'register_foul') {
|
||||||
|
// aplicar falta
|
||||||
|
} else if (actionType == 'subbing') {
|
||||||
|
// aplicar substituição
|
||||||
|
} else if (actionType == 'swap_players') {
|
||||||
|
// trocar posição
|
||||||
|
} else if (actionType == 'use_timeout') {
|
||||||
|
// usar timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fluxo Completo
|
||||||
|
|
||||||
|
1. **Ação Local** → `commitStat()` no controller
|
||||||
|
2. **Controller emite** → `_dispatchSyncAction('commit_stat', {action, player_data, remaining_seconds, is_running})`
|
||||||
|
3. **PlacarPage escuta** → `_onLocalControllerSync()` recebe o evento
|
||||||
|
4. **Envia ao Supabase** → `sendSyncEvent()` armazena em `game_sync_events`
|
||||||
|
5. **Parceiro recebe** → `listenToGameSyncOthers()` retorna o evento
|
||||||
|
6. **Aplica remotamente** → `_applyRemoteSyncEvent()` executa a ação no parceiro
|
||||||
|
7. **Estado sincronizado** → Ambos têm timer, pontos, faltas idênticos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultado
|
||||||
|
|
||||||
|
✅ Timer não reseta ao marcar ponto
|
||||||
|
✅ Pontos sincronizam entre os dois lados
|
||||||
|
✅ Faltas sincronizam
|
||||||
|
✅ Timeouts sincronizam
|
||||||
|
✅ Substituições sincronizam
|
||||||
|
✅ Posições de jogadores sincronizam
|
||||||
BIN
assets/campone.png
Normal file
BIN
assets/campone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 MiB |
108
create_tables.sql
Normal file
108
create_tables.sql
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- GARANTIR QUE A TABELA PROFILES TEM TODAS AS COLUNAS
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Verifica e adiciona colunas que podem estar em falta
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='id'
|
||||||
|
) THEN
|
||||||
|
CREATE TABLE profiles (
|
||||||
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
username VARCHAR(255),
|
||||||
|
full_name VARCHAR(255),
|
||||||
|
avatar_url TEXT,
|
||||||
|
selected_team_id UUID,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Adiciona colunas em falta se necessário
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='username'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN username VARCHAR(255);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='full_name'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN full_name VARCHAR(255);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='avatar_url'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN avatar_url TEXT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='selected_team_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN selected_team_id UUID;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CRIAR TABELAS DE COMPARTILHAMENTO DE JOGO
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS game_sync_events;
|
||||||
|
DROP TABLE IF EXISTS game_sessions;
|
||||||
|
|
||||||
|
CREATE TABLE game_sessions (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||||
|
created_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
|
shared_with_user_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
|
||||||
|
share_code VARCHAR(10) UNIQUE NOT NULL,
|
||||||
|
status VARCHAR(20) DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE game_sync_events (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
session_id UUID NOT NULL REFERENCES game_sessions(id) ON DELETE CASCADE,
|
||||||
|
action_type VARCHAR(50) NOT NULL,
|
||||||
|
action_data JSONB,
|
||||||
|
triggered_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CRIAR ÍNDICES PARA PERFORMANCE
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sessions_game_id ON game_sessions(game_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sessions_share_code ON game_sessions(share_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sessions_status ON game_sessions(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sync_events_session_id ON game_sync_events(session_id);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CRIAR TRIGGER AUTOMÁTICO PARA NOVOS UTILIZADORES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.profiles (id, username)
|
||||||
|
VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.email))
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Drop the trigger if it exists and create it
|
||||||
|
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||||
|
CREATE TRIGGER on_auth_user_created
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user();
|
||||||
127
lib/controllers/active_team.dart
Normal file
127
lib/controllers/active_team.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
class ActiveTeam {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String? logo;
|
||||||
|
final int wins;
|
||||||
|
final int losses;
|
||||||
|
final int draws;
|
||||||
|
|
||||||
|
ActiveTeam({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.logo,
|
||||||
|
this.wins = 0,
|
||||||
|
this.losses = 0,
|
||||||
|
this.draws = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🟢 A MÁGICA: Esta variável avisa a Home e a StatusPage ao mesmo tempo quando a equipa muda!
|
||||||
|
final ValueNotifier<ActiveTeam?> globalActiveTeam = ValueNotifier(null);
|
||||||
|
|
||||||
|
// 🟢 FUNÇÃO PARA CARREGAR A EQUIPA AO ABRIR A APP (Lê da Memória e do Supabase)
|
||||||
|
Future<void> loadGlobalTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
// 1. Carrega rápido da memória (para não piscar o ecrã)
|
||||||
|
if (savedId != null) {
|
||||||
|
globalActiveTeam.value = ActiveTeam(
|
||||||
|
id: savedId,
|
||||||
|
name: prefs.getString('last_team_name') ?? "Selecionar Equipa",
|
||||||
|
logo: prefs.getString('last_team_logo'),
|
||||||
|
wins: prefs.getInt('last_team_wins') ?? 0,
|
||||||
|
losses: prefs.getInt('last_team_losses') ?? 0,
|
||||||
|
draws: prefs.getInt('last_team_draws') ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Vai confirmar no Supabase se entraste com esta conta noutro telemóvel!
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) Prefer an explicit team selection stored on the user's profile (if any)
|
||||||
|
Map<String, dynamic>? teamData;
|
||||||
|
final profile = await supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
|
||||||
|
if (profile != null && profile['selected_team_id'] != null) {
|
||||||
|
final dbTeamId = profile['selected_team_id'].toString();
|
||||||
|
final dbTeam = await supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
|
||||||
|
if (dbTeam != null) teamData = Map<String, dynamic>.from(dbTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) If the user has no explicit profile selection, fall back to any team
|
||||||
|
// marked as favorite for that user (acts as a default)
|
||||||
|
if (teamData == null) {
|
||||||
|
final favTeam = await supabase
|
||||||
|
.from('teams')
|
||||||
|
.select()
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.eq('is_favorite', true)
|
||||||
|
.maybeSingle();
|
||||||
|
if (favTeam != null) teamData = Map<String, dynamic>.from(favTeam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a team (favorite or profile selection), set it as active and persist locally
|
||||||
|
if (teamData != null) {
|
||||||
|
final newTeam = ActiveTeam(
|
||||||
|
id: teamData['id'].toString(),
|
||||||
|
name: teamData['name'] ?? 'Desconhecido',
|
||||||
|
logo: teamData['image_url'],
|
||||||
|
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
|
||||||
|
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
|
||||||
|
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
|
||||||
|
);
|
||||||
|
globalActiveTeam.value = newTeam;
|
||||||
|
|
||||||
|
// Atualiza a memória do telemóvel para a próxima vez ser rápido
|
||||||
|
await prefs.setString('last_team_id', newTeam.id);
|
||||||
|
await prefs.setString('last_team_name', newTeam.name);
|
||||||
|
if (newTeam.logo != null && newTeam.logo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', newTeam.logo!);
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', newTeam.wins);
|
||||||
|
await prefs.setInt('last_team_losses', newTeam.losses);
|
||||||
|
await prefs.setInt('last_team_draws', newTeam.draws);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🟢 FUNÇÃO PARA GUARDAR A EQUIPA (Na Memória e no Supabase)
|
||||||
|
Future<void> saveGlobalTeam(ActiveTeam team) async {
|
||||||
|
globalActiveTeam.value = team; // Atualiza a app inteira!
|
||||||
|
|
||||||
|
// 1. Guarda no telemóvel
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString('last_team_id', team.id);
|
||||||
|
await prefs.setString('last_team_name', team.name);
|
||||||
|
if (team.logo != null && team.logo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', team.logo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', team.wins);
|
||||||
|
await prefs.setInt('last_team_losses', team.losses);
|
||||||
|
await prefs.setInt('last_team_draws', team.draws);
|
||||||
|
|
||||||
|
// 2. Guarda no Supabase!
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null) {
|
||||||
|
try {
|
||||||
|
await supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': team.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import '../utils/session_manager.dart';
|
||||||
import '../models/game_model.dart';
|
import '../models/game_model.dart';
|
||||||
|
|
||||||
class GameController {
|
class GameController {
|
||||||
@@ -53,6 +54,9 @@ class GameController {
|
|||||||
// CRIAR JOGO
|
// CRIAR JOGO
|
||||||
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
||||||
try {
|
try {
|
||||||
|
// Marca que existe uma sessão/jogo em progresso localmente
|
||||||
|
// (será limpa quando o jogo terminar ou em falha)
|
||||||
|
await SessionManager.setInProgress(true);
|
||||||
final response = await _supabase.from('games').insert({
|
final response = await _supabase.from('games').insert({
|
||||||
'user_id': myUserId,
|
'user_id': myUserId,
|
||||||
'my_team': myTeam,
|
'my_team': myTeam,
|
||||||
@@ -77,6 +81,10 @@ class GameController {
|
|||||||
return response['id']?.toString();
|
return response['id']?.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erro ao criar jogo: $e");
|
print("Erro ao criar jogo: $e");
|
||||||
|
// Se houve erro, limpa o flag para não exigir logout indevido
|
||||||
|
try {
|
||||||
|
await SessionManager.clear();
|
||||||
|
} catch (_) {}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
281
lib/controllers/game_sharing_controller.dart
Normal file
281
lib/controllers/game_sharing_controller.dart
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
// ...existing code...
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
class GameSharingController {
|
||||||
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
String get myUserId => _supabase.auth.currentUser?.id ?? '';
|
||||||
|
String get myUserEmail => _supabase.auth.currentUser?.email ?? '';
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 1️⃣ GERAR CÓDIGO E CRIAR SESSÃO
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<String?> createShareSession(String gameId) async {
|
||||||
|
try {
|
||||||
|
final shareCode = _generateShareCode();
|
||||||
|
|
||||||
|
final response = await _supabase.from('game_sessions').insert({
|
||||||
|
'game_id': gameId,
|
||||||
|
'created_by': myUserId,
|
||||||
|
'share_code': shareCode,
|
||||||
|
'status': 'active',
|
||||||
|
}).select().single();
|
||||||
|
|
||||||
|
return shareCode;
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Erro ao criar sessão de compartilhamento: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 2️⃣ ENTRAR EM JOGO COMPARTILHADO
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> joinGameByCode(String shareCode) async {
|
||||||
|
try {
|
||||||
|
print("🔍 Procurando sessão com código: $shareCode");
|
||||||
|
// Procura a sessão pelo código
|
||||||
|
final sessions = await _supabase
|
||||||
|
.from('game_sessions')
|
||||||
|
.select()
|
||||||
|
.eq('share_code', shareCode.toUpperCase())
|
||||||
|
.eq('status', 'active');
|
||||||
|
|
||||||
|
print("📋 Sessões encontradas: ${sessions.length}");
|
||||||
|
if (sessions.isEmpty) {
|
||||||
|
print("❌ Código inválido ou expirado");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final session = sessions.first;
|
||||||
|
final gameId = session['game_id'] as String;
|
||||||
|
final createdBy = session['created_by'] as String;
|
||||||
|
|
||||||
|
print("🎮 Game ID: $gameId, Criado por: $createdBy");
|
||||||
|
|
||||||
|
// Garante que o utilizador atual tem perfil
|
||||||
|
print("👤 Verificando perfil do utilizador: $myUserId");
|
||||||
|
await _ensureUserProfile();
|
||||||
|
|
||||||
|
// Atualiza a sessão para adicionar o utilizador que está a entrar
|
||||||
|
await _supabase.from('game_sessions').update({
|
||||||
|
'shared_with_user_id': myUserId,
|
||||||
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
|
}).eq('id', session['id']);
|
||||||
|
|
||||||
|
print("✅ Sessão atualizada com novo utilizador");
|
||||||
|
|
||||||
|
// Busca informações do jogo
|
||||||
|
final gameData = await _supabase
|
||||||
|
.from('games')
|
||||||
|
.select()
|
||||||
|
.eq('id', gameId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
print("📊 Dados do jogo: $gameData");
|
||||||
|
|
||||||
|
// Busca o nome do utilizador que criou
|
||||||
|
final creatorData = await _supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('username, full_name')
|
||||||
|
.eq('id', createdBy)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
final creatorName = creatorData != null
|
||||||
|
? (creatorData['full_name'] ?? creatorData['username'] ?? 'Utilizador')
|
||||||
|
: 'Utilizador';
|
||||||
|
|
||||||
|
print("👤 Criador: $creatorName");
|
||||||
|
|
||||||
|
return {
|
||||||
|
'session_id': session['id'],
|
||||||
|
'game_id': gameId,
|
||||||
|
'creator_name': creatorName,
|
||||||
|
'game': gameData,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Erro ao entrar no jogo: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// GARANTIR QUE UTILIZADOR TEM PERFIL
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<void> _ensureUserProfile() async {
|
||||||
|
try {
|
||||||
|
final user = _supabase.auth.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
// Verifica se o perfil existe
|
||||||
|
final existing = await _supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select()
|
||||||
|
.eq('id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
// Cria o perfil se não existir - usa apenas colunas básicas
|
||||||
|
print("📝 Criando perfil para novo utilizador");
|
||||||
|
await _supabase.from('profiles').upsert({
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.email?.split('@').first ?? 'user',
|
||||||
|
}, onConflict: 'id');
|
||||||
|
print("✅ Perfil criado com sucesso");
|
||||||
|
} else {
|
||||||
|
print("✅ Perfil já existe");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("⚠️ Aviso ao verificar/criar perfil: $e");
|
||||||
|
// Não falha o join se o perfil já existe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 3️⃣ OBTER INFORMAÇÕES DA SESSÃO
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getSessionInfo(String sessionId) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('game_sessions')
|
||||||
|
.select()
|
||||||
|
.eq('id', sessionId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Erro ao buscar informações da sessão: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 4️⃣ ENVIAR EVENTO DE SINCRONIZAÇÃO
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<bool> sendSyncEvent(
|
||||||
|
String sessionId,
|
||||||
|
String actionType,
|
||||||
|
Map<String, dynamic> actionData, {
|
||||||
|
String? playerId, // opcional: identifica jogador/entidade alvo
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _supabase.from('game_sync_events').insert({
|
||||||
|
'session_id': sessionId,
|
||||||
|
'action_type': actionType,
|
||||||
|
'action_data': actionData,
|
||||||
|
'triggered_by': myUserId,
|
||||||
|
if (playerId != null) 'player_id': playerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
print("✅ Evento sincronizado: $actionType");
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Erro ao enviar evento de sincronização: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 5️⃣ OUVIR EVENTOS EM TEMPO REAL
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Stream<dynamic> listenToGameSync(String sessionId) {
|
||||||
|
return _supabase
|
||||||
|
.from('game_sync_events')
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.eq('session_id', sessionId)
|
||||||
|
.order('created_at', ascending: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retorna apenas os eventos que NÃO foram disparados pelo utilizador atual.
|
||||||
|
/// Emite uma lista de eventos (List<Map<String, dynamic>>) por cada atualização.
|
||||||
|
Stream<List<Map<String, dynamic>>> listenToGameSyncOthers(String sessionId) {
|
||||||
|
return listenToGameSync(sessionId).map((data) {
|
||||||
|
List<Map<String, dynamic>> rows = [];
|
||||||
|
try {
|
||||||
|
if (data is List) {
|
||||||
|
rows = List<Map<String, dynamic>>.from(data);
|
||||||
|
} else if (data is Map) {
|
||||||
|
rows = [Map<String, dynamic>.from(data)];
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
return <Map<String, dynamic>>[];
|
||||||
|
}
|
||||||
|
return rows.where((r) => (r['triggered_by'] as String?) != myUserId).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 6️⃣ OBTER ÚLTIMOS EVENTOS
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> getRecentSyncEvents(String sessionId, {int limit = 10}) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('game_sync_events')
|
||||||
|
.select()
|
||||||
|
.eq('session_id', sessionId)
|
||||||
|
.order('created_at', ascending: false)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
return List<Map<String, dynamic>>.from(response);
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Erro ao buscar eventos: $e");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 7️⃣ TERMINAR SESSÃO COMPARTILHADA
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<bool> endShareSession(String sessionId) async {
|
||||||
|
try {
|
||||||
|
await _supabase
|
||||||
|
.from('game_sessions')
|
||||||
|
.update({'status': 'ended'})
|
||||||
|
.eq('id', sessionId);
|
||||||
|
|
||||||
|
print("✅ Sessão terminada");
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Erro ao terminar sessão: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 8️⃣ OBTER SESSÃO ATIVA DO JOGO
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getActiveSessionForGame(String gameId) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('game_sessions')
|
||||||
|
.select()
|
||||||
|
.eq('game_id', gameId)
|
||||||
|
.eq('status', 'active')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
return null; // Sem sessão ativa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// FUNÇÕES PRIVADAS
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
String _generateShareCode() {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
final random = Random();
|
||||||
|
return List.generate(6, (index) => chars[random.nextInt(chars.length)]).join();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,13 +25,23 @@ class ShotRecord {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'relativeX': relativeX, 'relativeY': relativeY, 'isMake': isMake,
|
'relativeX': relativeX,
|
||||||
'playerId': playerId, 'playerName': playerName, 'zone': zone, 'points': points,
|
'relativeY': relativeY,
|
||||||
|
'isMake': isMake,
|
||||||
|
'playerId': playerId,
|
||||||
|
'playerName': playerName,
|
||||||
|
'zone': zone,
|
||||||
|
'points': points,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord(
|
factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord(
|
||||||
relativeX: json['relativeX'], relativeY: json['relativeY'], isMake: json['isMake'],
|
relativeX: json['relativeX'],
|
||||||
playerId: json['playerId'], playerName: json['playerName'], zone: json['zone'], points: json['points'],
|
relativeY: json['relativeY'],
|
||||||
|
isMake: json['isMake'],
|
||||||
|
playerId: json['playerId'],
|
||||||
|
playerName: json['playerName'],
|
||||||
|
zone: json['zone'],
|
||||||
|
points: json['points'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,13 +49,126 @@ class PlacarController extends ChangeNotifier {
|
|||||||
final String gameId;
|
final String gameId;
|
||||||
final String myTeam;
|
final String myTeam;
|
||||||
final String opponentTeam;
|
final String opponentTeam;
|
||||||
|
final void Function(String actionType, Map<String, dynamic> actionData)?
|
||||||
|
onSyncAction;
|
||||||
|
|
||||||
PlacarController({
|
PlacarController({
|
||||||
required this.gameId,
|
required this.gameId,
|
||||||
required this.myTeam,
|
required this.myTeam,
|
||||||
required this.opponentTeam,
|
required this.opponentTeam,
|
||||||
|
this.onSyncAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _dispatchSyncAction(String actionType, Map<String, dynamic> actionData) {
|
||||||
|
if (onSyncAction != null) {
|
||||||
|
final enrichedActionData = Map<String, dynamic>.from(actionData)
|
||||||
|
..['remaining_seconds'] = durationNotifier.value.inSeconds
|
||||||
|
..['is_running'] = isRunning
|
||||||
|
..['current_quarter'] = currentQuarter
|
||||||
|
..['my_fouls'] = myFouls
|
||||||
|
..['opponent_fouls'] = opponentFouls
|
||||||
|
..['my_timeouts_used'] = myTimeoutsUsed
|
||||||
|
..['opponent_timeouts_used'] = opponentTimeoutsUsed;
|
||||||
|
onSyncAction!(actionType, enrichedActionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startTimer() {
|
||||||
|
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (!isRunning) return;
|
||||||
|
|
||||||
|
if (durationNotifier.value.inSeconds > 0) {
|
||||||
|
void addTimeToCourt(List<String> court) {
|
||||||
|
for (String id in court) {
|
||||||
|
if (playerStats.containsKey(id)) {
|
||||||
|
int currentSec = playerStats[id]!['sec'] ?? 0;
|
||||||
|
playerStats[id]!['sec'] = currentSec + 1;
|
||||||
|
playerStats[id]!['min'] = (currentSec + 1) ~/ 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimeToCourt(myCourt);
|
||||||
|
addTimeToCourt(oppCourt);
|
||||||
|
durationNotifier.value -= const Duration(seconds: 1);
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
isRunning = false;
|
||||||
|
if (currentQuarter < 4) {
|
||||||
|
currentQuarter++;
|
||||||
|
durationNotifier.value = const Duration(minutes: 10);
|
||||||
|
myFouls = 0;
|
||||||
|
opponentFouls = 0;
|
||||||
|
myTimeoutsUsed = 0;
|
||||||
|
opponentTimeoutsUsed = 0;
|
||||||
|
_scheduleAutoSave();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
_dispatchSyncAction('period_ended', {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setTimerRunning(bool shouldRun, {bool emitSync = true}) {
|
||||||
|
print("🔧 _setTimerRunning: shouldRun=$shouldRun, isRunning=$isRunning");
|
||||||
|
if (shouldRun == isRunning) {
|
||||||
|
print("🔧 Guardado: shouldRun == isRunning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = shouldRun;
|
||||||
|
if (!shouldRun) {
|
||||||
|
print("🛑 Cancelando timer");
|
||||||
|
timer?.cancel();
|
||||||
|
_scheduleAutoSave();
|
||||||
|
} else {
|
||||||
|
print("▶️ Iniciando timer");
|
||||||
|
_startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
print("✅ notifyListeners chamado");
|
||||||
|
|
||||||
|
if (emitSync) {
|
||||||
|
print("📡 Despachando sync action");
|
||||||
|
_dispatchSyncAction('toggle_timer', {'is_running': isRunning});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyRemoteTimerState(bool shouldRun) {
|
||||||
|
_setTimerRunning(shouldRun, emitSync: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyRemoteAddShot(Map<String, dynamic> shotJson) {
|
||||||
|
try {
|
||||||
|
matchShots.add(ShotRecord.fromJson(shotJson));
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erro ao aplicar shot remoto: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistShotRemote(Map<String, dynamic> shotJson) async {
|
||||||
|
try {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final row = {
|
||||||
|
'game_id': gameId,
|
||||||
|
'member_id': shotJson['playerId'] ?? shotJson['player_id'],
|
||||||
|
'player_name': shotJson['playerName'] ?? shotJson['player_name'],
|
||||||
|
'relative_x': shotJson['relativeX'] ?? shotJson['relative_x'],
|
||||||
|
'relative_y': shotJson['relativeY'] ?? shotJson['relative_y'],
|
||||||
|
'is_make': shotJson['isMake'] ?? shotJson['is_make'],
|
||||||
|
'zone': shotJson['zone'],
|
||||||
|
'points': shotJson['points'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await supabase.from('shot_locations').insert(row);
|
||||||
|
debugPrint('✅ Shot persisted remotely');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Erro ao persistir shot remoto: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
bool isSaving = false;
|
bool isSaving = false;
|
||||||
bool gameWasAlreadyFinished = false;
|
bool gameWasAlreadyFinished = false;
|
||||||
@@ -80,7 +203,9 @@ class PlacarController extends ChangeNotifier {
|
|||||||
|
|
||||||
List<String> playByPlay = [];
|
List<String> playByPlay = [];
|
||||||
|
|
||||||
ValueNotifier<Duration> durationNotifier = ValueNotifier(const Duration(minutes: 10));
|
ValueNotifier<Duration> durationNotifier = ValueNotifier(
|
||||||
|
const Duration(minutes: 10),
|
||||||
|
);
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
bool isRunning = false;
|
bool isRunning = false;
|
||||||
|
|
||||||
@@ -96,21 +221,41 @@ class PlacarController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
await Future.delayed(const Duration(milliseconds: 1500));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
|
|
||||||
myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear();
|
myCourt.clear();
|
||||||
playerNames.clear(); playerStats.clear(); playerNumbers.clear();
|
myBench.clear();
|
||||||
matchShots.clear(); playByPlay.clear(); myFouls = 0; opponentFouls = 0;
|
oppCourt.clear();
|
||||||
|
oppBench.clear();
|
||||||
|
playerNames.clear();
|
||||||
|
playerStats.clear();
|
||||||
|
playerNumbers.clear();
|
||||||
|
matchShots.clear();
|
||||||
|
playByPlay.clear();
|
||||||
|
myFouls = 0;
|
||||||
|
opponentFouls = 0;
|
||||||
|
|
||||||
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
|
final gameResponse = await supabase
|
||||||
|
.from('games')
|
||||||
|
.select()
|
||||||
|
.eq('id', gameId)
|
||||||
|
.single();
|
||||||
|
|
||||||
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
|
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
|
||||||
opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
|
opponentScore =
|
||||||
|
int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
|
||||||
|
|
||||||
int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600;
|
int totalSeconds =
|
||||||
|
int.tryParse(
|
||||||
|
gameResponse['remaining_seconds']?.toString() ?? '600',
|
||||||
|
) ??
|
||||||
|
600;
|
||||||
durationNotifier.value = Duration(seconds: totalSeconds);
|
durationNotifier.value = Duration(seconds: totalSeconds);
|
||||||
|
|
||||||
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
myTimeoutsUsed =
|
||||||
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
||||||
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
|
opponentTimeoutsUsed =
|
||||||
|
int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
||||||
|
currentQuarter =
|
||||||
|
int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
|
||||||
|
|
||||||
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
|
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
|
||||||
|
|
||||||
@@ -120,25 +265,49 @@ class PlacarController extends ChangeNotifier {
|
|||||||
playByPlay = [];
|
playByPlay = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
final teamsResponse = await supabase
|
||||||
|
.from('teams')
|
||||||
|
.select('id, name')
|
||||||
|
.inFilter('name', [myTeam, opponentTeam]);
|
||||||
for (var t in teamsResponse) {
|
for (var t in teamsResponse) {
|
||||||
if (t['name'] == myTeam) myTeamDbId = t['id'];
|
if (t['name'] == myTeam) myTeamDbId = t['id'];
|
||||||
if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
|
if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : [];
|
List<dynamic> myPlayers = myTeamDbId != null
|
||||||
List<dynamic> oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : [];
|
? await supabase
|
||||||
|
.from('members')
|
||||||
|
.select()
|
||||||
|
.eq('team_id', myTeamDbId!)
|
||||||
|
.eq('type', 'Jogador')
|
||||||
|
: [];
|
||||||
|
List<dynamic> oppPlayers = oppTeamDbId != null
|
||||||
|
? await supabase
|
||||||
|
.from('members')
|
||||||
|
.select()
|
||||||
|
.eq('team_id', oppTeamDbId!)
|
||||||
|
.eq('type', 'Jogador')
|
||||||
|
: [];
|
||||||
|
|
||||||
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
|
final statsResponse = await supabase
|
||||||
|
.from('player_stats')
|
||||||
|
.select()
|
||||||
|
.eq('game_id', gameId);
|
||||||
final Map<String, dynamic> savedStats = {
|
final Map<String, dynamic> savedStats = {
|
||||||
for (var item in statsResponse) item['member_id'].toString(): item
|
for (var item in statsResponse) item['member_id'].toString(): item,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < myPlayers.length; i++) {
|
for (int i = 0; i < myPlayers.length; i++) {
|
||||||
String dbId = myPlayers[i]['id'].toString();
|
String dbId = myPlayers[i]['id'].toString();
|
||||||
String name = myPlayers[i]['name'].toString();
|
String name = myPlayers[i]['name'].toString();
|
||||||
|
|
||||||
_registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5);
|
_registerPlayer(
|
||||||
|
name: name,
|
||||||
|
number: myPlayers[i]['number']?.toString() ?? "0",
|
||||||
|
dbId: dbId,
|
||||||
|
isMyTeam: true,
|
||||||
|
isCourt: i < 5,
|
||||||
|
);
|
||||||
|
|
||||||
if (savedStats.containsKey(dbId)) {
|
if (savedStats.containsKey(dbId)) {
|
||||||
var s = savedStats[dbId];
|
var s = savedStats[dbId];
|
||||||
@@ -152,7 +321,13 @@ class PlacarController extends ChangeNotifier {
|
|||||||
String dbId = oppPlayers[i]['id'].toString();
|
String dbId = oppPlayers[i]['id'].toString();
|
||||||
String name = oppPlayers[i]['name'].toString();
|
String name = oppPlayers[i]['name'].toString();
|
||||||
|
|
||||||
_registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5);
|
_registerPlayer(
|
||||||
|
name: name,
|
||||||
|
number: oppPlayers[i]['number']?.toString() ?? "0",
|
||||||
|
dbId: dbId,
|
||||||
|
isMyTeam: false,
|
||||||
|
isCourt: i < 5,
|
||||||
|
);
|
||||||
|
|
||||||
if (savedStats.containsKey(dbId)) {
|
if (savedStats.containsKey(dbId)) {
|
||||||
var s = savedStats[dbId];
|
var s = savedStats[dbId];
|
||||||
@@ -162,17 +337,24 @@ class PlacarController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
|
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
|
||||||
|
|
||||||
final shotsResponse = await supabase.from('shot_locations').select().eq('game_id', gameId);
|
final shotsResponse = await supabase
|
||||||
|
.from('shot_locations')
|
||||||
|
.select()
|
||||||
|
.eq('game_id', gameId);
|
||||||
for (var shotData in shotsResponse) {
|
for (var shotData in shotsResponse) {
|
||||||
matchShots.add(ShotRecord(
|
matchShots.add(
|
||||||
relativeX: double.parse(shotData['relative_x'].toString()),
|
ShotRecord(
|
||||||
relativeY: double.parse(shotData['relative_y'].toString()),
|
relativeX: double.parse(shotData['relative_x'].toString()),
|
||||||
isMake: shotData['is_make'] == true,
|
relativeY: double.parse(shotData['relative_y'].toString()),
|
||||||
playerId: shotData['member_id'].toString(),
|
isMake: shotData['is_make'] == true,
|
||||||
playerName: shotData['player_name'].toString(),
|
playerId: shotData['member_id'].toString(),
|
||||||
zone: shotData['zone']?.toString(),
|
playerName: shotData['player_name'].toString(),
|
||||||
points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null,
|
zone: shotData['zone']?.toString(),
|
||||||
));
|
points: shotData['points'] != null
|
||||||
|
? int.parse(shotData['points'].toString())
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _loadLocalBackup();
|
await _loadLocalBackup();
|
||||||
@@ -188,42 +370,103 @@ class PlacarController extends ChangeNotifier {
|
|||||||
|
|
||||||
void _loadSavedPlayerStats(String dbId, Map<String, dynamic> s) {
|
void _loadSavedPlayerStats(String dbId, Map<String, dynamic> s) {
|
||||||
playerStats[dbId] = {
|
playerStats[dbId] = {
|
||||||
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
|
"pts": s['pts'] ?? 0,
|
||||||
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0,
|
"rbs": s['rbs'] ?? 0,
|
||||||
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
|
"ast": s['ast'] ?? 0,
|
||||||
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
|
"stl": s['stl'] ?? 0,
|
||||||
"p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0,
|
"tov": s['tov'] ?? 0,
|
||||||
"so": s['so'] ?? 0, "il": s['il'] ?? 0, "li": s['li'] ?? 0,
|
"blk": s['blk'] ?? 0,
|
||||||
"pa": s['pa'] ?? 0, "tres_seg": s['tres_seg'] ?? 0, "dr": s['dr'] ?? 0,
|
"fls": s['fls'] ?? 0,
|
||||||
|
"fgm": s['fgm'] ?? 0,
|
||||||
|
"fga": s['fga'] ?? 0,
|
||||||
|
"ftm": s['ftm'] ?? 0,
|
||||||
|
"fta": s['fta'] ?? 0,
|
||||||
|
"orb": s['orb'] ?? 0,
|
||||||
|
"drb": s['drb'] ?? 0,
|
||||||
|
"p2m": s['p2m'] ?? 0,
|
||||||
|
"p2a": s['p2a'] ?? 0,
|
||||||
|
"p3m": s['p3m'] ?? 0,
|
||||||
|
"p3a": s['p3a'] ?? 0,
|
||||||
|
"so": s['so'] ?? 0,
|
||||||
|
"il": s['il'] ?? 0,
|
||||||
|
"li": s['li'] ?? 0,
|
||||||
|
"pa": s['pa'] ?? 0,
|
||||||
|
"tres_seg": s['tres_seg'] ?? 0,
|
||||||
|
"dr": s['dr'] ?? 0,
|
||||||
"min": (s['minutos_jogados'] ?? 0) ~/ 60,
|
"min": (s['minutos_jogados'] ?? 0) ~/ 60,
|
||||||
"sec": s['minutos_jogados'] ?? 0,
|
"sec": s['minutos_jogados'] ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) {
|
void _registerPlayer({
|
||||||
String id = dbId ?? "fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}";
|
required String name,
|
||||||
|
required String number,
|
||||||
|
String? dbId,
|
||||||
|
required bool isMyTeam,
|
||||||
|
required bool isCourt,
|
||||||
|
}) {
|
||||||
|
String id =
|
||||||
|
dbId ??
|
||||||
|
"fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}";
|
||||||
|
|
||||||
playerNames[id] = name;
|
playerNames[id] = name;
|
||||||
playerNumbers[id] = number;
|
playerNumbers[id] = number;
|
||||||
|
|
||||||
playerStats[id] = {
|
playerStats[id] = {
|
||||||
"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0,
|
"pts": 0,
|
||||||
"fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0,
|
"rbs": 0,
|
||||||
"p2m": 0, "p2a": 0, "p3m": 0, "p3a": 0,
|
"ast": 0,
|
||||||
"so": 0, "il": 0, "li": 0, "pa": 0, "tres_seg": 0, "dr": 0,
|
"stl": 0,
|
||||||
"min": 0, "sec": 0
|
"tov": 0,
|
||||||
|
"blk": 0,
|
||||||
|
"fls": 0,
|
||||||
|
"fgm": 0,
|
||||||
|
"fga": 0,
|
||||||
|
"ftm": 0,
|
||||||
|
"fta": 0,
|
||||||
|
"orb": 0,
|
||||||
|
"drb": 0,
|
||||||
|
"p2m": 0,
|
||||||
|
"p2a": 0,
|
||||||
|
"p3m": 0,
|
||||||
|
"p3a": 0,
|
||||||
|
"so": 0,
|
||||||
|
"il": 0,
|
||||||
|
"li": 0,
|
||||||
|
"pa": 0,
|
||||||
|
"tres_seg": 0,
|
||||||
|
"dr": 0,
|
||||||
|
"min": 0,
|
||||||
|
"sec": 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMyTeam) {
|
if (isMyTeam) {
|
||||||
if (isCourt) myCourt.add(id); else myBench.add(id);
|
if (isCourt)
|
||||||
|
myCourt.add(id);
|
||||||
|
else
|
||||||
|
myBench.add(id);
|
||||||
} else {
|
} else {
|
||||||
if (isCourt) oppCourt.add(id); else oppBench.add(id);
|
if (isCourt)
|
||||||
|
oppCourt.add(id);
|
||||||
|
else
|
||||||
|
oppBench.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _padTeam(List<String> court, List<String> bench, String prefix, {required bool isMyTeam}) {
|
void _padTeam(
|
||||||
|
List<String> court,
|
||||||
|
List<String> bench,
|
||||||
|
String prefix, {
|
||||||
|
required bool isMyTeam,
|
||||||
|
}) {
|
||||||
while (court.length < 5) {
|
while (court.length < 5) {
|
||||||
_registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true);
|
_registerPlayer(
|
||||||
|
name: "Sem $prefix ${court.length + 1}",
|
||||||
|
number: "0",
|
||||||
|
dbId: null,
|
||||||
|
isMyTeam: isMyTeam,
|
||||||
|
isCourt: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,12 +481,19 @@ class PlacarController extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final backupData = {
|
final backupData = {
|
||||||
'myScore': myScore, 'opponentScore': opponentScore,
|
'myScore': myScore,
|
||||||
'myFouls': myFouls, 'opponentFouls': opponentFouls,
|
'opponentScore': opponentScore,
|
||||||
'currentQuarter': currentQuarter, 'duration': durationNotifier.value.inSeconds,
|
'myFouls': myFouls,
|
||||||
'myTimeoutsUsed': myTimeoutsUsed, 'opponentTimeoutsUsed': opponentTimeoutsUsed,
|
'opponentFouls': opponentFouls,
|
||||||
|
'currentQuarter': currentQuarter,
|
||||||
|
'duration': durationNotifier.value.inSeconds,
|
||||||
|
'myTimeoutsUsed': myTimeoutsUsed,
|
||||||
|
'opponentTimeoutsUsed': opponentTimeoutsUsed,
|
||||||
'playerStats': playerStats,
|
'playerStats': playerStats,
|
||||||
'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench,
|
'myCourt': myCourt,
|
||||||
|
'myBench': myBench,
|
||||||
|
'oppCourt': oppCourt,
|
||||||
|
'oppBench': oppBench,
|
||||||
'matchShots': matchShots.map((s) => s.toJson()).toList(),
|
'matchShots': matchShots.map((s) => s.toJson()).toList(),
|
||||||
'playByPlay': playByPlay,
|
'playByPlay': playByPlay,
|
||||||
};
|
};
|
||||||
@@ -261,16 +511,24 @@ class PlacarController extends ChangeNotifier {
|
|||||||
if (backupString != null) {
|
if (backupString != null) {
|
||||||
final data = jsonDecode(backupString);
|
final data = jsonDecode(backupString);
|
||||||
|
|
||||||
myScore = data['myScore']; opponentScore = data['opponentScore'];
|
myScore = data['myScore'];
|
||||||
myFouls = data['myFouls']; opponentFouls = data['opponentFouls'];
|
opponentScore = data['opponentScore'];
|
||||||
currentQuarter = data['currentQuarter']; durationNotifier.value = Duration(seconds: data['duration']);
|
myFouls = data['myFouls'];
|
||||||
myTimeoutsUsed = data['myTimeoutsUsed']; opponentTimeoutsUsed = data['opponentTimeoutsUsed'];
|
opponentFouls = data['opponentFouls'];
|
||||||
|
currentQuarter = data['currentQuarter'];
|
||||||
|
durationNotifier.value = Duration(seconds: data['duration']);
|
||||||
|
myTimeoutsUsed = data['myTimeoutsUsed'];
|
||||||
|
opponentTimeoutsUsed = data['opponentTimeoutsUsed'];
|
||||||
|
|
||||||
myCourt = List<String>.from(data['myCourt']); myBench = List<String>.from(data['myBench']);
|
myCourt = List<String>.from(data['myCourt']);
|
||||||
oppCourt = List<String>.from(data['oppCourt']); oppBench = List<String>.from(data['oppBench']);
|
myBench = List<String>.from(data['myBench']);
|
||||||
|
oppCourt = List<String>.from(data['oppCourt']);
|
||||||
|
oppBench = List<String>.from(data['oppBench']);
|
||||||
|
|
||||||
Map<String, dynamic> decodedStats = data['playerStats'];
|
Map<String, dynamic> decodedStats = data['playerStats'];
|
||||||
playerStats = decodedStats.map((k, v) => MapEntry(k, Map<String, int>.from(v)));
|
playerStats = decodedStats.map(
|
||||||
|
(k, v) => MapEntry(k, Map<String, int>.from(v)),
|
||||||
|
);
|
||||||
|
|
||||||
List<dynamic> decodedShots = data['matchShots'];
|
List<dynamic> decodedShots = data['matchShots'];
|
||||||
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
|
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
|
||||||
@@ -283,43 +541,8 @@ class PlacarController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void toggleTimer(BuildContext context) {
|
void toggleTimer(BuildContext context) {
|
||||||
if (isRunning) {
|
print("⏱️ toggleTimer chamado: isRunning=$isRunning");
|
||||||
timer?.cancel();
|
_setTimerRunning(!isRunning);
|
||||||
_scheduleAutoSave();
|
|
||||||
} else {
|
|
||||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
||||||
if (durationNotifier.value.inSeconds > 0) {
|
|
||||||
|
|
||||||
void addTimeToCourt(List<String> court) {
|
|
||||||
for (String id in court) {
|
|
||||||
if (playerStats.containsKey(id)) {
|
|
||||||
int currentSec = playerStats[id]!["sec"] ?? 0;
|
|
||||||
playerStats[id]!["sec"] = currentSec + 1;
|
|
||||||
playerStats[id]!["min"] = (currentSec + 1) ~/ 60;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addTimeToCourt(myCourt);
|
|
||||||
addTimeToCourt(oppCourt);
|
|
||||||
|
|
||||||
durationNotifier.value -= const Duration(seconds: 1);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
timer.cancel();
|
|
||||||
isRunning = false;
|
|
||||||
if (currentQuarter < 4) {
|
|
||||||
currentQuarter++;
|
|
||||||
durationNotifier.value = const Duration(minutes: 10);
|
|
||||||
myFouls = 0; opponentFouls = 0;
|
|
||||||
myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
|
|
||||||
_scheduleAutoSave();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
isRunning = !isRunning;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void useTimeout(bool isOpponent) {
|
void useTimeout(bool isOpponent) {
|
||||||
@@ -332,19 +555,34 @@ class PlacarController extends ChangeNotifier {
|
|||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
_scheduleAutoSave();
|
_scheduleAutoSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_dispatchSyncAction('use_timeout', {'is_opponent': isOpponent});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleActionDrag(BuildContext context, String action, String playerData) {
|
void handleActionDrag(
|
||||||
String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
BuildContext context,
|
||||||
|
String action,
|
||||||
|
String playerData,
|
||||||
|
) {
|
||||||
|
String playerId = playerData
|
||||||
|
.replaceAll("player_my_", "")
|
||||||
|
.replaceAll("player_opp_", "");
|
||||||
final stats = playerStats[playerId]!;
|
final stats = playerStats[playerId]!;
|
||||||
final name = playerNames[playerId]!;
|
final name = playerNames[playerId]!;
|
||||||
|
|
||||||
if (stats["fls"]! >= 5 && action != "sub_foul") {
|
if (stats["fls"]! >= 5 && action != "sub_foul") {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red));
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('🛑 $name atingiu 5 faltas e está expulso!'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
|
if (action == "add_pts_2" ||
|
||||||
|
action == "add_pts_3" ||
|
||||||
|
action == "miss_2" ||
|
||||||
|
action == "miss_3") {
|
||||||
pendingAction = action;
|
pendingAction = action;
|
||||||
pendingPlayerId = playerData;
|
pendingPlayerId = playerData;
|
||||||
isSelectingShotLocation = true;
|
isSelectingShotLocation = true;
|
||||||
@@ -354,7 +592,12 @@ class PlacarController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSubbing(BuildContext context, String action, String courtPlayerId, bool isOpponent) {
|
void handleSubbing(
|
||||||
|
BuildContext context,
|
||||||
|
String action,
|
||||||
|
String courtPlayerId,
|
||||||
|
bool isOpponent,
|
||||||
|
) {
|
||||||
if (action.startsWith("bench_my_") && !isOpponent) {
|
if (action.startsWith("bench_my_") && !isOpponent) {
|
||||||
String benchPlayerId = action.replaceAll("bench_my_", "");
|
String benchPlayerId = action.replaceAll("bench_my_", "");
|
||||||
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
|
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
|
||||||
@@ -375,20 +618,92 @@ class PlacarController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_scheduleAutoSave();
|
_scheduleAutoSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_dispatchSyncAction('subbing', {
|
||||||
|
'action': action,
|
||||||
|
'court_player': courtPlayerId,
|
||||||
|
'is_opponent': isOpponent,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) {
|
// ── TROCAR JOGADORES NO CAMPO ──────────────────────────────────────────────
|
||||||
String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
void swapCourtPlayers(String draggedPlayerData, String targetPlayerData) {
|
||||||
|
// Verifica se são da mesma equipa (Minha Equipa)
|
||||||
|
if (draggedPlayerData.startsWith("player_my_") &&
|
||||||
|
targetPlayerData.startsWith("player_my_")) {
|
||||||
|
String id1 = draggedPlayerData.replaceAll("player_my_", "");
|
||||||
|
String id2 = targetPlayerData.replaceAll("player_my_", "");
|
||||||
|
|
||||||
|
int idx1 = myCourt.indexOf(id1);
|
||||||
|
int idx2 = myCourt.indexOf(id2);
|
||||||
|
|
||||||
|
if (idx1 != -1 && idx2 != -1) {
|
||||||
|
myCourt[idx1] = id2;
|
||||||
|
myCourt[idx2] = id1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verifica se são da mesma equipa (Adversário)
|
||||||
|
else if (draggedPlayerData.startsWith("player_opp_") &&
|
||||||
|
targetPlayerData.startsWith("player_opp_")) {
|
||||||
|
String id1 = draggedPlayerData.replaceAll("player_opp_", "");
|
||||||
|
String id2 = targetPlayerData.replaceAll("player_opp_", "");
|
||||||
|
|
||||||
|
int idx1 = oppCourt.indexOf(id1);
|
||||||
|
int idx2 = oppCourt.indexOf(id2);
|
||||||
|
|
||||||
|
if (idx1 != -1 && idx2 != -1) {
|
||||||
|
oppCourt[idx1] = id2;
|
||||||
|
oppCourt[idx2] = id1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Se forem de equipas diferentes ou dados inválidos, ignora.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleAutoSave();
|
||||||
|
notifyListeners();
|
||||||
|
_dispatchSyncAction('swap_players', {
|
||||||
|
'dragged': draggedPlayerData,
|
||||||
|
'target': targetPlayerData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerShotFromPopup(
|
||||||
|
BuildContext context,
|
||||||
|
String action,
|
||||||
|
String targetPlayer,
|
||||||
|
String zone,
|
||||||
|
int points,
|
||||||
|
double relativeX,
|
||||||
|
double relativeY,
|
||||||
|
) {
|
||||||
|
String playerId = targetPlayer
|
||||||
|
.replaceAll("player_my_", "")
|
||||||
|
.replaceAll("player_opp_", "");
|
||||||
bool isMake = action.startsWith("add_");
|
bool isMake = action.startsWith("add_");
|
||||||
String name = playerNames[playerId] ?? "Jogador";
|
String name = playerNames[playerId] ?? "Jogador";
|
||||||
|
|
||||||
matchShots.add(ShotRecord(
|
matchShots.add(
|
||||||
relativeX: relativeX, relativeY: relativeY, isMake: isMake,
|
ShotRecord(
|
||||||
playerId: playerId, playerName: name, zone: zone, points: points
|
relativeX: relativeX,
|
||||||
));
|
relativeY: relativeY,
|
||||||
|
isMake: isMake,
|
||||||
|
playerId: playerId,
|
||||||
|
playerName: name,
|
||||||
|
zone: zone,
|
||||||
|
points: points,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
||||||
commitStat(finalAction, targetPlayer);
|
commitStat(finalAction, targetPlayer);
|
||||||
|
// Emitir evento de shot para parceiros remotos
|
||||||
|
try {
|
||||||
|
final shotJson = matchShots.last.toJson();
|
||||||
|
_dispatchSyncAction('add_shot', {'shot': shotJson});
|
||||||
|
// Persist shot immediately on server (fire-and-forget)
|
||||||
|
_persistShotRemote(shotJson);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,13 +721,33 @@ class PlacarController extends ChangeNotifier {
|
|||||||
bool isMake = pendingAction!.startsWith("add_pts_");
|
bool isMake = pendingAction!.startsWith("add_pts_");
|
||||||
double relX = position.dx / size.width;
|
double relX = position.dx / size.width;
|
||||||
double relY = position.dy / size.height;
|
double relY = position.dy / size.height;
|
||||||
String pId = pendingPlayerId!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
String pId = pendingPlayerId!
|
||||||
|
.replaceAll("player_my_", "")
|
||||||
|
.replaceAll("player_opp_", "");
|
||||||
|
|
||||||
matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerId: pId, playerName: playerNames[pId]!));
|
matchShots.add(
|
||||||
|
ShotRecord(
|
||||||
|
relativeX: relX,
|
||||||
|
relativeY: relY,
|
||||||
|
isMake: isMake,
|
||||||
|
playerId: pId,
|
||||||
|
playerName: playerNames[pId]!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Emitir evento de shot para parceiros remotos
|
||||||
|
try {
|
||||||
|
final shotJson = matchShots.last.toJson();
|
||||||
|
_dispatchSyncAction('add_shot', {'shot': shotJson});
|
||||||
|
// Persist shot immediately on server (fire-and-forget)
|
||||||
|
_persistShotRemote(shotJson);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
commitStat(pendingAction!, pendingPlayerId!);
|
commitStat(pendingAction!, pendingPlayerId!);
|
||||||
|
|
||||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null;
|
isSelectingShotLocation = false;
|
||||||
|
pendingAction = null;
|
||||||
|
pendingPlayerId = null;
|
||||||
_scheduleAutoSave();
|
_scheduleAutoSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -445,17 +780,25 @@ class PlacarController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cancelShotLocation() {
|
void cancelShotLocation() {
|
||||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; notifyListeners();
|
isSelectingShotLocation = false;
|
||||||
|
pendingAction = null;
|
||||||
|
pendingPlayerId = null;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerFoul(String committerData, String foulType, String victimData) {
|
void registerFoul(String committerData, String foulType, String victimData) {
|
||||||
bool isOpponent = committerData.startsWith("player_opp_");
|
bool isOpponent = committerData.startsWith("player_opp_");
|
||||||
String committerId = committerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
String committerId = committerData
|
||||||
|
.replaceAll("player_my_", "")
|
||||||
|
.replaceAll("player_opp_", "");
|
||||||
final committerStats = playerStats[committerId]!;
|
final committerStats = playerStats[committerId]!;
|
||||||
final committerName = playerNames[committerId] ?? "Jogador";
|
final committerName = playerNames[committerId] ?? "Jogador";
|
||||||
|
|
||||||
committerStats["fls"] = committerStats["fls"]! + 1;
|
committerStats["fls"] = committerStats["fls"]! + 1;
|
||||||
if (isOpponent) opponentFouls++; else myFouls++;
|
if (isOpponent)
|
||||||
|
opponentFouls++;
|
||||||
|
else
|
||||||
|
myFouls++;
|
||||||
|
|
||||||
if (foulType == "Desqualificante") {
|
if (foulType == "Desqualificante") {
|
||||||
committerStats["fls"] = 5;
|
committerStats["fls"] = 5;
|
||||||
@@ -464,7 +807,9 @@ class PlacarController extends ChangeNotifier {
|
|||||||
String logText = "cometeu Falta $foulType";
|
String logText = "cometeu Falta $foulType";
|
||||||
|
|
||||||
if (victimData.isNotEmpty) {
|
if (victimData.isNotEmpty) {
|
||||||
String victimId = victimData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
String victimId = victimData
|
||||||
|
.replaceAll("player_my_", "")
|
||||||
|
.replaceAll("player_opp_", "");
|
||||||
final victimStats = playerStats[victimId]!;
|
final victimStats = playerStats[victimId]!;
|
||||||
final victimName = playerNames[victimId] ?? "Jogador";
|
final victimName = playerNames[victimId] ?? "Jogador";
|
||||||
|
|
||||||
@@ -474,11 +819,17 @@ class PlacarController extends ChangeNotifier {
|
|||||||
logText += " (Equipa/Banco) ⚠️";
|
logText += " (Equipa/Banco) ⚠️";
|
||||||
}
|
}
|
||||||
|
|
||||||
String time = "${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
String time =
|
||||||
|
"${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
playByPlay.insert(0, "P$currentQuarter - $time: $committerName $logText");
|
playByPlay.insert(0, "P$currentQuarter - $time: $committerName $logText");
|
||||||
|
|
||||||
_scheduleAutoSave();
|
_scheduleAutoSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_dispatchSyncAction('register_foul', {
|
||||||
|
'committer': committerData,
|
||||||
|
'foulType': foulType,
|
||||||
|
'victim': victimData,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void commitStat(String action, String playerData) {
|
void commitStat(String action, String playerData) {
|
||||||
@@ -517,13 +868,14 @@ class PlacarController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
logText = "marcou $pts pontos 🏀";
|
logText = "marcou $pts pontos 🏀";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ANULAR PONTOS ────────────────────────────────────────────────────────
|
// ── ANULAR PONTOS ────────────────────────────────────────────────────────
|
||||||
else if (action.startsWith("sub_pts_")) {
|
else if (action.startsWith("sub_pts_")) {
|
||||||
int ptsToAnul = int.parse(action.split("_").last);
|
int ptsToAnul = int.parse(action.split("_").last);
|
||||||
|
|
||||||
int lastShotIndex = matchShots.lastIndexWhere((s) =>
|
int lastShotIndex = matchShots.lastIndexWhere(
|
||||||
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul);
|
(s) =>
|
||||||
|
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul,
|
||||||
|
);
|
||||||
|
|
||||||
if (lastShotIndex != -1) {
|
if (lastShotIndex != -1) {
|
||||||
matchShots.removeAt(lastShotIndex);
|
matchShots.removeAt(lastShotIndex);
|
||||||
@@ -552,7 +904,6 @@ class PlacarController extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FALHAS ───────────────────────────────────────────────────────────────
|
// ── FALHAS ───────────────────────────────────────────────────────────────
|
||||||
else if (action == "miss_1") {
|
else if (action == "miss_1") {
|
||||||
stats["fta"] = stats["fta"]! + 1;
|
stats["fta"] = stats["fta"]! + 1;
|
||||||
@@ -566,7 +917,6 @@ class PlacarController extends ChangeNotifier {
|
|||||||
stats["p3a"] = stats["p3a"]! + 1;
|
stats["p3a"] = stats["p3a"]! + 1;
|
||||||
logText = "falhou lançamento de 3 ❌";
|
logText = "falhou lançamento de 3 ❌";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── RESSALTOS ─────────────────────────────────────────────────────────────
|
// ── RESSALTOS ─────────────────────────────────────────────────────────────
|
||||||
else if (action == "add_orb") {
|
else if (action == "add_orb") {
|
||||||
stats["orb"] = stats["orb"]! + 1;
|
stats["orb"] = stats["orb"]! + 1;
|
||||||
@@ -577,19 +927,16 @@ class PlacarController extends ChangeNotifier {
|
|||||||
stats["rbs"] = stats["rbs"]! + 1;
|
stats["rbs"] = stats["rbs"]! + 1;
|
||||||
logText = "ganhou ressalto defensivo 🛡️";
|
logText = "ganhou ressalto defensivo 🛡️";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ASSISTÊNCIA ───────────────────────────────────────────────────────────
|
// ── ASSISTÊNCIA ───────────────────────────────────────────────────────────
|
||||||
else if (action == "add_ast") {
|
else if (action == "add_ast") {
|
||||||
stats["ast"] = stats["ast"]! + 1;
|
stats["ast"] = stats["ast"]! + 1;
|
||||||
logText = "fez uma assistência 🤝";
|
logText = "fez uma assistência 🤝";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SOFRIDAS ──────────────────────────────────────────────────────────────
|
// ── SOFRIDAS ──────────────────────────────────────────────────────────────
|
||||||
else if (action == "add_so") {
|
else if (action == "add_so") {
|
||||||
stats["so"] = stats["so"]! + 1;
|
stats["so"] = stats["so"]! + 1;
|
||||||
logText = "sofreu uma falta 🤕";
|
logText = "sofreu uma falta 🤕";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// STEAL — ROUBO DE BOLA
|
// STEAL — ROUBO DE BOLA
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -601,7 +948,6 @@ class PlacarController extends ChangeNotifier {
|
|||||||
stats["il"] = stats["il"]! + 1;
|
stats["il"] = stats["il"]! + 1;
|
||||||
logText = "intercetou um lançamento 🛑";
|
logText = "intercetou um lançamento 🛑";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// BLOCK — DESARME
|
// BLOCK — DESARME
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -612,7 +958,6 @@ class PlacarController extends ChangeNotifier {
|
|||||||
stats["li"] = stats["li"]! + 1;
|
stats["li"] = stats["li"]! + 1;
|
||||||
logText = "sofreu um desarme 🚫";
|
logText = "sofreu um desarme 🚫";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ações independentes legadas
|
// Ações independentes legadas
|
||||||
else if (action == "add_il") {
|
else if (action == "add_il") {
|
||||||
stats["il"] = stats["il"]! + 1;
|
stats["il"] = stats["il"]! + 1;
|
||||||
@@ -621,7 +966,6 @@ class PlacarController extends ChangeNotifier {
|
|||||||
stats["li"] = stats["li"]! + 1;
|
stats["li"] = stats["li"]! + 1;
|
||||||
logText = "teve o lançamento intercetado 🚫";
|
logText = "teve o lançamento intercetado 🚫";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// TURNOVER — PERDE DE BOLA E INFRAÇÕES
|
// TURNOVER — PERDE DE BOLA E INFRAÇÕES
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -630,21 +974,20 @@ class PlacarController extends ChangeNotifier {
|
|||||||
logText = "fez um passe ruim 🤦";
|
logText = "fez um passe ruim 🤦";
|
||||||
} else if (action == "tov_3s") {
|
} else if (action == "tov_3s") {
|
||||||
stats["tres_seg"] = stats["tres_seg"]! + 1; // SOMA AOS 3 SEGUNDOS
|
stats["tres_seg"] = stats["tres_seg"]! + 1; // SOMA AOS 3 SEGUNDOS
|
||||||
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
||||||
logText = "violação de 3 segundos ⏱️";
|
logText = "violação de 3 segundos ⏱️";
|
||||||
} else if (action == "tov_clock") {
|
} else if (action == "tov_clock") {
|
||||||
stats["tov"] = stats["tov"]! + 1;
|
stats["tov"] = stats["tov"]! + 1;
|
||||||
logText = "violação de 24 segundos ⏱️";
|
logText = "violação de 24 segundos ⏱️";
|
||||||
} else if (action == "tov_travel") {
|
} else if (action == "tov_travel") {
|
||||||
stats["pa"] = stats["pa"]! + 1; // SOMA AOS PASSOS
|
stats["pa"] = stats["pa"]! + 1; // SOMA AOS PASSOS
|
||||||
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
||||||
logText = "cometeu passos 🚶";
|
logText = "cometeu passos 🚶";
|
||||||
} else if (action == "tov_double") {
|
} else if (action == "tov_double") {
|
||||||
stats["dr"] = stats["dr"]! + 1; // SOMA AOS DRIBLES DUPLOS
|
stats["dr"] = stats["dr"]! + 1; // SOMA AOS DRIBLES DUPLOS
|
||||||
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
||||||
logText = "fez drible duplo 🏀";
|
logText = "fez drible duplo 🏀";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ANULAR FALTA ──────────────────────────────────────────────────────────
|
// ── ANULAR FALTA ──────────────────────────────────────────────────────────
|
||||||
else if (action == "sub_foul") {
|
else if (action == "sub_foul") {
|
||||||
if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1;
|
if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1;
|
||||||
@@ -664,6 +1007,10 @@ class PlacarController extends ChangeNotifier {
|
|||||||
|
|
||||||
_scheduleAutoSave();
|
_scheduleAutoSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_dispatchSyncAction('commit_stat', {
|
||||||
|
'action': action,
|
||||||
|
'player_data': playerData,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -672,7 +1019,8 @@ class PlacarController extends ChangeNotifier {
|
|||||||
_autoSaveTimer?.cancel();
|
_autoSaveTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
Future<void> saveGameStats(BuildContext context) async {
|
|
||||||
|
Future<void> saveGameStats(BuildContext context) async {
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
isSaving = true;
|
isSaving = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -707,7 +1055,7 @@ Future<void> saveGameStats(BuildContext context) async {
|
|||||||
|
|
||||||
double mvpScore =
|
double mvpScore =
|
||||||
((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) -
|
((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) -
|
||||||
((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35));
|
((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35));
|
||||||
mvpScore = mvpScore * (minJogados / 40.0);
|
mvpScore = mvpScore * (minJogados / 40.0);
|
||||||
|
|
||||||
String pName = playerNames[playerId] ?? '---';
|
String pName = playerNames[playerId] ?? '---';
|
||||||
@@ -731,20 +1079,23 @@ Future<void> saveGameStats(BuildContext context) async {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 1. Atualizar o Jogo
|
// 1. Atualizar o Jogo
|
||||||
await supabase.from('games').update({
|
await supabase
|
||||||
'my_score': myScore,
|
.from('games')
|
||||||
'opponent_score': opponentScore,
|
.update({
|
||||||
'remaining_seconds': durationNotifier.value.inSeconds,
|
'my_score': myScore,
|
||||||
'my_timeouts': myTimeoutsUsed,
|
'opponent_score': opponentScore,
|
||||||
'opp_timeouts': opponentTimeoutsUsed,
|
'remaining_seconds': durationNotifier.value.inSeconds,
|
||||||
'current_quarter': currentQuarter,
|
'my_timeouts': myTimeoutsUsed,
|
||||||
'status': newStatus,
|
'opp_timeouts': opponentTimeoutsUsed,
|
||||||
'top_pts_name': topPtsName,
|
'current_quarter': currentQuarter,
|
||||||
'top_ast_name': topAstName,
|
'status': newStatus,
|
||||||
'top_rbs_name': topRbsName,
|
'top_pts_name': topPtsName,
|
||||||
'mvp_name': mvpName,
|
'top_ast_name': topAstName,
|
||||||
'play_by_play': playByPlay,
|
'top_rbs_name': topRbsName,
|
||||||
}).eq('id', gameId);
|
'mvp_name': mvpName,
|
||||||
|
'play_by_play': playByPlay,
|
||||||
|
})
|
||||||
|
.eq('id', gameId);
|
||||||
|
|
||||||
// 2. Preparar as Estatísticas dos Jogadores
|
// 2. Preparar as Estatísticas dos Jogadores
|
||||||
List<Map<String, dynamic>> batchStats = [];
|
List<Map<String, dynamic>> batchStats = [];
|
||||||
@@ -784,7 +1135,7 @@ Future<void> saveGameStats(BuildContext context) async {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Preparar os Locais dos Lançamentos (MAPA DE CALOR) - O QUE FALTAVA
|
// 3. Preparar os Locais dos Lançamentos (MAPA DE CALOR)
|
||||||
List<Map<String, dynamic>> batchShots = [];
|
List<Map<String, dynamic>> batchShots = [];
|
||||||
for (var shot in matchShots) {
|
for (var shot in matchShots) {
|
||||||
if (!shot.playerId.startsWith("fake_")) {
|
if (!shot.playerId.startsWith("fake_")) {
|
||||||
@@ -818,16 +1169,22 @@ Future<void> saveGameStats(BuildContext context) async {
|
|||||||
await prefs.remove('backup_$gameId');
|
await prefs.remove('backup_$gameId');
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
content: Text('Guardado com Sucesso!'),
|
content: Text('Guardado com Sucesso!'),
|
||||||
backgroundColor: Colors.green));
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Erro ao gravar estatísticas: $e");
|
debugPrint("Erro ao gravar estatísticas: $e");
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
content: Text('Erro ao guardar: $e'),
|
content: Text('Erro ao guardar: $e'),
|
||||||
backgroundColor: Colors.red));
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isSaving = false;
|
isSaving = false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:playmaker/controllers/active_team.dart';
|
||||||
|
|
||||||
class TeamController {
|
class TeamController {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
@@ -65,10 +66,34 @@ class TeamController {
|
|||||||
// 4. FAVORITAR
|
// 4. FAVORITAR
|
||||||
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
||||||
try {
|
try {
|
||||||
await _supabase
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
.from('teams')
|
if (userId == null) return;
|
||||||
.update({'is_favorite': !currentStatus})
|
|
||||||
.eq('id', teamId);
|
// If we're marking this team as favorite, clear other favorites for this user
|
||||||
|
if (!currentStatus) {
|
||||||
|
await _supabase.from('teams').update({'is_favorite': false}).eq('user_id', userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the chosen team's favorite flag
|
||||||
|
await _supabase.from('teams').update({'is_favorite': !currentStatus}).eq('id', teamId);
|
||||||
|
|
||||||
|
// If it became favorite, load its data and set global active team
|
||||||
|
if (!currentStatus) {
|
||||||
|
final teamData = await _supabase.from('teams').select().eq('id', teamId).maybeSingle();
|
||||||
|
if (teamData != null) {
|
||||||
|
final newTeam = ActiveTeam(
|
||||||
|
id: teamData['id'].toString(),
|
||||||
|
name: teamData['name'] ?? 'Desconhecido',
|
||||||
|
logo: teamData['image_url'],
|
||||||
|
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
|
||||||
|
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
|
||||||
|
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update global active team so UI reflects the favorite immediately
|
||||||
|
await saveGlobalTeam(newTeam);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("❌ Erro ao favoritar: $e");
|
print("❌ Erro ao favoritar: $e");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:playmaker/controllers/placar_controller.dart';
|
import 'package:playmaker/controllers/placar_controller.dart';
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 4. PAINEL DE BOTÕES DE ACÇÃO (DRAG & DROP)
|
|
||||||
// ============================================================================
|
|
||||||
class ActionButtonsPanel extends StatelessWidget {
|
class ActionButtonsPanel extends StatelessWidget {
|
||||||
final PlacarController controller;
|
final PlacarController controller;
|
||||||
final double sf;
|
final double sf;
|
||||||
@@ -24,23 +21,29 @@ class ActionButtonsPanel extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
_columnBtn([
|
_columnBtn([
|
||||||
_dragAndTargetBtn("-1", AppTheme.actionMiss, "miss_1", baseSize, feedSize, sf, badge: ""),
|
// 🔴 1ª Linha: Agora "sub_pts_1" (Anular pontos)
|
||||||
|
_dragAndTargetBtn("-1", AppTheme.actionMiss, "sub_pts_1", baseSize, feedSize, sf, badge: ""),
|
||||||
_dragAndTargetBtn("1", AppTheme.actionPoints, "add_pts_1", baseSize, feedSize, sf, badge: "FTM"),
|
_dragAndTargetBtn("1", AppTheme.actionPoints, "add_pts_1", baseSize, feedSize, sf, badge: "FTM"),
|
||||||
_dragAndTargetBtn("1", AppTheme.actionPoints, "sub_pts_1", baseSize, feedSize, sf, badge: "FTA", isX: true),
|
// ❌ 3ª Linha: Agora "miss_1" (Falhados)
|
||||||
|
_dragAndTargetBtn("1", AppTheme.actionPoints, "miss_1", baseSize, feedSize, sf, badge: "FTA", isX: true),
|
||||||
_dragAndTargetBtn("STL", AppTheme.actionSteal, "add_stl", baseSize, feedSize, sf, badge: "STL"),
|
_dragAndTargetBtn("STL", AppTheme.actionSteal, "add_stl", baseSize, feedSize, sf, badge: "STL"),
|
||||||
], gap),
|
], gap),
|
||||||
SizedBox(width: gap),
|
SizedBox(width: gap),
|
||||||
_columnBtn([
|
_columnBtn([
|
||||||
_dragAndTargetBtn("-2", AppTheme.actionMiss, "miss_2", baseSize, feedSize, sf, badge: ""),
|
// 🔴 1ª Linha: Agora "sub_pts_2" (Anular pontos)
|
||||||
|
_dragAndTargetBtn("-2", AppTheme.actionMiss, "sub_pts_2", baseSize, feedSize, sf, badge: ""),
|
||||||
_dragAndTargetBtn("2", AppTheme.actionPoints, "add_pts_2", baseSize, feedSize, sf, badge: "2PM"),
|
_dragAndTargetBtn("2", AppTheme.actionPoints, "add_pts_2", baseSize, feedSize, sf, badge: "2PM"),
|
||||||
_dragAndTargetBtn("2", AppTheme.actionPoints, "sub_pts_2", baseSize, feedSize, sf, badge: "2PA", isX: true),
|
// ❌ 3ª Linha: Agora "miss_2" (Falhados)
|
||||||
|
_dragAndTargetBtn("2", AppTheme.actionPoints, "miss_2", baseSize, feedSize, sf, badge: "2PA", isX: true),
|
||||||
_dragAndTargetBtn("AST", AppTheme.actionAssist, "add_ast", baseSize, feedSize, sf, badge: "AST"),
|
_dragAndTargetBtn("AST", AppTheme.actionAssist, "add_ast", baseSize, feedSize, sf, badge: "AST"),
|
||||||
], gap),
|
], gap),
|
||||||
SizedBox(width: gap),
|
SizedBox(width: gap),
|
||||||
_columnBtn([
|
_columnBtn([
|
||||||
_dragAndTargetBtn("-3", AppTheme.actionMiss, "miss_3", baseSize, feedSize, sf, badge: ""),
|
// 🔴 1ª Linha: Agora "sub_pts_3" (Anular pontos)
|
||||||
|
_dragAndTargetBtn("-3", AppTheme.actionMiss, "sub_pts_3", baseSize, feedSize, sf, badge: ""),
|
||||||
_dragAndTargetBtn("3", AppTheme.actionPoints, "add_pts_3", baseSize, feedSize, sf, badge: "3PM"),
|
_dragAndTargetBtn("3", AppTheme.actionPoints, "add_pts_3", baseSize, feedSize, sf, badge: "3PM"),
|
||||||
_dragAndTargetBtn("3", AppTheme.actionPoints, "sub_pts_3", baseSize, feedSize, sf, badge: "3PA", isX: true),
|
// ❌ 3ª Linha: Agora "miss_3" (Falhados)
|
||||||
|
_dragAndTargetBtn("3", AppTheme.actionPoints, "miss_3", baseSize, feedSize, sf, badge: "3PA", isX: true),
|
||||||
_dragAndTargetBtn("TOV", AppTheme.actionMiss, "add_tov", baseSize, feedSize, sf, badge: "TOV"),
|
_dragAndTargetBtn("TOV", AppTheme.actionMiss, "add_tov", baseSize, feedSize, sf, badge: "TOV"),
|
||||||
], gap),
|
], gap),
|
||||||
SizedBox(width: gap),
|
SizedBox(width: gap),
|
||||||
@@ -53,7 +56,6 @@ class ActionButtonsPanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _columnBtn(List<Widget> children, double gap) {
|
Widget _columnBtn(List<Widget> children, double gap) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; // Para as orientações
|
|||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'pages/login.dart';
|
import 'pages/login.dart';
|
||||||
|
import 'utils/session_manager.dart';
|
||||||
|
|
||||||
// Variável global para controlar o Tema
|
// Variável global para controlar o Tema
|
||||||
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
|
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
|
||||||
@@ -25,9 +26,41 @@ void main() async {
|
|||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatefulWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
// Quando a app for para background/terminar, se houver sessão em progresso, desliga a sessão
|
||||||
|
if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
|
||||||
|
final inProgress = await SessionManager.isInProgress();
|
||||||
|
if (inProgress) {
|
||||||
|
try {
|
||||||
|
await Supabase.instance.client.auth.signOut();
|
||||||
|
await SessionManager.clear();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<ThemeMode>(
|
return ValueListenableBuilder<ThemeMode>(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
375
lib/pages/excel_export_service.dart
Normal file
375
lib/pages/excel_export_service.dart
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:excel/excel.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart' hide Border, BorderStyle;
|
||||||
|
|
||||||
|
class ExcelExportService {
|
||||||
|
static Future<void> generateAndPrintBoxScoreExcel({
|
||||||
|
required String gameId,
|
||||||
|
required String myTeam,
|
||||||
|
required String opponentTeam,
|
||||||
|
required String myScore,
|
||||||
|
required String opponentScore,
|
||||||
|
required String season,
|
||||||
|
required String targetTeam,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// ── 1. DADOS DO JOGO ───────────────────────────────────────────────────
|
||||||
|
final gameData = await supabase.from('games').select().eq('id', gameId).maybeSingle();
|
||||||
|
String dateStr = "---";
|
||||||
|
if (gameData != null && gameData['game_date'] != null) {
|
||||||
|
String rawDate = gameData['game_date'].toString();
|
||||||
|
dateStr = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. ESTATÍSTICAS DOS JOGADORES ──────────────────────────────────────
|
||||||
|
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
|
if (statsResponse.isEmpty) return;
|
||||||
|
|
||||||
|
// ── 3. NOMES E NÚMEROS DAS EQUIPAS E JOGADORES ───────────────────────
|
||||||
|
final membersResponse = await supabase.from('members').select('id, name, number');
|
||||||
|
final Map<String, Map<String, dynamic>> memberInfo = {
|
||||||
|
for (var m in membersResponse) m['id'].toString(): m
|
||||||
|
};
|
||||||
|
|
||||||
|
final teamsResponse = await supabase.from('teams').select('id, name');
|
||||||
|
final Map<String, String> teamNames = {
|
||||||
|
for (var t in teamsResponse) t['id'].toString(): t['name'].toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 4. CONFIGURAÇÃO DO EXCEL ───────────────────────────────────────────
|
||||||
|
var excel = Excel.createExcel();
|
||||||
|
String sheetName = 'Estatísticas';
|
||||||
|
Sheet sheet = excel[sheetName];
|
||||||
|
excel.setDefaultSheet(sheetName);
|
||||||
|
if (excel.tables.keys.contains('Sheet1')) excel.delete('Sheet1');
|
||||||
|
|
||||||
|
// ── ESTILOS E CORES PREMIUM ───────────────────────────────────────────
|
||||||
|
final corPrincipal = ExcelColor.fromHexString('#A00000'); // Vermelho escuro
|
||||||
|
final corFundoCinza = ExcelColor.fromHexString('#F5F5F5');
|
||||||
|
final corFundoCinzaEscuro = ExcelColor.fromHexString('#E0E0E0');
|
||||||
|
final cor2P = ExcelColor.fromHexString('#E3F2FD'); // Azul claro
|
||||||
|
final cor3P = ExcelColor.fromHexString('#E8F5E9'); // Verde claro
|
||||||
|
final corGlobal = ExcelColor.fromHexString('#FFF9C4');// Amarelo claro
|
||||||
|
final corLL = ExcelColor.fromHexString('#FFF3E0'); // Laranja claro
|
||||||
|
final corReb = ExcelColor.fromHexString('#F3E5F5'); // Roxo claro
|
||||||
|
final borderGrey = ExcelColor.fromHexString('#BDBDBD');
|
||||||
|
|
||||||
|
CellStyle styleTituloJogo = CellStyle(bold: true, fontSize: 16);
|
||||||
|
CellStyle styleNomeEquipa = CellStyle(bold: true, fontSize: 14, fontColorHex: ExcelColor.white, backgroundColorHex: corPrincipal, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
|
||||||
|
CellStyle styleTituloSecundario = CellStyle(bold: true, fontSize: 12, fontColorHex: ExcelColor.black, backgroundColorHex: corFundoCinzaEscuro, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
|
||||||
|
|
||||||
|
CellStyle styleGrelha(ExcelColor bgCol, {bool isBold = false}) {
|
||||||
|
return CellStyle(
|
||||||
|
bold: isBold, backgroundColorHex: bgCol,
|
||||||
|
horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center,
|
||||||
|
leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final styleGeral = styleGrelha(ExcelColor.white);
|
||||||
|
final styleGeralBold = styleGrelha(ExcelColor.white, isBold: true);
|
||||||
|
final styleNome = CellStyle(horizontalAlign: HorizontalAlign.Left, verticalAlign: VerticalAlign.Center, leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey));
|
||||||
|
|
||||||
|
// ── CABEÇALHO DO JOGO ────────────────────────────────────────────────
|
||||||
|
sheet.cell(CellIndex.indexByString("A1")).value = TextCellValue("JOGO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A1")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B1")).value = TextCellValue("$myTeam vs $opponentTeam");
|
||||||
|
sheet.cell(CellIndex.indexByString("B1")).cellStyle = styleTituloJogo;
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A2")).value = TextCellValue("COMPETIÇÃO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A2")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B2")).value = TextCellValue(season);
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A3")).value = TextCellValue("DATA:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A3")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B3")).value = TextCellValue(dateStr);
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A4")).value = TextCellValue("RESULTADO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A4")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B4")).value = TextCellValue("$myScore - $opponentScore");
|
||||||
|
sheet.cell(CellIndex.indexByString("B4")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
|
||||||
|
|
||||||
|
// ── TOTAIS POR PERÍODO (NOVA SECÇÃO) ─────────────────────────────────
|
||||||
|
sheet.cell(CellIndex.indexByString("A6")).value = TextCellValue("PONTUAÇÃO POR PERÍODO");
|
||||||
|
sheet.cell(CellIndex.indexByString("A6")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
|
||||||
|
|
||||||
|
List<String> periodHeaders = ["EQUIPA", "1º Q", "2º Q", "3º Q", "4º Q", "TOTAL"];
|
||||||
|
for (int i = 0; i < periodHeaders.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 6));
|
||||||
|
cell.value = TextCellValue(periodHeaders[i]);
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linha Minha Equipa
|
||||||
|
List<dynamic> myRow = [
|
||||||
|
myTeam,
|
||||||
|
gameData?['my_q1']?.toString() ?? '-',
|
||||||
|
gameData?['my_q2']?.toString() ?? '-',
|
||||||
|
gameData?['my_q3']?.toString() ?? '-',
|
||||||
|
gameData?['my_q4']?.toString() ?? '-',
|
||||||
|
myScore
|
||||||
|
];
|
||||||
|
for (int i = 0; i < myRow.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 7));
|
||||||
|
cell.value = TextCellValue(myRow[i].toString());
|
||||||
|
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linha Adversário
|
||||||
|
List<dynamic> oppRow = [
|
||||||
|
opponentTeam,
|
||||||
|
gameData?['opp_q1']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q2']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q3']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q4']?.toString() ?? '-',
|
||||||
|
opponentScore
|
||||||
|
];
|
||||||
|
for (int i = 0; i < oppRow.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 8));
|
||||||
|
cell.value = TextCellValue(oppRow[i].toString());
|
||||||
|
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = 11; // 👈 AS TABELAS PRINCIPAIS AGORA COMEÇAM MAIS ABAIXO (Linha 12 no Excel)
|
||||||
|
|
||||||
|
// Agrupar estatísticas por equipa
|
||||||
|
Map<String, List<dynamic>> statsByTeam = {};
|
||||||
|
for(var s in statsResponse) {
|
||||||
|
String tId = s['team_id'].toString();
|
||||||
|
statsByTeam.putIfAbsent(tId, () => []).add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CONSTRUÇÃO DAS TABELAS DE CADA EQUIPA ────────────────────────────
|
||||||
|
for (var entry in statsByTeam.entries) {
|
||||||
|
String tId = entry.key;
|
||||||
|
List<dynamic> tStats = entry.value;
|
||||||
|
String tName = teamNames[tId] ?? "Equipa $tId";
|
||||||
|
|
||||||
|
if (targetTeam != 'Ambas' && tName != targetTeam) continue;
|
||||||
|
|
||||||
|
tStats.sort((a, b) {
|
||||||
|
var mInfoA = memberInfo[a['member_id'].toString()];
|
||||||
|
var mInfoB = memberInfo[b['member_id'].toString()];
|
||||||
|
int numA = int.tryParse(mInfoA?['number']?.toString() ?? '0') ?? 0;
|
||||||
|
int numB = int.tryParse(mInfoB?['number']?.toString() ?? '0') ?? 0;
|
||||||
|
return numA.compareTo(numB);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> processedPlayers = [];
|
||||||
|
int tMin=0, tPts=0, t2m=0, t2a=0, t3m=0, t3a=0, tFgm=0, tFga=0, tftm=0, tfta=0;
|
||||||
|
int torb=0, tdrb=0, tStl=0, tAst=0, tTov=0, tBlk=0, tFls=0;
|
||||||
|
int tSo=0, tIl=0, tLi=0, tPa=0, tTresS=0, tDr=0;
|
||||||
|
|
||||||
|
for(var stat in tStats) {
|
||||||
|
var mInfo = memberInfo[stat['member_id'].toString()];
|
||||||
|
String pNum = mInfo != null ? (mInfo['number']?.toString() ?? "-") : "-";
|
||||||
|
String pName = mInfo != null ? (mInfo['name']?.toString() ?? "Desconhecido") : "Desconhecido";
|
||||||
|
|
||||||
|
int minSecs = stat['minutos_jogados'] ?? 0;
|
||||||
|
int pts = stat['pts'] ?? 0;
|
||||||
|
int p2m = stat['p2m'] ?? 0; int p2a = stat['p2a'] ?? 0;
|
||||||
|
int p3m = stat['p3m'] ?? 0; int p3a = stat['p3a'] ?? 0;
|
||||||
|
int fgm = stat['fgm'] ?? 0; int fga = stat['fga'] ?? 0;
|
||||||
|
int ftm = stat['ftm'] ?? 0; int fta = stat['fta'] ?? 0;
|
||||||
|
int orb = stat['orb'] ?? 0; int drb = stat['drb'] ?? 0; int tr = orb + drb;
|
||||||
|
int stl = stat['stl'] ?? 0; int ast = stat['ast'] ?? 0;
|
||||||
|
int tov = stat['tov'] ?? 0; int blk = stat['blk'] ?? 0; int fls = stat['fls'] ?? 0;
|
||||||
|
int so = stat['so'] ?? 0; int il = stat['il'] ?? 0; int li = stat['li'] ?? 0;
|
||||||
|
int pa = stat['pa'] ?? 0; int tresS = stat['tres_seg'] ?? 0; int dr = stat['dr'] ?? 0;
|
||||||
|
|
||||||
|
tMin+=minSecs; tPts+=pts; t2m+=p2m; t2a+=p2a; t3m+=p3m; t3a+=p3a;
|
||||||
|
tFgm+=fgm; tFga+=fga; tftm+=ftm; tfta+=fta; torb+=orb; tdrb+=drb;
|
||||||
|
tStl+=stl; tAst+=ast; tTov+=tov; tBlk+=blk; tFls+=fls;
|
||||||
|
tSo+=so; tIl+=il; tLi+=li; tPa+=pa; tTresS+=tresS; tDr+=dr;
|
||||||
|
|
||||||
|
processedPlayers.add({
|
||||||
|
'num': pNum, 'name': pName, 'minSecs': minSecs, 'pts': pts,
|
||||||
|
'p2m': p2m, 'p2a': p2a, 'p3m': p3m, 'p3a': p3a, 'fgm': fgm, 'fga': fga,
|
||||||
|
'ftm': ftm, 'fta': fta, 'orb': orb, 'drb': drb, 'tr': tr,
|
||||||
|
'stl': stl, 'ast': ast, 'tov': tov, 'blk': blk, 'fls': fls,
|
||||||
|
'so': so, 'il': il, 'li': li, 'pa': pa, '3s': tresS, 'dr': dr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TABELA 1: LANÇAMENTOS E RESSALTOS
|
||||||
|
var teamStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var teamEnd = CellIndex.indexByColumnRow(columnIndex: 18, rowIndex: r);
|
||||||
|
sheet.merge(teamStart, teamEnd, customValue: TextCellValue("ESTATÍSTICAS DA EQUIPA: ${tName.toUpperCase()} (Lançamentos e Ressaltos)"));
|
||||||
|
for(int i=0; i<=18; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
void criarCategoria(int colStart, int colEnd, String texto, CellStyle estilo) {
|
||||||
|
sheet.merge(CellIndex.indexByColumnRow(columnIndex: colStart, rowIndex: r), CellIndex.indexByColumnRow(columnIndex: colEnd, rowIndex: r), customValue: TextCellValue(texto));
|
||||||
|
for(int i=colStart; i<=colEnd; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = estilo;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleGrelha(corFundoCinza);
|
||||||
|
criarCategoria(4, 6, "2 PONTOS", styleGrelha(cor2P, isBold: true));
|
||||||
|
criarCategoria(7, 9, "3 PONTOS", styleGrelha(cor3P, isBold: true));
|
||||||
|
criarCategoria(10, 12, "GLOBAL", styleGrelha(corGlobal, isBold: true));
|
||||||
|
criarCategoria(13, 15, "L. LIVRES", styleGrelha(corLL, isBold: true));
|
||||||
|
criarCategoria(16, 18, "RESSALTOS", styleGrelha(corReb, isBold: true));
|
||||||
|
r++;
|
||||||
|
|
||||||
|
List<String> colsT1 = ["Nº", "NOME", "MIN", "PTS", "C", "T", "%", "C", "T", "%", "C", "T", "%", "C", "T", "%", "RO", "RD", "TR"];
|
||||||
|
for(int i = 0; i < colsT1.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = TextCellValue(colsT1[i]);
|
||||||
|
|
||||||
|
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
|
||||||
|
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
|
||||||
|
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
|
||||||
|
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
|
||||||
|
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
|
||||||
|
else cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
|
||||||
|
for(var p in processedPlayers) {
|
||||||
|
String minStr = '${p['minSecs'] ~/ 60}:${(p['minSecs'] % 60).toString().padLeft(2, '0')}';
|
||||||
|
String p2Pct = p['p2a'] > 0 ? '${((p['p2m'] / p['p2a']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String p3Pct = p['p3a'] > 0 ? '${((p['p3m'] / p['p3a']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String fgPct = p['fga'] > 0 ? '${((p['fgm'] / p['fga']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String ftPct = p['fta'] > 0 ? '${((p['ftm'] / p['fta']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
|
||||||
|
List<CellValue> rowData = [
|
||||||
|
TextCellValue(p['num']), TextCellValue(p['name']), TextCellValue(minStr), IntCellValue(p['pts']),
|
||||||
|
IntCellValue(p['p2m']), IntCellValue(p['p2a']), TextCellValue(p2Pct),
|
||||||
|
IntCellValue(p['p3m']), IntCellValue(p['p3a']), TextCellValue(p3Pct),
|
||||||
|
IntCellValue(p['fgm']), IntCellValue(p['fga']), TextCellValue(fgPct),
|
||||||
|
IntCellValue(p['ftm']), IntCellValue(p['fta']), TextCellValue(ftPct),
|
||||||
|
IntCellValue(p['orb']), IntCellValue(p['drb']), IntCellValue(p['tr'])
|
||||||
|
];
|
||||||
|
|
||||||
|
for(int i = 0; i < rowData.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = rowData[i];
|
||||||
|
if (i == 1) cell.cellStyle = styleNome;
|
||||||
|
else if (i == 3) cell.cellStyle = styleGeralBold;
|
||||||
|
else cell.cellStyle = styleGeral;
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String t2Pct = t2a > 0 ? '${((t2m / t2a) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String t3Pct = t3a > 0 ? '${((t3m / t3a) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tFgPct = tFga > 0 ? '${((tFgm / tFga) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tftPct = tfta > 0 ? '${((tftm / tfta) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tMinStr = '${tMin ~/ 60}:${(tMin % 60).toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
List<CellValue> totalRowT1 = [
|
||||||
|
TextCellValue(""), TextCellValue("TOTAL EQUIPA"), TextCellValue(tMinStr), IntCellValue(tPts),
|
||||||
|
IntCellValue(t2m), IntCellValue(t2a), TextCellValue(t2Pct),
|
||||||
|
IntCellValue(t3m), IntCellValue(t3a), TextCellValue(t3Pct),
|
||||||
|
IntCellValue(tFgm), IntCellValue(tFga), TextCellValue(tFgPct),
|
||||||
|
IntCellValue(tftm), IntCellValue(tfta), TextCellValue(tftPct),
|
||||||
|
IntCellValue(torb), IntCellValue(tdrb), IntCellValue(torb + tdrb)
|
||||||
|
];
|
||||||
|
|
||||||
|
for(int i = 0; i < totalRowT1.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = totalRowT1[i];
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
|
||||||
|
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
|
||||||
|
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
|
||||||
|
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
|
||||||
|
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
|
||||||
|
}
|
||||||
|
r += 3;
|
||||||
|
|
||||||
|
// TABELA 2: OUTRAS ESTATÍSTICAS
|
||||||
|
var secStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var secEnd = CellIndex.indexByColumnRow(columnIndex: 12, rowIndex: r);
|
||||||
|
sheet.merge(secStart, secEnd, customValue: TextCellValue("OUTRAS ESTATÍSTICAS: ${tName.toUpperCase()}"));
|
||||||
|
for(int i=0; i<=12; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleTituloSecundario;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
List<String> colsT2 = ["Nº", "NOME", "BR", "AS", "BP", "BLK", "FLS", "SO", "IL", "LI", "PA", "3S", "DR"];
|
||||||
|
for(int i = 0; i < colsT2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = TextCellValue(colsT2[i]);
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
|
||||||
|
for(var p in processedPlayers) {
|
||||||
|
List<CellValue> rowData2 = [
|
||||||
|
TextCellValue(p['num']), TextCellValue(p['name']),
|
||||||
|
IntCellValue(p['stl']), IntCellValue(p['ast']), IntCellValue(p['tov']),
|
||||||
|
IntCellValue(p['blk']), IntCellValue(p['fls']), IntCellValue(p['so']),
|
||||||
|
IntCellValue(p['il']), IntCellValue(p['li']), IntCellValue(p['pa']),
|
||||||
|
IntCellValue(p['3s']), IntCellValue(p['dr'])
|
||||||
|
];
|
||||||
|
for(int i = 0; i < rowData2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = rowData2[i];
|
||||||
|
cell.cellStyle = (i == 1) ? styleNome : styleGeral;
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CellValue> totalRowT2 = [
|
||||||
|
TextCellValue(""), TextCellValue("TOTAL EQUIPA"),
|
||||||
|
IntCellValue(tStl), IntCellValue(tAst), IntCellValue(tTov), IntCellValue(tBlk), IntCellValue(tFls),
|
||||||
|
IntCellValue(tSo), IntCellValue(tIl), IntCellValue(tLi), IntCellValue(tPa), IntCellValue(tTresS), IntCellValue(tDr)
|
||||||
|
];
|
||||||
|
for(int i = 0; i < totalRowT2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = totalRowT2[i];
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DESTAQUES DO JOGO ───────────────────────────────────────
|
||||||
|
if (gameData != null) {
|
||||||
|
var startD = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var endD = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
|
||||||
|
sheet.merge(startD, endD, customValue: TextCellValue("DESTAQUES DO JOGO"));
|
||||||
|
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
void adicionarDestaque(String titulo, String valor) {
|
||||||
|
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).value = TextCellValue(titulo);
|
||||||
|
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).cellStyle = CellStyle(bold: true);
|
||||||
|
var sV = CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: r);
|
||||||
|
var eV = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
|
||||||
|
sheet.merge(sV, eV, customValue: TextCellValue(valor));
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
adicionarDestaque("Melhor Marcador:", gameData['top_pts_name'] ?? '---');
|
||||||
|
adicionarDestaque("Melhor Ressaltador:", gameData['top_rbs_name'] ?? '---');
|
||||||
|
adicionarDestaque("Melhor Passador:", gameData['top_ast_name'] ?? '---');
|
||||||
|
adicionarDestaque("MVP da Partida:", gameData['mvp_name'] ?? '---');
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.setColumnWidth(0, 18.0);
|
||||||
|
sheet.setColumnWidth(1, 26.0);
|
||||||
|
sheet.setColumnWidth(2, 8.0);
|
||||||
|
sheet.setColumnWidth(3, 6.0);
|
||||||
|
for(int i=4; i<=18; i++) sheet.setColumnWidth(i, 5.5);
|
||||||
|
|
||||||
|
var fileBytes = excel.save();
|
||||||
|
if (fileBytes != null) {
|
||||||
|
final directory = await getTemporaryDirectory();
|
||||||
|
String safeName = targetTeam == 'Ambas' ? '${myTeam}_vs_${opponentTeam}'.replaceAll(' ', '_') : targetTeam.replaceAll(' ', '_');
|
||||||
|
final filePath = '${directory.path}/BoxScore_$safeName.xlsx';
|
||||||
|
|
||||||
|
File(filePath)..createSync(recursive: true)..writeAsBytesSync(fileBytes);
|
||||||
|
await Share.shareXFiles([XFile(filePath)], text: 'Estatísticas do Jogo: $myTeam vs $opponentTeam');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erro ao gerar Excel: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
|
import '../models/game_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/pages/PlacarPage.dart';
|
import 'package:playmaker/pages/PlacarPage.dart';
|
||||||
|
import 'package:playmaker/widgets/share_game_dialog.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../controllers/game_controller.dart';
|
import '../controllers/game_controller.dart';
|
||||||
import '../models/game_model.dart';
|
import '../controllers/game_sharing_controller.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
import 'pdf_export_service.dart';
|
import 'pdf_export_service.dart';
|
||||||
|
import 'excel_export_service.dart';
|
||||||
|
|
||||||
class GameResultCard extends StatelessWidget {
|
class GameResultCard extends StatelessWidget {
|
||||||
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
||||||
@@ -21,6 +24,67 @@ class GameResultCard extends StatelessWidget {
|
|||||||
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
|
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _showTeamSelectionDialog(BuildContext context, String format) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
|
||||||
|
title: Text('Gerar ${format.toUpperCase()}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
content: Text('De qual equipa pretende exportar as estatísticas?', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, myTeam);
|
||||||
|
},
|
||||||
|
child: Text(myTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, opponentTeam);
|
||||||
|
},
|
||||||
|
child: Text(opponentTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, 'Ambas');
|
||||||
|
},
|
||||||
|
child: Text('Ambas', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportDocument(BuildContext context, String format, String targetTeam) async {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('A gerar ${format.toUpperCase()}...'), duration: const Duration(seconds: 1)));
|
||||||
|
|
||||||
|
if (format == 'pdf') {
|
||||||
|
await PdfExportService.generateAndPrintBoxScore(
|
||||||
|
gameId: gameId,
|
||||||
|
myTeam: myTeam,
|
||||||
|
opponentTeam: opponentTeam,
|
||||||
|
myScore: myScore,
|
||||||
|
opponentScore: opponentScore,
|
||||||
|
season: season,
|
||||||
|
targetTeam: targetTeam,
|
||||||
|
);
|
||||||
|
} else if (format == 'excel') {
|
||||||
|
await ExcelExportService.generateAndPrintBoxScoreExcel(
|
||||||
|
gameId: gameId,
|
||||||
|
myTeam: myTeam,
|
||||||
|
opponentTeam: opponentTeam,
|
||||||
|
myScore: myScore,
|
||||||
|
opponentScore: opponentScore,
|
||||||
|
season: season,
|
||||||
|
targetTeam: targetTeam,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||||
@@ -46,32 +110,71 @@ class GameResultCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 👇 MENU DOS 3 PONTOS (MAIS NÍTIDO E MODERNO)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -10 * sf,
|
top: -12 * sf,
|
||||||
right: -10 * sf,
|
right: -12 * sf,
|
||||||
child: Row(
|
child: PopupMenuButton<String>(
|
||||||
children: [
|
icon: Icon(Icons.more_vert, color: Colors.grey.shade600, size: 26 * sf), // Ícone um pouco maior
|
||||||
IconButton(
|
splashRadius: 24 * sf,
|
||||||
icon: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed.withOpacity(0.8), size: 22 * sf),
|
elevation: 8, // Adiciona sombra para não se misturar com o fundo
|
||||||
splashRadius: 20 * sf,
|
shadowColor: Colors.black45,
|
||||||
tooltip: 'Gerar PDF',
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * sf)),
|
||||||
onPressed: () async {
|
color: Theme.of(context).colorScheme.surface,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A gerar PDF...'), duration: Duration(seconds: 1)));
|
surfaceTintColor: Theme.of(context).colorScheme.surface, // Previne que o material 3 mude a cor
|
||||||
await PdfExportService.generateAndPrintBoxScore(
|
onSelected: (value) {
|
||||||
gameId: gameId,
|
if (value == 'pdf' || value == 'excel') {
|
||||||
myTeam: myTeam,
|
_showTeamSelectionDialog(context, value);
|
||||||
opponentTeam: opponentTeam,
|
} else if (value == 'delete') {
|
||||||
myScore: myScore,
|
_showDeleteConfirmation(context);
|
||||||
opponentScore: opponentScore,
|
}
|
||||||
season: season,
|
},
|
||||||
);
|
itemBuilder: (context) => [
|
||||||
},
|
PopupMenuItem(
|
||||||
|
value: 'pdf',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Gerar PDF', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
PopupMenuItem(
|
||||||
icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf),
|
value: 'excel',
|
||||||
splashRadius: 20 * sf,
|
child: Row(
|
||||||
tooltip: 'Eliminar Jogo',
|
children: [
|
||||||
onPressed: () => _showDeleteConfirmation(context),
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.green.shade600.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.table_chart, color: Colors.green.shade600, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Gerar Excel', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(height: 1),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade500.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.delete_outline, color: Colors.grey.shade700, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Eliminar Jogo', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -179,6 +282,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
late TextEditingController _seasonController;
|
late TextEditingController _seasonController;
|
||||||
final TextEditingController _myTeamController = TextEditingController();
|
final TextEditingController _myTeamController = TextEditingController();
|
||||||
final TextEditingController _opponentController = TextEditingController();
|
final TextEditingController _opponentController = TextEditingController();
|
||||||
|
final GameSharingController _sharingController = GameSharingController();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -216,6 +320,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
|
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
|
||||||
|
TextButton(onPressed: _isLoading ? null : () async => await _joinRoom(), child: Text('ENTRAR NA SALA', style: TextStyle(fontSize: 14 * widget.sf))),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
|
||||||
onPressed: _isLoading ? null : () async {
|
onPressed: _isLoading ? null : () async {
|
||||||
@@ -235,6 +340,40 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _joinRoom() async {
|
||||||
|
print("🚪 Abrindo diálogo para entrar na sala");
|
||||||
|
final result = await showDialog<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => JoinGameDialog(controller: _sharingController),
|
||||||
|
);
|
||||||
|
|
||||||
|
print("📦 Resultado do diálogo: $result");
|
||||||
|
if (result == null) {
|
||||||
|
print("❌ Resultado nulo, cancelado");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final gameData = result['game'] as Map<String, dynamic>?;
|
||||||
|
print("🎮 Game data: $gameData");
|
||||||
|
if (gameData == null) {
|
||||||
|
print("❌ Game data nula");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String gameId = gameData['id']?.toString() ?? '';
|
||||||
|
final String myTeam = gameData['my_team']?.toString() ?? _myTeamController.text;
|
||||||
|
final String opponentTeam = gameData['opponent_team']?.toString() ?? _opponentController.text;
|
||||||
|
|
||||||
|
print("🆔 Game ID: $gameId, My Team: $myTeam, Opponent: $opponentTeam");
|
||||||
|
|
||||||
|
if (gameId.isNotEmpty && context.mounted) {
|
||||||
|
print("➡️ Navegando para PlacarPage");
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: gameId, myTeam: myTeam, opponentTeam: opponentTeam)));
|
||||||
|
} else {
|
||||||
|
print("❌ Game ID vazio ou contexto não montado");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSearch(BuildContext context, String label, TextEditingController controller) {
|
Widget _buildSearch(BuildContext context, String label, TextEditingController controller) {
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: widget.teamController.teamsStream,
|
stream: widget.teamController.teamsStream,
|
||||||
|
|||||||
1126
lib/pages/home.dart
1126
lib/pages/home.dart
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import 'package:pdf/widgets.dart' as pw;
|
|||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
// Modelo local para os tiros
|
|
||||||
class _ShotDot {
|
class _ShotDot {
|
||||||
final double relX;
|
final double relX;
|
||||||
final double relY;
|
final double relY;
|
||||||
@@ -13,10 +12,6 @@ class _ShotDot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PdfExportService {
|
class PdfExportService {
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
// ENTRY POINT
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
static Future<void> generateAndPrintBoxScore({
|
static Future<void> generateAndPrintBoxScore({
|
||||||
required String gameId,
|
required String gameId,
|
||||||
required String myTeam,
|
required String myTeam,
|
||||||
@@ -24,18 +19,15 @@ class PdfExportService {
|
|||||||
required String myScore,
|
required String myScore,
|
||||||
required String opponentScore,
|
required String opponentScore,
|
||||||
required String season,
|
required String season,
|
||||||
|
required String targetTeam,
|
||||||
}) async {
|
}) async {
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// ── Jogo ────────────────────────────────────────────────────────────────
|
// ── Jogo ────────────────────────────────────────────────────────────────
|
||||||
final gameData =
|
final gameData = await supabase.from('games').select().eq('id', gameId).single();
|
||||||
await supabase.from('games').select().eq('id', gameId).single();
|
|
||||||
|
|
||||||
// ── Equipas ─────────────────────────────────────────────────────────────
|
// ── Equipas ─────────────────────────────────────────────────────────────
|
||||||
final teamsData = await supabase
|
final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
||||||
.from('teams')
|
|
||||||
.select('id, name')
|
|
||||||
.inFilter('name', [myTeam, opponentTeam]);
|
|
||||||
|
|
||||||
String? myTeamId;
|
String? myTeamId;
|
||||||
for (var t in teamsData) {
|
for (var t in teamsData) {
|
||||||
@@ -44,32 +36,19 @@ class PdfExportService {
|
|||||||
|
|
||||||
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
||||||
List<dynamic> myPlayers = myTeamId != null
|
List<dynamic> myPlayers = myTeamId != null
|
||||||
? await supabase
|
? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador')
|
||||||
.from('members')
|
|
||||||
.select()
|
|
||||||
.eq('team_id', myTeamId)
|
|
||||||
.eq('type', 'Jogador')
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// ── Estatísticas ─────────────────────────────────────────────────────────
|
// ── Estatísticas ─────────────────────────────────────────────────────────
|
||||||
final statsData =
|
final statsData = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
await supabase.from('player_stats').select().eq('game_id', gameId);
|
|
||||||
Map<String, Map<String, dynamic>> statsMap = {};
|
Map<String, Map<String, dynamic>> statsMap = {};
|
||||||
for (var s in statsData) {
|
for (var s in statsData) {
|
||||||
statsMap[s['member_id'].toString()] = s;
|
statsMap[s['member_id'].toString()] = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tiros (para o mapa de calor da minha equipa) ──────────────────────
|
// ── Tiros ──────────────────────
|
||||||
final shotsData = await supabase
|
final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId);
|
||||||
.from('shot_locations')
|
final Set<String> myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet();
|
||||||
.select()
|
|
||||||
.eq('game_id', gameId);
|
|
||||||
|
|
||||||
// IDs da minha equipa
|
|
||||||
final Set<String> myPlayerIds =
|
|
||||||
myPlayers.map((p) => p['id'].toString()).toSet();
|
|
||||||
|
|
||||||
// Separa os tiros: todos da minha equipa, depois por jogador
|
|
||||||
final List<_ShotDot> myTeamShots = [];
|
final List<_ShotDot> myTeamShots = [];
|
||||||
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
||||||
|
|
||||||
@@ -86,16 +65,14 @@ class PdfExportService {
|
|||||||
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tabela de estatísticas (Apenas a minha equipa) ────────────────────
|
// ── Tabela de estatísticas ────────────────────
|
||||||
List<List<String>> myTeamTable =
|
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
|
||||||
_buildTeamTableData(myPlayers, statsMap);
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
// CONSTRUÇÃO DO PDF
|
// CONSTRUÇÃO DO PDF
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
final pdf = pw.Document();
|
final pdf = pw.Document();
|
||||||
|
|
||||||
// ── PÁGINA 1: Box Score ──────────────────────────────────────────────
|
|
||||||
pdf.addPage(
|
pdf.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
pageFormat: PdfPageFormat.a4.landscape,
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
@@ -110,71 +87,81 @@ class PdfExportService {
|
|||||||
children: [
|
children: [
|
||||||
pw.Row(
|
pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
pw.Text('Relatório Estatístico',
|
pw.Column(
|
||||||
style: pw.TextStyle(
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
fontSize: 22,
|
children: [
|
||||||
fontWeight: pw.FontWeight.bold)),
|
pw.Text('Relatório Estatístico', style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
pw.Column(
|
pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
pw.Text('$myTeam vs $opponentTeam',
|
pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)),
|
||||||
style: pw.TextStyle(
|
pw.Text('Resultado: $myScore — $opponentScore', style: const pw.TextStyle(fontSize: 13)),
|
||||||
fontSize: 15,
|
pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)),
|
||||||
fontWeight: pw.FontWeight.bold)),
|
pw.SizedBox(height: 10),
|
||||||
pw.Text('Resultado: $myScore — $opponentScore',
|
|
||||||
style: const pw.TextStyle(fontSize: 13)),
|
// 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇
|
||||||
pw.Text('Época: $season',
|
pw.Table.fromTextArray(
|
||||||
style: const pw.TextStyle(fontSize: 11)),
|
context: context,
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||||
|
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8),
|
||||||
|
cellStyle: const pw.TextStyle(fontSize: 8),
|
||||||
|
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||||
|
cellAlignment: pw.Alignment.center,
|
||||||
|
data: <List<String>>[
|
||||||
|
['Equipa', '1ºQ', '2ºQ', '3ºQ', '4ºQ', 'F'],
|
||||||
|
[
|
||||||
|
myTeam,
|
||||||
|
gameData['my_q1']?.toString() ?? '-',
|
||||||
|
gameData['my_q2']?.toString() ?? '-',
|
||||||
|
gameData['my_q3']?.toString() ?? '-',
|
||||||
|
gameData['my_q4']?.toString() ?? '-',
|
||||||
|
myScore
|
||||||
|
],
|
||||||
|
[
|
||||||
|
opponentTeam,
|
||||||
|
gameData['opp_q1']?.toString() ?? '-',
|
||||||
|
gameData['opp_q2']?.toString() ?? '-',
|
||||||
|
gameData['opp_q3']?.toString() ?? '-',
|
||||||
|
gameData['opp_q4']?.toString() ?? '-',
|
||||||
|
opponentScore
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 12),
|
|
||||||
|
|
||||||
pw.Text('Equipa: $myTeam',
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: const PdfColor.fromInt(0xFFA00000))),
|
|
||||||
pw.SizedBox(height: 8),
|
pw.SizedBox(height: 8),
|
||||||
|
|
||||||
pw.Text('Pontos e Lançamentos',
|
pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.grey700)),
|
|
||||||
pw.SizedBox(height: 2),
|
pw.SizedBox(height: 2),
|
||||||
_buildPdfTablePart1(
|
_buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||||
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
|
||||||
|
|
||||||
pw.SizedBox(height: 14),
|
pw.SizedBox(height: 14),
|
||||||
|
|
||||||
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)',
|
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.grey700)),
|
|
||||||
pw.SizedBox(height: 2),
|
pw.SizedBox(height: 2),
|
||||||
_buildPdfTablePart2(
|
_buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||||
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
|
||||||
|
|
||||||
pw.SizedBox(height: 16),
|
pw.SizedBox(height: 16),
|
||||||
|
|
||||||
pw.Row(
|
pw.Row(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSummaryBox('Melhor Marcador',
|
_buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'),
|
||||||
gameData['top_pts_name'] ?? '---'),
|
|
||||||
pw.SizedBox(width: 10),
|
pw.SizedBox(width: 10),
|
||||||
_buildSummaryBox('Melhor Ressaltador',
|
_buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'),
|
||||||
gameData['top_rbs_name'] ?? '---'),
|
|
||||||
pw.SizedBox(width: 10),
|
pw.SizedBox(width: 10),
|
||||||
_buildSummaryBox('Melhor Passador',
|
_buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'),
|
||||||
gameData['top_ast_name'] ?? '---'),
|
|
||||||
pw.SizedBox(width: 10),
|
pw.SizedBox(width: 10),
|
||||||
_buildSummaryBox(
|
_buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'),
|
||||||
'MVP', gameData['mvp_name'] ?? '---'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -195,15 +182,13 @@ class PdfExportService {
|
|||||||
return pw.Column(
|
return pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)',
|
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)),
|
||||||
const PdfColor.fromInt(0xFFA00000)),
|
|
||||||
pw.SizedBox(height: 12),
|
pw.SizedBox(height: 12),
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
child: pw.Center(
|
child: pw.Center(
|
||||||
child: pw.CustomPaint(
|
child: pw.CustomPaint(
|
||||||
size: const PdfPoint(360, 360),
|
size: const PdfPoint(360, 360),
|
||||||
painter: (canvas, size) =>
|
painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots),
|
||||||
_paintCourt(canvas, size, myTeamShots),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -217,7 +202,6 @@ class PdfExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
|
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
|
||||||
// 👇 FILTRO ATIVO: Só entra aqui quem tiver tiros na lista "shotsByPlayer"!
|
|
||||||
final activePlayers = myPlayers.where((p) {
|
final activePlayers = myPlayers.where((p) {
|
||||||
final pid = p['id'].toString();
|
final pid = p['id'].toString();
|
||||||
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
|
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
|
||||||
@@ -280,9 +264,6 @@ class PdfExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
// WIDGET DO MAPA DE CALOR INDIVIDUAL
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
|
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
|
||||||
final String playerName = player['name']?.toString() ?? 'Jogador';
|
final String playerName = player['name']?.toString() ?? 'Jogador';
|
||||||
final String playerNumber = player['number']?.toString() ?? '0';
|
final String playerNumber = player['number']?.toString() ?? '0';
|
||||||
@@ -320,16 +301,11 @@ class PdfExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
// CORREÇÃO: DESENHO DO CAMPO E LINHAS ADAPTADAS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
|
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
|
||||||
final double w = size.x;
|
final double w = size.x;
|
||||||
final double h = size.y;
|
final double h = size.y;
|
||||||
final double basketX = w / 2;
|
final double basketX = w / 2;
|
||||||
|
|
||||||
// Fundo Amarelo (Toda a área)
|
|
||||||
canvas
|
canvas
|
||||||
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
|
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
|
||||||
..drawRect(0, 0, w, h)
|
..drawRect(0, 0, w, h)
|
||||||
@@ -341,7 +317,6 @@ class PdfExportService {
|
|||||||
final double alturaDoArco = larguraDoArco * 0.30;
|
final double alturaDoArco = larguraDoArco * 0.30;
|
||||||
final double totalArcoHeight = alturaDoArco * 4;
|
final double totalArcoHeight = alturaDoArco * 4;
|
||||||
|
|
||||||
// ── 1. LINHAS BRANCAS ───────────────────────────────────────────────
|
|
||||||
canvas.setStrokeColor(PdfColors.white);
|
canvas.setStrokeColor(PdfColors.white);
|
||||||
canvas.setLineWidth(2.0);
|
canvas.setLineWidth(2.0);
|
||||||
|
|
||||||
@@ -350,7 +325,6 @@ class PdfExportService {
|
|||||||
_drawLine(canvas, h, 0, length, margin, length);
|
_drawLine(canvas, h, 0, length, margin, length);
|
||||||
_drawLine(canvas, h, w - margin, length, w, length);
|
_drawLine(canvas, h, w - margin, length, w, length);
|
||||||
|
|
||||||
// Arco 3pts
|
|
||||||
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
|
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
|
||||||
|
|
||||||
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
|
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
|
||||||
@@ -361,46 +335,34 @@ class PdfExportService {
|
|||||||
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
|
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
|
||||||
_drawLine(canvas, h, sXR, sYR, w, h * 0.85);
|
_drawLine(canvas, h, sXR, sYR, w, h * 0.85);
|
||||||
|
|
||||||
// ── 2. LINHAS PRETAS ─────────────────────────────────────────────────
|
|
||||||
canvas.setStrokeColor(PdfColors.black);
|
canvas.setStrokeColor(PdfColors.black);
|
||||||
canvas.setLineWidth(1.5);
|
canvas.setLineWidth(1.5);
|
||||||
|
|
||||||
final double pW = w * 0.28;
|
final double pW = w * 0.28;
|
||||||
final double pH = h * 0.38;
|
final double pH = h * 0.38;
|
||||||
|
|
||||||
// Garrafão
|
|
||||||
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
|
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
|
||||||
|
|
||||||
// Círculo Lances Livres
|
|
||||||
final double ftR = pW / 2;
|
final double ftR = pW / 2;
|
||||||
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
|
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
|
||||||
// Tracejado
|
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
|
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linhas oblíquas do garrafão
|
|
||||||
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
|
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
|
||||||
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
|
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
|
||||||
|
|
||||||
// Meio Campo
|
|
||||||
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
|
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
|
||||||
|
|
||||||
// Cesto e Tabela
|
|
||||||
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
|
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
|
||||||
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
|
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
|
||||||
|
|
||||||
// ── 3. TIROS ─────────────────────────────────────────────────────────
|
|
||||||
for (final shot in shots) {
|
for (final shot in shots) {
|
||||||
final double px = shot.relX * w;
|
final double px = shot.relX * w;
|
||||||
final double py = shot.relY * h;
|
final double py = shot.relY * h;
|
||||||
|
|
||||||
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
|
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
|
||||||
|
|
||||||
// Desenha Círculo Colorido
|
|
||||||
_fillCircle(canvas, h, px, py, 6, dotColor);
|
_fillCircle(canvas, h, px, py, 6, dotColor);
|
||||||
|
|
||||||
// Símbolos
|
|
||||||
canvas.setStrokeColor(PdfColors.white);
|
canvas.setStrokeColor(PdfColors.white);
|
||||||
canvas.setLineWidth(1.2);
|
canvas.setLineWidth(1.2);
|
||||||
if (shot.isMake) {
|
if (shot.isMake) {
|
||||||
@@ -413,19 +375,12 @@ class PdfExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers com inversão automática do Eixo Y para casar com Flutter ──
|
|
||||||
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
|
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
|
||||||
c.moveTo(x1, canvasH - y1);
|
c.moveTo(x1, canvasH - y1);
|
||||||
c.lineTo(x2, canvasH - y2);
|
c.lineTo(x2, canvasH - y2);
|
||||||
c.strokePath();
|
c.strokePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _lineRaw(PdfGraphics c, double x1, double y1, double x2, double y2) {
|
|
||||||
c.moveTo(x1, y1);
|
|
||||||
c.lineTo(x2, y2);
|
|
||||||
c.strokePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
|
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
|
||||||
c.drawRect(x, canvasH - (y + height), width, height);
|
c.drawRect(x, canvasH - (y + height), width, height);
|
||||||
c.strokePath();
|
c.strokePath();
|
||||||
@@ -460,12 +415,7 @@ class PdfExportService {
|
|||||||
c.strokePath();
|
c.strokePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
static List<List<String>> _buildTeamTableData(List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
||||||
// TABELAS DE ESTATÍSTICAS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
static List<List<String>> _buildTeamTableData(
|
|
||||||
List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
|
||||||
List<List<String>> tableData = [];
|
List<List<String>> tableData = [];
|
||||||
|
|
||||||
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
|
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
|
||||||
@@ -485,27 +435,16 @@ class PdfExportService {
|
|||||||
var s = statsMap[id] ?? {};
|
var s = statsMap[id] ?? {};
|
||||||
|
|
||||||
int pts = s['pts'] ?? 0;
|
int pts = s['pts'] ?? 0;
|
||||||
int fgm = s['fgm'] ?? 0;
|
int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0;
|
||||||
int fga = s['fga'] ?? 0;
|
int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 0;
|
||||||
int ftm = s['ftm'] ?? 0;
|
int p2m = s['p2m'] ?? 0; int p2a = s['p2a'] ?? 0;
|
||||||
int fta = s['fta'] ?? 0;
|
int p3m = s['p3m'] ?? 0; int p3a = s['p3a'] ?? 0;
|
||||||
int p2m = s['p2m'] ?? 0;
|
|
||||||
int p2a = s['p2a'] ?? 0;
|
|
||||||
int p3m = s['p3m'] ?? 0;
|
|
||||||
int p3a = s['p3a'] ?? 0;
|
|
||||||
int fls = s['fls'] ?? 0;
|
int fls = s['fls'] ?? 0;
|
||||||
int orb = s['orb'] ?? 0;
|
int orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0;
|
||||||
int drb = s['drb'] ?? 0;
|
int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0;
|
||||||
int stl = s['stl'] ?? 0;
|
int tov = s['tov'] ?? 0; int blk = s['blk'] ?? 0;
|
||||||
int ast = s['ast'] ?? 0;
|
int so = s['so'] ?? 0; int il = s['il'] ?? 0; int li = s['li'] ?? 0;
|
||||||
int tov = s['tov'] ?? 0;
|
int pa = s['pa'] ?? 0; int tresS = s['tres_seg'] ?? 0; int dr = s['dr'] ?? 0;
|
||||||
int blk = s['blk'] ?? 0;
|
|
||||||
int so = s['so'] ?? 0;
|
|
||||||
int il = s['il'] ?? 0;
|
|
||||||
int li = s['li'] ?? 0;
|
|
||||||
int pa = s['pa'] ?? 0;
|
|
||||||
int tresS = s['tres_seg'] ?? 0;
|
|
||||||
int dr = s['dr'] ?? 0;
|
|
||||||
int sec = s['minutos_jogados'] ?? 0;
|
int sec = s['minutos_jogados'] ?? 0;
|
||||||
|
|
||||||
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
||||||
@@ -525,8 +464,7 @@ class PdfExportService {
|
|||||||
tableData.add([
|
tableData.add([
|
||||||
p['number']?.toString() ?? '-',
|
p['number']?.toString() ?? '-',
|
||||||
p['name']?.toString() ?? '?',
|
p['name']?.toString() ?? '?',
|
||||||
minStr,
|
minStr, pts.toString(),
|
||||||
pts.toString(),
|
|
||||||
p2m.toString(), p2a.toString(), p2Pct,
|
p2m.toString(), p2a.toString(), p2Pct,
|
||||||
p3m.toString(), p3a.toString(), p3Pct,
|
p3m.toString(), p3a.toString(), p3Pct,
|
||||||
fgm.toString(), fga.toString(), fgPct,
|
fgm.toString(), fga.toString(), fgPct,
|
||||||
@@ -717,8 +655,7 @@ class PdfExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pw.Widget _groupHeader(
|
static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
||||||
String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
|
||||||
return pw.Column(
|
return pw.Column(
|
||||||
children: [
|
children: [
|
||||||
pw.Container(
|
pw.Container(
|
||||||
@@ -726,54 +663,28 @@ class PdfExportService {
|
|||||||
alignment: pw.Alignment.center,
|
alignment: pw.Alignment.center,
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||||
decoration: const pw.BoxDecoration(
|
decoration: const pw.BoxDecoration(
|
||||||
border: pw.Border(
|
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
||||||
bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
|
||||||
),
|
),
|
||||||
child: pw.Text(title, style: hStyle),
|
child: pw.Text(title, style: hStyle),
|
||||||
),
|
),
|
||||||
pw.Row(children: [
|
pw.Row(children: [
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('C', style: sStyle))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: pw.Text('C', style: sStyle))),
|
|
||||||
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('T', style: sStyle))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: pw.Text('T', style: sStyle))),
|
|
||||||
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: pw.Text('%', style: sStyle))),
|
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pw.Widget _groupData(
|
static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) {
|
||||||
String c, String t, String pct, pw.TextStyle style) {
|
|
||||||
return pw.Row(children: [
|
return pw.Row(children: [
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(c, style: style))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: pw.Text(c, style: style))),
|
|
||||||
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(t, style: style))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: pw.Text(t, style: style))),
|
|
||||||
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(pct, style: style))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: pw.Text(pct, style: style))),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,17 +692,8 @@ class PdfExportService {
|
|||||||
return pw.Container(
|
return pw.Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: pw.BoxDecoration(
|
decoration: pw.BoxDecoration(color: color, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6))),
|
||||||
color: color,
|
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||||
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
|
|
||||||
),
|
|
||||||
child: pw.Text(
|
|
||||||
title,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
color: PdfColors.white,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: pw.FontWeight.bold),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,15 +701,11 @@ class PdfExportService {
|
|||||||
return pw.Row(
|
return pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
pw.Container(width: 12, height: 12,
|
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
||||||
decoration: const pw.BoxDecoration(
|
|
||||||
color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
|
||||||
pw.SizedBox(width: 4),
|
pw.SizedBox(width: 4),
|
||||||
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
|
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
|
||||||
pw.SizedBox(width: 20),
|
pw.SizedBox(width: 20),
|
||||||
pw.Container(width: 12, height: 12,
|
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
||||||
decoration: const pw.BoxDecoration(
|
|
||||||
color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
|
||||||
pw.SizedBox(width: 4),
|
pw.SizedBox(width: 4),
|
||||||
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
||||||
],
|
],
|
||||||
@@ -817,28 +715,15 @@ class PdfExportService {
|
|||||||
static pw.Widget _buildSummaryBox(String title, String value) {
|
static pw.Widget _buildSummaryBox(String title, String value) {
|
||||||
return pw.Container(
|
return pw.Container(
|
||||||
width: 120,
|
width: 120,
|
||||||
decoration: pw.BoxDecoration(
|
decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)),
|
||||||
border: pw.TableBorder.all(color: PdfColors.black, width: 1),
|
|
||||||
),
|
|
||||||
child: pw.Column(children: [
|
child: pw.Column(children: [
|
||||||
pw.Container(
|
pw.Container(
|
||||||
width: double.infinity,
|
width: double.infinity, padding: const pw.EdgeInsets.all(6), color: const PdfColor.fromInt(0xFFA00000),
|
||||||
padding: const pw.EdgeInsets.all(6),
|
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 9, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||||
color: const PdfColor.fromInt(0xFFA00000),
|
|
||||||
child: pw.Text(title,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
color: PdfColors.white,
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: pw.FontWeight.bold),
|
|
||||||
textAlign: pw.TextAlign.center),
|
|
||||||
),
|
),
|
||||||
pw.Container(
|
pw.Container(
|
||||||
width: double.infinity,
|
width: double.infinity, padding: const pw.EdgeInsets.all(8),
|
||||||
padding: const pw.EdgeInsets.all(8),
|
child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||||
child: pw.Text(value,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10, fontWeight: pw.FontWeight.bold),
|
|
||||||
textAlign: pw.TextAlign.center),
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart'; // 👇 IMPORTAÇÃO PARA CACHE
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart'; // 👇 IMPORTAÇÃO PARA MEMÓRIA RÁPIDA
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
import 'login.dart';
|
import 'login.dart';
|
||||||
@@ -23,7 +23,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
File? _localImageFile;
|
File? _localImageFile;
|
||||||
String? _uploadedImageUrl;
|
String? _uploadedImageUrl;
|
||||||
bool _isUploadingImage = false;
|
bool _isUploadingImage = false;
|
||||||
bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR
|
bool _isMemoryLoaded = false;
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
@@ -33,16 +33,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD
|
String _prefsKey(String key) {
|
||||||
|
final userId = supabase.auth.currentUser?.id ?? 'guest';
|
||||||
|
return '${key}_$userId';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadUserAvatar() async {
|
Future<void> _loadUserAvatar() async {
|
||||||
// 1. Lê da memória rápida primeiro!
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final savedUrl = prefs.getString('meu_avatar_guardado');
|
final savedUrl = prefs.getString(_prefsKey('meu_avatar_guardado'));
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (savedUrl != null) _uploadedImageUrl = savedUrl;
|
if (savedUrl != null) {
|
||||||
_isMemoryLoaded = true; // Avisa que já leu a memória
|
_uploadedImageUrl = savedUrl;
|
||||||
|
} else {
|
||||||
|
_uploadedImageUrl = null;
|
||||||
|
}
|
||||||
|
_isMemoryLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +66,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (mounted && data != null && data['avatar_url'] != null) {
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
final urlDoSupabase = data['avatar_url'];
|
final urlDoSupabase = data['avatar_url'];
|
||||||
|
|
||||||
// Atualiza a memória se a foto na base de dados for diferente
|
|
||||||
if (urlDoSupabase != savedUrl) {
|
if (urlDoSupabase != savedUrl) {
|
||||||
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
await prefs.setString(_prefsKey('meu_avatar_guardado'), urlDoSupabase);
|
||||||
setState(() {
|
setState(() {
|
||||||
_uploadedImageUrl = urlDoSupabase;
|
_uploadedImageUrl = urlDoSupabase;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erro ao carregar avatar: $e");
|
debugPrint("Erro ao carregar avatar: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +101,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||||
);
|
);
|
||||||
|
|
||||||
final String publicUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
// 👇 TRUQUE MÁGICO PARA O AVATAR ATUALIZAR: Adicionar o timestamp ao URL!
|
||||||
|
final String baseUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
||||||
|
final String publicUrl = '$baseUrl?v=${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
await supabase
|
await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
@@ -104,9 +112,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'avatar_url': publicUrl
|
'avatar_url': publicUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
// 👇 MÁGICA: GUARDA LOGO O NOVO URL NA MEMÓRIA PARA A HOME SABER!
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('meu_avatar_guardado', publicUrl);
|
await prefs.setString(_prefsKey('meu_avatar_guardado'), publicUrl);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -280,7 +287,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 AVATAR OTIMIZADO: SEM LAG, COM CACHE E MEMÓRIA
|
|
||||||
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -298,29 +304,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: _isUploadingImage && _localImageFile != null
|
child: _isUploadingImage && _localImageFile != null
|
||||||
// 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload
|
|
||||||
? Image.file(_localImageFile!, fit: BoxFit.cover)
|
? Image.file(_localImageFile!, fit: BoxFit.cover)
|
||||||
|
|
||||||
// 2. Antes da memória carregar, fica só o fundo (evita piscar)
|
|
||||||
: !_isMemoryLoaded
|
: !_isMemoryLoaded
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
|
|
||||||
// 3. Depois da memória carregar, se houver URL, desenha com Cache!
|
|
||||||
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: _uploadedImageUrl!,
|
imageUrl: _uploadedImageUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
fadeInDuration: Duration.zero, // Fica instantâneo!
|
fadeInDuration: Duration.zero,
|
||||||
placeholder: (context, url) => const SizedBox(),
|
placeholder: (context, url) => const SizedBox(),
|
||||||
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Se não houver URL, mete o boneco
|
|
||||||
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ÍCONE DE LÁPIS
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -335,7 +333,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// LOADING OVERLAY (Enquanto faz o upload)
|
|
||||||
if (_isUploadingImage)
|
if (_isUploadingImage)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -364,9 +361,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// Limpa a memória do Avatar ao sair para não aparecer na conta de outra pessoa!
|
// 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR!
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove('meu_avatar_guardado');
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null) {
|
||||||
|
await prefs.remove(_prefsKey('meu_avatar_guardado'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_id'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_name'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_logo'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_wins'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_losses'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_draws'));
|
||||||
|
}
|
||||||
|
|
||||||
await Supabase.instance.client.auth.signOut();
|
await Supabase.instance.client.auth.signOut();
|
||||||
if (ctx.mounted) {
|
if (ctx.mounted) {
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
|
import '../controllers/active_team.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class StatusPage extends StatefulWidget {
|
class StatusPage extends StatefulWidget {
|
||||||
const StatusPage({super.key});
|
final String? initialTeamId;
|
||||||
|
final String initialTeamName;
|
||||||
|
final String? initialTeamLogo;
|
||||||
|
|
||||||
|
const StatusPage({
|
||||||
|
super.key,
|
||||||
|
this.initialTeamId,
|
||||||
|
this.initialTeamName = "Selecionar Equipa",
|
||||||
|
this.initialTeamLogo,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatusPage> createState() => _StatusPageState();
|
State<StatusPage> createState() => _StatusPageState();
|
||||||
@@ -16,11 +27,110 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
final TeamController _teamController = TeamController();
|
final TeamController _teamController = TeamController();
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
String? _selectedTeamId;
|
late String? _selectedTeamId;
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
late String _selectedTeamName;
|
||||||
|
late String? _selectedTeamLogo;
|
||||||
|
|
||||||
String _sortColumn = 'pts';
|
String _sortColumn = 'pts';
|
||||||
bool _isAscending = false;
|
bool _isAscending = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedTeamId = widget.initialTeamId;
|
||||||
|
_selectedTeamName = widget.initialTeamName;
|
||||||
|
_selectedTeamLogo = widget.initialTeamLogo;
|
||||||
|
|
||||||
|
// Se não vieram parâmetros da HomeScreen, tenta carregar do SharedPreferences
|
||||||
|
if (_selectedTeamId == null) {
|
||||||
|
_loadSelectedTeamFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to global active team changes (e.g., when user marks favorite)
|
||||||
|
globalActiveTeam.addListener(_onGlobalActiveTeamChanged);
|
||||||
|
|
||||||
|
// Se já existe um globalActiveTeam no momento da abertura da página, aplica-o
|
||||||
|
final atNow = globalActiveTeam.value;
|
||||||
|
if (atNow != null) {
|
||||||
|
_selectedTeamId = atNow.id;
|
||||||
|
_selectedTeamName = atNow.name;
|
||||||
|
_selectedTeamLogo = atNow.logo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onGlobalActiveTeamChanged() {
|
||||||
|
final at = globalActiveTeam.value;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Atualiza sempre para a equipa ativa global (favorita). Isto força a Status
|
||||||
|
// a mostrar a equipa marcada como favorita assim que o utilizador a define.
|
||||||
|
if (at != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = at.id;
|
||||||
|
_selectedTeamName = at.name;
|
||||||
|
_selectedTeamLogo = at.logo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _prefsKey(String key) {
|
||||||
|
final userId = _supabase.auth.currentUser?.id ?? 'guest';
|
||||||
|
return '${key}_$userId';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(StatusPage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// Quando a HomeScreen muda a equipa, a StatusPage atualiza automaticamente
|
||||||
|
if (widget.initialTeamId != oldWidget.initialTeamId) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = widget.initialTeamId;
|
||||||
|
_selectedTeamName = widget.initialTeamName;
|
||||||
|
_selectedTeamLogo = widget.initialTeamLogo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fallback: só usado se a HomeScreen não passou nenhuma equipa ainda
|
||||||
|
Future<void> _loadSelectedTeamFallback() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString(_prefsKey('last_team_id'));
|
||||||
|
if (savedId != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = savedId;
|
||||||
|
_selectedTeamName = prefs.getString(_prefsKey('last_team_name')) ?? "Selecionar Equipa";
|
||||||
|
_selectedTeamLogo = prefs.getString(_prefsKey('last_team_logo'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guarda a equipa selecionada localmente (quando muda dentro da StatusPage)
|
||||||
|
Future<void> _saveSelectedTeamLocally() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_selectedTeamId != null) {
|
||||||
|
await prefs.setString(_prefsKey('last_team_id'), _selectedTeamId!);
|
||||||
|
await prefs.setString(_prefsKey('last_team_name'), _selectedTeamName);
|
||||||
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
||||||
|
await prefs.setString(_prefsKey('last_team_logo'), _selectedTeamLogo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove(_prefsKey('last_team_logo'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Também guarda no Supabase
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null && _selectedTeamId != null) {
|
||||||
|
try {
|
||||||
|
await _supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': _selectedTeamId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
|
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
|
||||||
@@ -38,15 +148,38 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
color: bgColor,
|
color: bgColor,
|
||||||
borderRadius: BorderRadius.circular(15 * context.sf),
|
borderRadius: BorderRadius.circular(15 * context.sf),
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
(_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
|
||||||
|
? ClipOval(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: _selectedTeamLogo!,
|
||||||
|
width: 24 * context.sf,
|
||||||
|
height: 24 * context.sf,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 24 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(
|
||||||
|
Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 24 * context.sf),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield,
|
||||||
|
color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
SizedBox(width: 10 * context.sf),
|
SizedBox(width: 10 * context.sf),
|
||||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
Text(_selectedTeamName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)),
|
||||||
]),
|
]),
|
||||||
Icon(Icons.arrow_drop_down, color: textColor),
|
Icon(Icons.arrow_drop_down, color: textColor),
|
||||||
],
|
],
|
||||||
@@ -57,60 +190,124 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _selectedTeamId == null
|
child: _selectedTeamId == null
|
||||||
? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)))
|
? Center(
|
||||||
: StreamBuilder<List<Map<String, dynamic>>>(
|
child: Text(
|
||||||
stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
"Seleciona uma equipa acima.",
|
||||||
builder: (context, statsSnapshot) {
|
style: TextStyle(
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
color: Colors.grey, fontSize: 14 * context.sf),
|
||||||
stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName),
|
),
|
||||||
builder: (context, gamesSnapshot) {
|
)
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
stream: _supabase
|
||||||
builder: (context, membersSnapshot) {
|
.from('player_stats_with_names')
|
||||||
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
|
.stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
||||||
return const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
|
builder: (context, statsSnapshot) {
|
||||||
}
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _supabase
|
||||||
|
.from('games')
|
||||||
|
.stream(primaryKey: ['id']).eq('my_team', _selectedTeamName),
|
||||||
|
builder: (context, gamesSnapshot) {
|
||||||
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _supabase
|
||||||
|
.from('members')
|
||||||
|
.stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
||||||
|
builder: (context, membersSnapshot) {
|
||||||
|
if (statsSnapshot.connectionState ==
|
||||||
|
ConnectionState.waiting ||
|
||||||
|
gamesSnapshot.connectionState ==
|
||||||
|
ConnectionState.waiting ||
|
||||||
|
membersSnapshot.connectionState ==
|
||||||
|
ConnectionState.waiting) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppTheme.primaryRed));
|
||||||
|
}
|
||||||
|
|
||||||
final membersData = membersSnapshot.data ?? [];
|
final membersData = membersSnapshot.data ?? [];
|
||||||
if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)));
|
if (membersData.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
"Esta equipa não tem jogadores registados.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 14 * context.sf)));
|
||||||
|
}
|
||||||
|
|
||||||
final statsData = statsSnapshot.data ?? [];
|
final statsData = statsSnapshot.data ?? [];
|
||||||
final gamesData = gamesSnapshot.data ?? [];
|
final gamesData = gamesSnapshot.data ?? [];
|
||||||
final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length;
|
final totalGamesPlayedByTeam = gamesData
|
||||||
|
.where((g) => g['status'] == 'Terminado')
|
||||||
|
.length;
|
||||||
|
|
||||||
final List<Map<String, dynamic>> playerTotals = _aggregateStats(statsData, gamesData, membersData);
|
final List<Map<String, dynamic>> playerTotals =
|
||||||
final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam);
|
_aggregateStats(statsData, gamesData, membersData);
|
||||||
|
final teamTotals = _calculateTeamTotals(
|
||||||
|
playerTotals, totalGamesPlayedByTeam);
|
||||||
|
|
||||||
playerTotals.sort((a, b) {
|
playerTotals.sort((a, b) {
|
||||||
var valA = a[_sortColumn] ?? 0;
|
var valA = a[_sortColumn] ?? 0;
|
||||||
var valB = b[_sortColumn] ?? 0;
|
var valB = b[_sortColumn] ?? 0;
|
||||||
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
|
return _isAscending
|
||||||
});
|
? valA.compareTo(valB)
|
||||||
|
: valB.compareTo(valA);
|
||||||
|
});
|
||||||
|
|
||||||
return _buildStatsGrid(context, playerTotals, teamTotals, bgColor, textColor);
|
return _buildStatsGrid(
|
||||||
}
|
context, playerTotals, teamTotals, bgColor, textColor);
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 AGORA GUARDA TAMBÉM O IMAGE_URL DO MEMBRO PARA MOSTRAR NA TABELA
|
@override
|
||||||
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
void dispose() {
|
||||||
|
globalActiveTeam.removeListener(_onGlobalActiveTeamChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _aggregateStats(
|
||||||
|
List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
||||||
Map<String, Map<String, dynamic>> aggregated = {};
|
Map<String, Map<String, dynamic>> aggregated = {};
|
||||||
|
|
||||||
for (var member in members) {
|
for (var member in members) {
|
||||||
String name = member['name']?.toString() ?? "Desconhecido";
|
String name = member['name']?.toString() ?? "Desconhecido";
|
||||||
String? imageUrl = member['image_url']?.toString(); // 👈 CAPTURA A IMAGEM AQUI
|
String? imageUrl = member['image_url']?.toString();
|
||||||
aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
aggregated[name] = {
|
||||||
|
'name': name,
|
||||||
|
'image_url': imageUrl,
|
||||||
|
'j': 0,
|
||||||
|
'pts': 0,
|
||||||
|
'ast': 0,
|
||||||
|
'rbs': 0,
|
||||||
|
'stl': 0,
|
||||||
|
'blk': 0,
|
||||||
|
'mvp': 0,
|
||||||
|
'def': 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var row in stats) {
|
for (var row in stats) {
|
||||||
String name = row['player_name']?.toString() ?? "Desconhecido";
|
String name = row['player_name']?.toString() ?? "Desconhecido";
|
||||||
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'image_url': null, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
if (!aggregated.containsKey(name)) {
|
||||||
|
aggregated[name] = {
|
||||||
|
'name': name,
|
||||||
|
'image_url': null,
|
||||||
|
'j': 0,
|
||||||
|
'pts': 0,
|
||||||
|
'ast': 0,
|
||||||
|
'rbs': 0,
|
||||||
|
'stl': 0,
|
||||||
|
'blk': 0,
|
||||||
|
'mvp': 0,
|
||||||
|
'def': 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
aggregated[name]!['j'] += 1;
|
aggregated[name]!['j'] += 1;
|
||||||
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
||||||
aggregated[name]!['ast'] += (row['ast'] ?? 0);
|
aggregated[name]!['ast'] += (row['ast'] ?? 0);
|
||||||
@@ -118,140 +315,298 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
aggregated[name]!['stl'] += (row['stl'] ?? 0);
|
aggregated[name]!['stl'] += (row['stl'] ?? 0);
|
||||||
aggregated[name]!['blk'] += (row['blk'] ?? 0);
|
aggregated[name]!['blk'] += (row['blk'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var game in games) {
|
for (var game in games) {
|
||||||
String? mvp = game['mvp_name'];
|
String? mvp = game['mvp_name'];
|
||||||
String? defRaw = game['top_def_name'];
|
String? defRaw = game['top_def_name'];
|
||||||
if (mvp != null && aggregated.containsKey(mvp)) aggregated[mvp]!['mvp'] += 1;
|
if (mvp != null && aggregated.containsKey(mvp)) {
|
||||||
|
aggregated[mvp]!['mvp'] += 1;
|
||||||
|
}
|
||||||
if (defRaw != null) {
|
if (defRaw != null) {
|
||||||
String defName = defRaw.split(' (')[0].trim();
|
String defName = defRaw.split(' (')[0].trim();
|
||||||
if (aggregated.containsKey(defName)) aggregated[defName]!['def'] += 1;
|
if (aggregated.containsKey(defName)) {
|
||||||
|
aggregated[defName]!['def'] += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregated.values.toList();
|
return aggregated.values.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _calculateTeamTotals(List<Map<String, dynamic>> players, int teamGames) {
|
Map<String, dynamic> _calculateTeamTotals(
|
||||||
int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0;
|
List<Map<String, dynamic>> players, int teamGames) {
|
||||||
|
int tPts = 0,
|
||||||
|
tAst = 0,
|
||||||
|
tRbs = 0,
|
||||||
|
tStl = 0,
|
||||||
|
tBlk = 0,
|
||||||
|
tMvp = 0,
|
||||||
|
tDef = 0;
|
||||||
for (var p in players) {
|
for (var p in players) {
|
||||||
tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); tDef += (p['def'] as int);
|
tPts += (p['pts'] as int);
|
||||||
|
tAst += (p['ast'] as int);
|
||||||
|
tRbs += (p['rbs'] as int);
|
||||||
|
tStl += (p['stl'] as int);
|
||||||
|
tBlk += (p['blk'] as int);
|
||||||
|
tMvp += (p['mvp'] as int);
|
||||||
|
tDef += (p['def'] as int);
|
||||||
}
|
}
|
||||||
return {'name': 'TOTAL EQUIPA', 'image_url': null, 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
|
return {
|
||||||
|
'name': 'TOTAL EQUIPA',
|
||||||
|
'image_url': null,
|
||||||
|
'j': teamGames,
|
||||||
|
'pts': tPts,
|
||||||
|
'ast': tAst,
|
||||||
|
'rbs': tRbs,
|
||||||
|
'stl': tStl,
|
||||||
|
'blk': tBlk,
|
||||||
|
'mvp': tMvp,
|
||||||
|
'def': tDef,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
Widget _buildStatsGrid(
|
||||||
|
BuildContext context,
|
||||||
|
List<Map<String, dynamic>> players,
|
||||||
|
Map<String, dynamic> teamTotals,
|
||||||
|
Color bgColor,
|
||||||
|
Color textColor) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
width: double.infinity,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: DataTable(
|
physics: const ClampingScrollPhysics(),
|
||||||
columnSpacing: 25 * context.sf,
|
child: ConstrainedBox(
|
||||||
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
constraints:
|
||||||
dataRowMaxHeight: 60 * context.sf,
|
BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
dataRowMinHeight: 60 * context.sf,
|
child: DataTable(
|
||||||
columns: [
|
columnSpacing: 20 * context.sf,
|
||||||
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
|
horizontalMargin: 16 * context.sf,
|
||||||
_buildSortableColumn(context, 'J', 'j', textColor),
|
headingRowColor: WidgetStateProperty.all(
|
||||||
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
Theme.of(context).colorScheme.surface),
|
||||||
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
dataRowMaxHeight: 60 * context.sf,
|
||||||
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
|
dataRowMinHeight: 60 * context.sf,
|
||||||
_buildSortableColumn(context, 'STL', 'stl', textColor),
|
columns: [
|
||||||
_buildSortableColumn(context, 'BLK', 'blk', textColor),
|
DataColumn(
|
||||||
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
|
label: Text('JOGADOR',
|
||||||
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
|
style: TextStyle(color: textColor))),
|
||||||
],
|
_buildSortableColumn(context, 'J', 'j', textColor),
|
||||||
rows: [
|
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
||||||
...players.map((player) => DataRow(cells: [
|
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
||||||
DataCell(
|
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
|
||||||
Row(
|
_buildSortableColumn(context, 'STL', 'stl', textColor),
|
||||||
children: [
|
_buildSortableColumn(context, 'BLK', 'blk', textColor),
|
||||||
// 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇
|
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
|
||||||
ClipOval(
|
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
|
||||||
child: Container(
|
],
|
||||||
width: 30 * context.sf,
|
rows: [
|
||||||
height: 30 * context.sf,
|
...players.map((player) => DataRow(cells: [
|
||||||
color: Colors.grey.withOpacity(0.2),
|
DataCell(
|
||||||
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
|
Row(children: [
|
||||||
? CachedNetworkImage(
|
ClipOval(
|
||||||
imageUrl: player['image_url'],
|
child: Container(
|
||||||
fit: BoxFit.cover,
|
width: 30 * context.sf,
|
||||||
fadeInDuration: Duration.zero,
|
height: 30 * context.sf,
|
||||||
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
color: Colors.grey.withOpacity(0.2),
|
||||||
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
child: (player['image_url'] != null &&
|
||||||
)
|
player['image_url']
|
||||||
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
.toString()
|
||||||
),
|
.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: player['image_url'],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 18 * context.sf,
|
||||||
|
color: Colors.grey),
|
||||||
|
errorWidget: (context, url, error) =>
|
||||||
|
Icon(Icons.person,
|
||||||
|
size: 18 * context.sf,
|
||||||
|
color: Colors.grey),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person,
|
||||||
|
size: 18 * context.sf,
|
||||||
|
color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
Text(player['name'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13 * context.sf,
|
||||||
|
color: textColor)),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10 * context.sf),
|
DataCell(Center(
|
||||||
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
|
child: Text(player['j'].toString(),
|
||||||
]
|
style: TextStyle(color: textColor)))),
|
||||||
)
|
_buildStatCell(context, player['pts'], textColor,
|
||||||
|
isHighlight: true),
|
||||||
|
_buildStatCell(context, player['ast'], textColor),
|
||||||
|
_buildStatCell(context, player['rbs'], textColor),
|
||||||
|
_buildStatCell(context, player['stl'], textColor),
|
||||||
|
_buildStatCell(context, player['blk'], textColor),
|
||||||
|
_buildStatCell(context, player['def'], textColor,
|
||||||
|
isBlue: true),
|
||||||
|
_buildStatCell(context, player['mvp'], textColor,
|
||||||
|
isGold: true),
|
||||||
|
])),
|
||||||
|
DataRow(
|
||||||
|
color: WidgetStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
||||||
|
cells: [
|
||||||
|
DataCell(Text('TOTAL EQUIPA',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 12 * context.sf))),
|
||||||
|
DataCell(Center(
|
||||||
|
child: Text(teamTotals['j'].toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)))),
|
||||||
|
_buildStatCell(context, teamTotals['pts'], textColor,
|
||||||
|
isHighlight: true),
|
||||||
|
_buildStatCell(context, teamTotals['ast'], textColor),
|
||||||
|
_buildStatCell(context, teamTotals['rbs'], textColor),
|
||||||
|
_buildStatCell(context, teamTotals['stl'], textColor),
|
||||||
|
_buildStatCell(context, teamTotals['blk'], textColor),
|
||||||
|
_buildStatCell(context, teamTotals['def'], textColor,
|
||||||
|
isBlue: true),
|
||||||
|
_buildStatCell(context, teamTotals['mvp'], textColor,
|
||||||
|
isGold: true),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
],
|
||||||
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
),
|
||||||
_buildStatCell(context, player['ast'], textColor),
|
|
||||||
_buildStatCell(context, player['rbs'], textColor),
|
|
||||||
_buildStatCell(context, player['stl'], textColor),
|
|
||||||
_buildStatCell(context, player['blk'], textColor),
|
|
||||||
_buildStatCell(context, player['def'], textColor, isBlue: true),
|
|
||||||
_buildStatCell(context, player['mvp'], textColor, isGold: true),
|
|
||||||
])),
|
|
||||||
DataRow(
|
|
||||||
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
|
||||||
cells: [
|
|
||||||
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
|
|
||||||
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
|
|
||||||
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
|
|
||||||
_buildStatCell(context, teamTotals['ast'], textColor),
|
|
||||||
_buildStatCell(context, teamTotals['rbs'], textColor),
|
|
||||||
_buildStatCell(context, teamTotals['stl'], textColor),
|
|
||||||
_buildStatCell(context, teamTotals['blk'], textColor),
|
|
||||||
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
|
|
||||||
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey, Color textColor) {
|
DataColumn _buildSortableColumn(
|
||||||
return DataColumn(label: InkWell(
|
BuildContext context, String title, String sortKey, Color textColor) {
|
||||||
onTap: () => setState(() {
|
return DataColumn(
|
||||||
if (_sortColumn == sortKey) _isAscending = !_isAscending;
|
label: InkWell(
|
||||||
else { _sortColumn = sortKey; _isAscending = false; }
|
onTap: () => setState(() {
|
||||||
}),
|
if (_sortColumn == sortKey) {
|
||||||
child: Row(children: [
|
_isAscending = !_isAscending;
|
||||||
Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
} else {
|
||||||
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: AppTheme.primaryRed),
|
_sortColumn = sortKey;
|
||||||
]),
|
_isAscending = false;
|
||||||
));
|
}
|
||||||
|
}),
|
||||||
|
child: Row(children: [
|
||||||
|
Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)),
|
||||||
|
if (_sortColumn == sortKey)
|
||||||
|
Icon(
|
||||||
|
_isAscending
|
||||||
|
? Icons.arrow_drop_up
|
||||||
|
: Icons.arrow_drop_down,
|
||||||
|
size: 18 * context.sf,
|
||||||
|
color: AppTheme.primaryRed),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DataCell _buildStatCell(BuildContext context, int value, Color textColor, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
|
DataCell _buildStatCell(BuildContext context, int value, Color textColor,
|
||||||
return DataCell(Center(child: Container(
|
{bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
return DataCell(Center(
|
||||||
decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)),
|
child: Container(
|
||||||
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
|
padding: EdgeInsets.symmetric(
|
||||||
fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600,
|
horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
||||||
fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? AppTheme.successGreen : textColor))
|
decoration: BoxDecoration(
|
||||||
)),
|
color: isGold && value > 0
|
||||||
)));
|
? Colors.amber.withOpacity(0.2)
|
||||||
|
: (isBlue && value > 0
|
||||||
|
? Colors.blue.withOpacity(0.1)
|
||||||
|
: Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value == 0 ? "-" : value.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: (isHighlight || isGold || isBlue)
|
||||||
|
? FontWeight.w900
|
||||||
|
: FontWeight.w600,
|
||||||
|
fontSize: 14 * context.sf,
|
||||||
|
color: isGold && value > 0
|
||||||
|
? Colors.orange.shade900
|
||||||
|
: (isBlue && value > 0
|
||||||
|
? Colors.blue.shade800
|
||||||
|
: (isHighlight ? AppTheme.successGreen : textColor)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showTeamSelector(BuildContext context) {
|
void _showTeamSelector(BuildContext context) {
|
||||||
showModalBottomSheet(context: context, backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
|
showModalBottomSheet(
|
||||||
stream: _teamController.teamsStream,
|
context: context,
|
||||||
builder: (context, snapshot) {
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
final teams = snapshot.data ?? [];
|
builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
|
stream: _teamController.teamsStream,
|
||||||
title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
builder: (context, snapshot) {
|
||||||
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
|
final teams = snapshot.data ?? [];
|
||||||
));
|
return ListView.builder(
|
||||||
},
|
itemCount: teams.length,
|
||||||
));
|
itemBuilder: (context, i) {
|
||||||
|
final team = teams[i];
|
||||||
|
final logoUrl = team['image_url'];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 36 * context.sf,
|
||||||
|
height: 36 * context.sf,
|
||||||
|
color: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: logoUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 20 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(
|
||||||
|
Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 20 * context.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield,
|
||||||
|
color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(team['name'],
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = team['id'].toString();
|
||||||
|
_selectedTeamName = team['name'];
|
||||||
|
_selectedTeamLogo = logoUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _saveSelectedTeamLocally();
|
||||||
|
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
lib/utils/session_manager.dart
Normal file
20
lib/utils/session_manager.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class SessionManager {
|
||||||
|
static const _key = 'session_in_progress';
|
||||||
|
|
||||||
|
static Future<void> setInProgress(bool v) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_key, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> isInProgress() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getBool(_key) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> clear() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,10 +42,7 @@ class ActionSubtypeDialog extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(title, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||||
title,
|
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -80,7 +77,7 @@ class ActionSubtypeDialog extends StatelessWidget {
|
|||||||
side: BorderSide(color: Colors.white12, width: 1 * sf),
|
side: BorderSide(color: Colors.white12, width: 1 * sf),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => onSelected(e.key), // Retorna a chave correta (ex: "tov_3s")
|
onPressed: () => onSelected(e.key),
|
||||||
child: Text(
|
child: Text(
|
||||||
e.value,
|
e.value,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf),
|
||||||
@@ -106,7 +103,6 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
|||||||
final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed;
|
final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed;
|
||||||
final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList();
|
final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList();
|
||||||
|
|
||||||
// Função interna para verificar se o jogador tem de sair
|
|
||||||
void checkFouledOut() {
|
void checkFouledOut() {
|
||||||
final fouls = controller.playerStats[committerId]?["fls"] ?? 0;
|
final fouls = controller.playerStats[committerId]?["fls"] ?? 0;
|
||||||
final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId);
|
final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId);
|
||||||
@@ -116,12 +112,12 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
|||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false, // Obriga a fazer a substituição
|
barrierDismissible: false,
|
||||||
builder: (ctx) => SubstitutionDialog(
|
builder: (ctx) => SubstitutionDialog(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isOpponent: isCommitterOpponent,
|
isOpponent: isCommitterOpponent,
|
||||||
sf: sf,
|
sf: sf,
|
||||||
forcedStarterId: committerId, // Passamos o jogador que foi expulso
|
forcedStarterId: committerId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -155,10 +151,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
|||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text("Quem sofreu a falta?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||||
"Quem sofreu a falta?",
|
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -190,7 +183,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id");
|
controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id");
|
||||||
checkFouledOut(); // Verifica 5 faltas!
|
checkFouledOut();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 80 * sf,
|
width: 80 * sf,
|
||||||
@@ -232,9 +225,9 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
|||||||
icon: Icon(Icons.group, color: Colors.white, size: 16 * sf),
|
icon: Icon(Icons.group, color: Colors.white, size: 16 * sf),
|
||||||
label: Text("Equipa / Sem Vítima Específica", style: TextStyle(fontSize: 12 * sf)),
|
label: Text("Equipa / Sem Vítima Específica", style: TextStyle(fontSize: 12 * sf)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
controller.registerFoul("$prefixCommitter$committerId", foulType, "");
|
controller.registerFoul("$prefixCommitter$committerId", foulType, "");
|
||||||
checkFouledOut(); // Verifica 5 faltas!
|
checkFouledOut();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -282,10 +275,7 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is
|
|||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text("Houve Assistência?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||||
"Houve Assistência?",
|
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@@ -317,7 +307,11 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
controller.commitStat("add_ast", "$prefix$id");
|
controller.commitStat("add_ast", "$prefix$id");
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Assistência: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Assistência: $name'),
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
backgroundColor: AppTheme.successGreen,
|
||||||
|
));
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 75 * sf,
|
width: 75 * sf,
|
||||||
@@ -383,135 +377,83 @@ class TopScoreboard extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.placarDarkSurface,
|
color: AppTheme.placarDarkSurface,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(22 * sf),
|
bottomLeft: Radius.circular(22 * sf),
|
||||||
bottomRight: Radius.circular(22 * sf)),
|
bottomRight: Radius.circular(22 * sf),
|
||||||
|
),
|
||||||
border: Border.all(color: Colors.white, width: 2.0 * sf),
|
border: Border.all(color: Colors.white, width: 2.0 * sf),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildTeamSection(
|
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, AppTheme.myTeamBlue, false, sf),
|
||||||
controller.myTeam,
|
|
||||||
controller.myScore,
|
|
||||||
controller.myFouls,
|
|
||||||
controller.myTimeoutsUsed,
|
|
||||||
AppTheme.myTeamBlue,
|
|
||||||
false,
|
|
||||||
sf),
|
|
||||||
SizedBox(width: 20 * sf),
|
SizedBox(width: 20 * sf),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 4 * sf),
|
||||||
horizontal: 14 * sf, vertical: 4 * sf),
|
decoration: BoxDecoration(color: AppTheme.placarTimerBg, borderRadius: BorderRadius.circular(9 * sf)),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.placarTimerBg,
|
|
||||||
borderRadius: BorderRadius.circular(9 * sf)),
|
|
||||||
child: ValueListenableBuilder<Duration>(
|
child: ValueListenableBuilder<Duration>(
|
||||||
valueListenable: controller.durationNotifier,
|
valueListenable: controller.durationNotifier,
|
||||||
builder: (context, duration, child) {
|
builder: (context, duration, child) {
|
||||||
String formatTime =
|
String formatTime = "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
"${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
return Text(formatTime, style: TextStyle(color: Colors.white, fontSize: 24 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 1.5 * sf));
|
||||||
return Text(formatTime,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 24 * sf,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
letterSpacing: 1.5 * sf));
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4 * sf),
|
SizedBox(height: 4 * sf),
|
||||||
Text("PERÍODO ${controller.currentQuarter}",
|
Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: AppTheme.warningAmber, fontSize: 12 * sf, fontWeight: FontWeight.w900)),
|
||||||
style: TextStyle(
|
|
||||||
color: AppTheme.warningAmber,
|
|
||||||
fontSize: 12 * sf,
|
|
||||||
fontWeight: FontWeight.w900)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 20 * sf),
|
SizedBox(width: 20 * sf),
|
||||||
_buildTeamSection(
|
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, AppTheme.oppTeamRed, true, sf),
|
||||||
controller.opponentTeam,
|
|
||||||
controller.opponentScore,
|
|
||||||
controller.opponentFouls,
|
|
||||||
controller.opponentTimeoutsUsed,
|
|
||||||
AppTheme.oppTeamRed,
|
|
||||||
true,
|
|
||||||
sf),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTeamSection(String name, int score, int fouls, int timeouts,
|
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) {
|
||||||
Color color, bool isOpp, double sf) {
|
|
||||||
int displayFouls = fouls > 5 ? 5 : fouls;
|
int displayFouls = fouls > 5 ? 5 : fouls;
|
||||||
final timeoutIndicators = Row(
|
final timeoutIndicators = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: List.generate(
|
children: List.generate(3, (index) => Container(
|
||||||
3,
|
margin: EdgeInsets.symmetric(horizontal: 2.5 * sf),
|
||||||
(index) => Container(
|
width: 10 * sf,
|
||||||
margin: EdgeInsets.symmetric(horizontal: 2.5 * sf),
|
height: 10 * sf,
|
||||||
width: 10 * sf,
|
decoration: BoxDecoration(
|
||||||
height: 10 * sf,
|
shape: BoxShape.circle,
|
||||||
decoration: BoxDecoration(
|
color: index < timeouts ? AppTheme.warningAmber : Colors.grey.shade600,
|
||||||
shape: BoxShape.circle,
|
border: Border.all(color: Colors.white54, width: 1.0 * sf),
|
||||||
color: index < timeouts
|
),
|
||||||
? AppTheme.warningAmber
|
)),
|
||||||
: Colors.grey.shade600,
|
|
||||||
border:
|
|
||||||
Border.all(color: Colors.white54, width: 1.0 * sf)),
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
List<Widget> content = [
|
List<Widget> content = [
|
||||||
Column(children: [
|
Column(children: [_scoreBox(score, color, sf), SizedBox(height: 5 * sf), timeoutIndicators]),
|
||||||
_scoreBox(score, color, sf),
|
|
||||||
SizedBox(height: 5 * sf),
|
|
||||||
timeoutIndicators
|
|
||||||
]),
|
|
||||||
SizedBox(width: 12 * sf),
|
SizedBox(width: 12 * sf),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
|
||||||
isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
Text(name.toUpperCase(),
|
Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.0 * sf)),
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16 * sf,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
letterSpacing: 1.0 * sf)),
|
|
||||||
SizedBox(height: 3 * sf),
|
SizedBox(height: 3 * sf),
|
||||||
Text("FALTAS: $displayFouls",
|
Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? AppTheme.actionMiss : AppTheme.warningAmber, fontSize: 11 * sf, fontWeight: FontWeight.bold)),
|
||||||
style: TextStyle(
|
|
||||||
color: displayFouls >= 5
|
|
||||||
? AppTheme.actionMiss
|
|
||||||
: AppTheme.warningAmber,
|
|
||||||
fontSize: 11 * sf,
|
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
return Row(
|
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList());
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: isOpp ? content : content.reversed.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _scoreBox(int score, Color color, double sf) => Container(
|
Widget _scoreBox(int score, Color color, double sf) => Container(
|
||||||
width: 45 * sf,
|
width: 45 * sf, height: 35 * sf,
|
||||||
height: 35 * sf,
|
alignment: Alignment.center,
|
||||||
alignment: Alignment.center,
|
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6 * sf)),
|
||||||
decoration: BoxDecoration(
|
child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900)),
|
||||||
color: color, borderRadius: BorderRadius.circular(6 * sf)),
|
);
|
||||||
child: Text(score.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 20 * sf,
|
|
||||||
fontWeight: FontWeight.w900)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// SHIRT PAINTER
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
class ShirtPainter extends CustomPainter {
|
class ShirtPainter extends CustomPainter {
|
||||||
final Color color;
|
final Color color;
|
||||||
final bool isFouledOut;
|
final bool isFouledOut;
|
||||||
@@ -523,15 +465,8 @@ class ShirtPainter extends CustomPainter {
|
|||||||
final double h = size.height;
|
final double h = size.height;
|
||||||
final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color;
|
final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color;
|
||||||
|
|
||||||
final paint = Paint()
|
final paint = Paint()..color = shirtColor..style = PaintingStyle.fill;
|
||||||
..color = shirtColor
|
final trimPaint = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = w * 0.04..strokeJoin = StrokeJoin.round;
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final trimPaint = Paint()
|
|
||||||
..color = Colors.white
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeWidth = w * 0.04
|
|
||||||
..strokeJoin = StrokeJoin.round;
|
|
||||||
|
|
||||||
final path = Path();
|
final path = Path();
|
||||||
path.moveTo(w * 0.32, h * 0.10);
|
path.moveTo(w * 0.32, h * 0.10);
|
||||||
@@ -553,13 +488,61 @@ class ShirtPainter extends CustomPainter {
|
|||||||
bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut;
|
bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// PLAYER COURT CARD — com feedback de camisola e troca de posições
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
class PlayerCourtCard extends StatelessWidget {
|
class PlayerCourtCard extends StatelessWidget {
|
||||||
final PlacarController controller;
|
final PlacarController controller;
|
||||||
final String playerId;
|
final String playerId;
|
||||||
final bool isOpponent;
|
final bool isOpponent;
|
||||||
final double sf;
|
final double sf;
|
||||||
|
|
||||||
const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf});
|
const PlayerCourtCard({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.playerId,
|
||||||
|
required this.isOpponent,
|
||||||
|
required this.sf,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Camisola flutuante mostrada durante o drag ─────────────────────────────
|
||||||
|
Widget _dragFeedback(String number, Color teamColor) {
|
||||||
|
const double size = 64;
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
CustomPaint(
|
||||||
|
size: const Size(size, size),
|
||||||
|
painter: ShirtPainter(color: teamColor),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: size * 0.15),
|
||||||
|
child: Text(
|
||||||
|
number,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: size * 0.38,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
shadows: const [Shadow(color: Colors.black54, blurRadius: 3, offset: Offset(1, 1))],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -571,42 +554,52 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
|
|
||||||
return Draggable<String>(
|
return Draggable<String>(
|
||||||
data: "$prefix$playerId",
|
data: "$prefix$playerId",
|
||||||
feedback: Material(
|
// ✅ CORRIGIDO: mostra camisola + número durante o drag
|
||||||
color: Colors.transparent,
|
feedback: _dragFeedback(number, teamColor),
|
||||||
child: Container(
|
childWhenDragging: Opacity(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
opacity: 0.35,
|
||||||
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6)),
|
child: _playerCardUI(number, realName, stats, teamColor, false, false, false, sf),
|
||||||
child: Text(realName, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, realName, stats, teamColor, false, false, sf)),
|
|
||||||
child: DragTarget<String>(
|
child: DragTarget<String>(
|
||||||
|
onWillAcceptWithDetails: (details) {
|
||||||
|
final data = details.data;
|
||||||
|
// Jogadores da mesma equipa → troca de posição
|
||||||
|
if (data.startsWith("player_my_") || data.startsWith("player_opp_")) {
|
||||||
|
final sameTeam = isOpponent ? data.startsWith("player_opp_") : data.startsWith("player_my_");
|
||||||
|
return sameTeam && data != "$prefix$playerId";
|
||||||
|
}
|
||||||
|
return true; // aceita ações normais
|
||||||
|
},
|
||||||
onAcceptWithDetails: (details) {
|
onAcceptWithDetails: (details) {
|
||||||
final action = details.data;
|
final action = details.data;
|
||||||
|
|
||||||
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
|
// ── Troca de posição entre jogadores do campo ──────────────────
|
||||||
bool isMake = action.startsWith("add_");
|
if (action.startsWith("player_my_") || action.startsWith("player_opp_")) {
|
||||||
bool is3Pt = action.endsWith("_3");
|
final sameTeam = isOpponent ? action.startsWith("player_opp_") : action.startsWith("player_my_");
|
||||||
|
if (sameTeam && action != "$prefix$playerId") {
|
||||||
showDialog(
|
controller.swapCourtPlayers(action, "$prefix$playerId");
|
||||||
context: context,
|
}
|
||||||
builder: (ctx) => ZoneMapDialog(
|
return;
|
||||||
playerName: realName,
|
|
||||||
isMake: isMake,
|
|
||||||
is3PointAction: is3Pt,
|
|
||||||
onZoneSelected: (zone, points, relX, relY) {
|
|
||||||
Navigator.pop(ctx);
|
|
||||||
controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY);
|
|
||||||
|
|
||||||
if (isMake) {
|
|
||||||
showAssistDialog(context, controller, isOpponent, playerId, sf);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// ─── NOVO POP-UP PARA AS FALTAS ───
|
|
||||||
else if (action == "add_foul") {
|
// ── Ações normais (inalteradas) ────────────────────────────────
|
||||||
|
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
|
||||||
|
bool isMake = action.startsWith("add_");
|
||||||
|
bool is3Pt = action.endsWith("_3");
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => ZoneMapDialog(
|
||||||
|
playerName: realName,
|
||||||
|
isMake: isMake,
|
||||||
|
is3PointAction: is3Pt,
|
||||||
|
onZoneSelected: (zone, points, relX, relY) {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY);
|
||||||
|
if (isMake) showAssistDialog(context, controller, isOpponent, playerId, sf);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (action == "add_foul") {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => ActionSubtypeDialog(
|
builder: (ctx) => ActionSubtypeDialog(
|
||||||
@@ -621,14 +614,11 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
sf: sf,
|
sf: sf,
|
||||||
onSelected: (foulType) {
|
onSelected: (foulType) {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
// Depois de escolher o tipo de falta, abre o pop-up a perguntar quem sofreu
|
|
||||||
showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf);
|
showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
} else if (action == "add_tov") {
|
||||||
// ─── POP-UPS PARA TOV, STL E BLK ───
|
|
||||||
else if (action == "add_tov") {
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => ActionSubtypeDialog(
|
builder: (ctx) => ActionSubtypeDialog(
|
||||||
@@ -647,8 +637,7 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
} else if (action == "add_stl") {
|
||||||
else if (action == "add_stl") {
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => ActionSubtypeDialog(
|
builder: (ctx) => ActionSubtypeDialog(
|
||||||
@@ -664,8 +653,7 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
} else if (action == "add_blk") {
|
||||||
else if (action == "add_blk") {
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => ActionSubtypeDialog(
|
builder: (ctx) => ActionSubtypeDialog(
|
||||||
@@ -681,34 +669,55 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
} else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
|
||||||
// ─── FIM DOS POP-UPS ESPECIAIS ───
|
controller.handleActionDrag(context, action, "$prefix$playerId");
|
||||||
else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
|
} else if (action.startsWith("bench_")) {
|
||||||
controller.handleActionDrag(context, action, "$prefix$playerId");
|
controller.handleSubbing(context, action, playerId, isOpponent);
|
||||||
}
|
|
||||||
else if (action.startsWith("bench_")) {
|
|
||||||
controller.handleSubbing(context, action, playerId, isOpponent);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, candidateData, rejectedData) {
|
builder: (context, candidateData, rejectedData) {
|
||||||
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_")));
|
bool isSwapHover = candidateData.any((data) =>
|
||||||
bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
|
data != null &&
|
||||||
|
(data.startsWith("player_my_") || data.startsWith("player_opp_")) &&
|
||||||
|
data != "$prefix$playerId");
|
||||||
|
bool isSubbing = candidateData.any((data) =>
|
||||||
|
data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_")));
|
||||||
|
bool isActionHover = candidateData.any((data) =>
|
||||||
|
data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
|
||||||
|
|
||||||
return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, sf);
|
return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, isSwapHover, sf);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _playerCardUI(String number, String displayNameStr, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) {
|
Widget _playerCardUI(
|
||||||
|
String number,
|
||||||
|
String displayNameStr,
|
||||||
|
Map<String, int> stats,
|
||||||
|
Color teamColor,
|
||||||
|
bool isSubbing,
|
||||||
|
bool isActionHover,
|
||||||
|
bool isSwapHover,
|
||||||
|
double sf,
|
||||||
|
) {
|
||||||
bool isFouledOut = stats["fls"]! >= 5;
|
bool isFouledOut = stats["fls"]! >= 5;
|
||||||
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white;
|
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white;
|
||||||
Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent;
|
Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent;
|
||||||
|
|
||||||
if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; }
|
if (isSwapHover) {
|
||||||
else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; }
|
bgColor = Colors.green.shade50;
|
||||||
|
borderColor = Colors.green.shade600;
|
||||||
|
} else if (isSubbing) {
|
||||||
|
bgColor = Colors.blue.shade50;
|
||||||
|
borderColor = AppTheme.myTeamBlue;
|
||||||
|
} else if (isActionHover && !isFouledOut) {
|
||||||
|
bgColor = Colors.orange.shade50;
|
||||||
|
borderColor = AppTheme.actionPoints;
|
||||||
|
}
|
||||||
|
|
||||||
int fgm = stats["fgm"]!; int fga = stats["fga"]!;
|
int fgm = stats["fgm"]!;
|
||||||
|
int fga = stats["fga"]!;
|
||||||
String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0";
|
String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0";
|
||||||
String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr;
|
String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr;
|
||||||
final double shirtSize = 40 * sf;
|
final double shirtSize = 40 * sf;
|
||||||
@@ -734,10 +743,7 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
CustomPaint(
|
CustomPaint(
|
||||||
size: Size(shirtSize, shirtSize),
|
size: Size(shirtSize, shirtSize),
|
||||||
painter: ShirtPainter(
|
painter: ShirtPainter(color: teamColor, isFouledOut: isFouledOut),
|
||||||
color: teamColor,
|
|
||||||
isFouledOut: isFouledOut,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: shirtSize * 0.15),
|
padding: EdgeInsets.only(top: shirtSize * 0.15),
|
||||||
@@ -761,10 +767,24 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(displayName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
|
Text(
|
||||||
|
displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14 * sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isFouledOut ? AppTheme.actionMiss : Colors.black87,
|
||||||
|
decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(height: 1.5 * sf),
|
SizedBox(height: 1.5 * sf),
|
||||||
Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600)),
|
Text(
|
||||||
Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600)),
|
"${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)",
|
||||||
|
style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls",
|
||||||
|
style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -774,11 +794,15 @@ class PlayerCourtCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// SUBSTITUTION DIALOG
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
class SubstitutionDialog extends StatefulWidget {
|
class SubstitutionDialog extends StatefulWidget {
|
||||||
final PlacarController controller;
|
final PlacarController controller;
|
||||||
final bool isOpponent;
|
final bool isOpponent;
|
||||||
final double sf;
|
final double sf;
|
||||||
final String? forcedStarterId; // <--- ADICIONADO PARA EXPULSÕES
|
final String? forcedStarterId;
|
||||||
|
|
||||||
const SubstitutionDialog({
|
const SubstitutionDialog({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -804,26 +828,18 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
|
Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
|
||||||
String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam;
|
String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam;
|
||||||
bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null;
|
bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null;
|
||||||
bool get isForced => widget.forcedStarterId != null; // NOVO
|
bool get isForced => widget.forcedStarterId != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Se for obrigado a sair, já aparece selecionado!
|
if (isForced) _selectedStarterId = widget.forcedStarterId;
|
||||||
if (isForced) {
|
|
||||||
_selectedStarterId = widget.forcedStarterId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmSwap() {
|
void _confirmSwap() {
|
||||||
if (!canConfirm) return;
|
if (!canConfirm) return;
|
||||||
final benchPrefix = isOpp ? "bench_opp_" : "bench_my_";
|
final benchPrefix = isOpp ? "bench_opp_" : "bench_my_";
|
||||||
ctrl.handleSubbing(
|
ctrl.handleSubbing(context, "$benchPrefix$_selectedBenchId", _selectedStarterId!, isOpp);
|
||||||
context,
|
|
||||||
"$benchPrefix$_selectedBenchId",
|
|
||||||
_selectedStarterId!,
|
|
||||||
isOpp,
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +854,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1A1F2E),
|
color: const Color(0xFF1A1F2E),
|
||||||
borderRadius: BorderRadius.circular(14 * sf),
|
borderRadius: BorderRadius.circular(14 * sf),
|
||||||
border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), // Borda vermelha se for expulsão
|
border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -846,7 +862,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf),
|
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), // Fundo vermelho no título
|
color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540),
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)),
|
||||||
border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))),
|
border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))),
|
||||||
),
|
),
|
||||||
@@ -857,7 +873,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
isForced ? "SUBSTITUIÇÃO OBRIGATÓRIA (5 Faltas)" : "Substituição — ${teamName.toUpperCase()}",
|
isForced ? "SUBSTITUIÇÃO OBRIGATÓRIA (5 Faltas)" : "Substituição — ${teamName.toUpperCase()}",
|
||||||
style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600),
|
style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
if (!isForced) // Esconde o "X" de fechar se for forçado
|
if (!isForced)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => Navigator.pop(context),
|
onTap: () => Navigator.pop(context),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -876,10 +892,8 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
isStarter: true,
|
isStarter: true,
|
||||||
activeColor: activeColor,
|
activeColor: activeColor,
|
||||||
onTap: (id) {
|
onTap: (id) {
|
||||||
if (isForced) return; // Se for forçado, não deixa clicar/desmarcar o titular
|
if (isForced) return;
|
||||||
setState(() {
|
setState(() => _selectedStarterId = _selectedStarterId == id ? null : id);
|
||||||
_selectedStarterId = _selectedStarterId == id ? null : id;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf),
|
Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf),
|
||||||
@@ -893,13 +907,12 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
final fouls = ctrl.playerStats[id]?["fls"] ?? 0;
|
final fouls = ctrl.playerStats[id]?["fls"] ?? 0;
|
||||||
if (fouls >= 5) {
|
if (fouls >= 5) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
content: Text('🛑 ${ctrl.playerNames[id]} expulso!'),
|
content: Text('🛑 ${ctrl.playerNames[id]} expulso!'),
|
||||||
backgroundColor: AppTheme.actionMiss));
|
backgroundColor: AppTheme.actionMiss,
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() => _selectedBenchId = _selectedBenchId == id ? null : id);
|
||||||
_selectedBenchId = _selectedBenchId == id ? null : id;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -914,7 +927,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf),
|
padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (!isForced) // Esconde o botão de cancelar
|
if (!isForced)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white12,
|
backgroundColor: Colors.white12,
|
||||||
@@ -950,33 +963,24 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _hintText() {
|
String _hintText() {
|
||||||
if (isForced && _selectedBenchId == null) {
|
if (isForced && _selectedBenchId == null) return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente.";
|
||||||
return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente.";
|
if (_selectedStarterId == null && _selectedBenchId == null) return "Seleciona um titular e um suplente para fazer a troca";
|
||||||
} else if (_selectedStarterId == null && _selectedBenchId == null) {
|
if (_selectedStarterId != null && _selectedBenchId == null) return "Agora seleciona o suplente que vai entrar";
|
||||||
return "Seleciona um titular e um suplente para fazer a troca";
|
if (_selectedStarterId == null && _selectedBenchId != null) return "Agora seleciona o titular que vai sair";
|
||||||
} else if (_selectedStarterId != null && _selectedBenchId == null) {
|
final s = ctrl.playerNames[_selectedStarterId] ?? "";
|
||||||
return "Agora seleciona o suplente que vai entrar";
|
final sNum = ctrl.playerNumbers[_selectedStarterId] ?? "";
|
||||||
} else if (_selectedStarterId == null && _selectedBenchId != null) {
|
final b = ctrl.playerNames[_selectedBenchId] ?? "";
|
||||||
return "Agora seleciona o titular que vai sair";
|
final bNum = ctrl.playerNumbers[_selectedBenchId] ?? "";
|
||||||
} else {
|
return "#$sNum $s ↔ #$bNum $b";
|
||||||
final s = ctrl.playerNames[_selectedStarterId] ?? "";
|
|
||||||
final sNum = ctrl.playerNumbers[_selectedStarterId] ?? "";
|
|
||||||
final b = ctrl.playerNames[_selectedBenchId] ?? "";
|
|
||||||
final bNum = ctrl.playerNumbers[_selectedBenchId] ?? "";
|
|
||||||
return "#$sNum $s ↔ #$bNum $b";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _sectionLabel(String label) => Padding(
|
Widget _sectionLabel(String label) => Padding(
|
||||||
padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf),
|
padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(label.toUpperCase(), style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500)),
|
||||||
label.toUpperCase(),
|
),
|
||||||
style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500),
|
);
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _playerGrid({
|
Widget _playerGrid({
|
||||||
required List<String> players,
|
required List<String> players,
|
||||||
@@ -1021,10 +1025,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
CustomPaint(
|
CustomPaint(size: Size(36 * sf, 36 * sf), painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut)),
|
||||||
size: Size(36 * sf, 36 * sf),
|
|
||||||
painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 36 * sf * 0.15),
|
padding: EdgeInsets.only(top: 36 * sf * 0.15),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1042,12 +1043,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 3 * sf),
|
SizedBox(height: 3 * sf),
|
||||||
Text(
|
Text(shortName, style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center),
|
||||||
shortName,
|
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
if (isFouledOut)
|
if (isFouledOut)
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(top: 2 * sf),
|
margin: EdgeInsets.only(top: 2 * sf),
|
||||||
@@ -1068,6 +1064,11 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// HEATMAP DIALOG
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
class HeatmapDialog extends StatefulWidget {
|
class HeatmapDialog extends StatefulWidget {
|
||||||
final List<dynamic> shots;
|
final List<dynamic> shots;
|
||||||
final String myTeamName;
|
final String myTeamName;
|
||||||
@@ -1166,18 +1167,11 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = 'Todos'; _isMapVisible = true; }),
|
||||||
_selectedTeam = teamName;
|
|
||||||
_selectedPlayerId = 'Todos';
|
|
||||||
_isMapVisible = true;
|
|
||||||
}),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: teamColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8))),
|
||||||
color: teamColor,
|
|
||||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
|
Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
@@ -1205,11 +1199,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
|||||||
leading: Icon(Icons.person, color: teamColor),
|
leading: Icon(Icons.person, color: teamColor),
|
||||||
title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
|
title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
|
||||||
trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)),
|
trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)),
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = pId; _isMapVisible = true; }),
|
||||||
_selectedTeam = teamName;
|
|
||||||
_selectedPlayerId = pId;
|
|
||||||
_isMapVisible = true;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1247,11 +1237,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||||
child: Row(children: [
|
child: Row(children: [Icon(Icons.arrow_back, color: headerColor, size: 14), const SizedBox(width: 4), Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12))]),
|
||||||
Icon(Icons.arrow_back, color: headerColor, size: 14),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1274,19 +1260,16 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
|||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
CustomPaint(
|
CustomPaint(size: Size(constraints.maxWidth, constraints.maxHeight), painter: HeatmapCourtPainter()),
|
||||||
size: Size(constraints.maxWidth, constraints.maxHeight),
|
|
||||||
painter: HeatmapCourtPainter(),
|
|
||||||
),
|
|
||||||
...filteredShots.map((shot) => Positioned(
|
...filteredShots.map((shot) => Positioned(
|
||||||
left: (shot.relativeX * constraints.maxWidth) - 8,
|
left: (shot.relativeX * constraints.maxWidth) - 8,
|
||||||
top: (shot.relativeY * constraints.maxHeight) - 8,
|
top: (shot.relativeY * constraints.maxHeight) - 8,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 8,
|
radius: 8,
|
||||||
backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss,
|
backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss,
|
||||||
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white),
|
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -1345,6 +1328,10 @@ class HeatmapCourtPainter extends CustomPainter {
|
|||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// PLAY BY PLAY DIALOG
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
class PlayByPlayDialog extends StatelessWidget {
|
class PlayByPlayDialog extends StatelessWidget {
|
||||||
final PlacarController controller;
|
final PlacarController controller;
|
||||||
const PlayByPlayDialog({super.key, required this.controller});
|
const PlayByPlayDialog({super.key, required this.controller});
|
||||||
@@ -1387,6 +1374,10 @@ class PlayByPlayDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// BOX SCORE DIALOG
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
class BoxScoreDialog extends StatelessWidget {
|
class BoxScoreDialog extends StatelessWidget {
|
||||||
final PlacarController controller;
|
final PlacarController controller;
|
||||||
final double sf;
|
final double sf;
|
||||||
@@ -1400,10 +1391,7 @@ class BoxScoreDialog extends StatelessWidget {
|
|||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
backgroundColor: AppTheme.placarDarkSurface,
|
backgroundColor: AppTheme.placarDarkSurface,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12 * sf), side: BorderSide(color: Colors.white24, width: 1 * sf)),
|
||||||
borderRadius: BorderRadius.circular(12 * sf),
|
|
||||||
side: BorderSide(color: Colors.white24, width: 1 * sf),
|
|
||||||
),
|
|
||||||
insetPadding: EdgeInsets.all(8 * sf),
|
insetPadding: EdgeInsets.all(8 * sf),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -1419,12 +1407,7 @@ class BoxScoreDialog extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
|
Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
|
||||||
IconButton(
|
IconButton(icon: Icon(Icons.close, color: Colors.white, size: 24 * sf), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () => Navigator.pop(context))
|
||||||
icon: Icon(Icons.close, color: Colors.white, size: 24 * sf),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1529,7 +1512,7 @@ class BoxScoreDialog extends StatelessWidget {
|
|||||||
DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))),
|
DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))),
|
||||||
DataCell(Text((s['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))),
|
DataCell(Text((s['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))),
|
||||||
DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
|
DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
|
||||||
DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), // CORRIGIDO PARA MOSTRAR OS 3 SEG NO BOX SCORE
|
DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
|
||||||
DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
|
DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
|
||||||
DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))),
|
DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))),
|
||||||
]);
|
]);
|
||||||
|
|||||||
239
lib/widgets/share_game_dialog.dart
Normal file
239
lib/widgets/share_game_dialog.dart
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:playmaker/controllers/game_sharing_controller.dart';
|
||||||
|
|
||||||
|
class ShareGameDialog extends StatefulWidget {
|
||||||
|
final String gameId;
|
||||||
|
final GameSharingController controller;
|
||||||
|
final String? activeSessionId;
|
||||||
|
final String? activeShareCode;
|
||||||
|
|
||||||
|
const ShareGameDialog({
|
||||||
|
super.key,
|
||||||
|
required this.gameId,
|
||||||
|
required this.controller,
|
||||||
|
this.activeSessionId,
|
||||||
|
this.activeShareCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShareGameDialog> createState() => _ShareGameDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareGameDialogState extends State<ShareGameDialog> {
|
||||||
|
String? _shareCode;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_shareCode = widget.activeShareCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createSession() async {
|
||||||
|
setState(() {
|
||||||
|
_error = null;
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final code = await widget.controller.createShareSession(widget.gameId);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
if (code == null) {
|
||||||
|
_error = 'Erro ao criar sessão. Tenta novamente.';
|
||||||
|
} else {
|
||||||
|
_shareCode = code;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_shareCode != null && widget.activeSessionId == null) {
|
||||||
|
final session = await widget.controller.getActiveSessionForGame(
|
||||||
|
widget.gameId,
|
||||||
|
);
|
||||||
|
if (session != null && mounted) {
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop({'session_id': session['id'], 'share_code': _shareCode});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
title: Text(
|
||||||
|
'Partilhar Jogo',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_shareCode != null) ...[
|
||||||
|
Text(
|
||||||
|
'Código de partilha:',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SelectableText(
|
||||||
|
_shareCode!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Envie este código ao seu parceiro e peça que ele entre no jogo.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Text(
|
||||||
|
'Crie um código e partilhe o jogo com outro utilizador.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (_error != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Fechar'),
|
||||||
|
),
|
||||||
|
if (_shareCode == null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _createSession,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Criar código'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop({
|
||||||
|
'session_id': widget.activeSessionId,
|
||||||
|
'share_code': _shareCode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JoinGameDialog extends StatefulWidget {
|
||||||
|
final GameSharingController controller;
|
||||||
|
|
||||||
|
const JoinGameDialog({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<JoinGameDialog> createState() => _JoinGameDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _JoinGameDialogState extends State<JoinGameDialog> {
|
||||||
|
final TextEditingController _codeController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
Future<void> _joinSession() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await widget.controller.joinGameByCode(
|
||||||
|
_codeController.text.trim(),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Código inválido ou sessão não disponível.';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
title: Text(
|
||||||
|
'Entrar em Jogo Partilhado',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Digite o código enviado pelo outro utilizador.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _codeController,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Código',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'ABC123',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_error != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _joinSession,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Entrar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import app_links
|
|||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import printing
|
import printing
|
||||||
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|||||||
142
pubspec.lock
142
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.9"
|
version: "3.6.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -109,10 +109,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -121,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -177,6 +185,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.1"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
excel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: excel
|
||||||
|
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -189,10 +213,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.2.0"
|
||||||
|
ffi_leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi_leak_tracker
|
||||||
|
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -288,6 +320,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0"
|
version: "2.5.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
gotrue:
|
gotrue:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -304,6 +344,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -324,10 +372,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
version: "4.3.0"
|
||||||
image_cropper:
|
image_cropper:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -468,18 +516,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -496,6 +544,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
native_toolchain_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_c
|
||||||
|
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.6"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -624,14 +680,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
posix:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: posix
|
|
||||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.5.0"
|
|
||||||
postgrest:
|
postgrest:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -656,6 +704,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
qr:
|
qr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -672,6 +728,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -688,6 +752,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.1.0"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -873,10 +953,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.10"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -997,6 +1077,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1013,6 +1101,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
yet_another_json_isolate:
|
yet_another_json_isolate:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1023,4 +1119,4 @@ packages:
|
|||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.38.1"
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ dependencies:
|
|||||||
shared_preferences: ^2.5.4
|
shared_preferences: ^2.5.4
|
||||||
printing: ^5.14.3
|
printing: ^5.14.3
|
||||||
pdf: ^3.12.0
|
pdf: ^3.12.0
|
||||||
|
excel: ^4.0.6
|
||||||
|
share_plus: ^13.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -69,6 +71,9 @@ flutter:
|
|||||||
- assets/assit.png
|
- assets/assit.png
|
||||||
- assets/tov.png
|
- assets/tov.png
|
||||||
- assets/stl.png
|
- assets/stl.png
|
||||||
|
- assets/campone.png
|
||||||
|
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: playmaker
|
- family: playmaker
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
app_links
|
app_links
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
printing
|
printing
|
||||||
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user