Compare commits

...

8 Commits

Author SHA1 Message Date
144a6c71a5 pronta para a pap? 2026-06-15 15:47:24 +01:00
29e887cb14 espero que nao presisse mudar. 2026-06-11 09:52:06 +01:00
947e119dba tentar aresolver a home 2026-06-08 14:54:04 +01:00
7d2f3c4679 bora para o relatorio 2026-05-21 10:51:35 +01:00
332361c296 nao sei 2026-05-15 12:43:30 +01:00
1e38c4ad57 JOGO 2026-05-11 17:22:04 +01:00
60656d77e8 pdf e exel 2026-05-06 12:47:17 +01:00
c3a90f2816 ok 2026-05-04 15:00:09 +01:00
27 changed files with 5064 additions and 1661 deletions

51
.github/copilot-instructions.md vendored Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

108
create_tables.sql Normal file
View 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();

View 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");
}
}
}

View File

@@ -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;
} }
@@ -94,4 +102,4 @@ class GameController {
} }
} }
void dispose() {} void dispose() {}
} }

View 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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
} }

View File

@@ -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,

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Para as orientações 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

View 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 = ["", "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 = ["", "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');
}
}
}

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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,29 +12,22 @@ 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,
required String opponentTeam, required String opponentTeam,
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),
), ),
]), ]),
); );

View File

@@ -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) {

View File

@@ -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 '../utils/size_extension.dart'; import '../controllers/active_team.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();
@@ -15,12 +26,111 @@ class StatusPage extends StatefulWidget {
class _StatusPageState extends State<StatusPage> { 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;
@@ -35,18 +145,41 @@ class _StatusPageState extends State<StatusPage> {
child: Container( child: Container(
padding: EdgeInsets.all(12 * context.sf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
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)
SizedBox(width: 10 * context.sf), ? ClipOval(
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor)) 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),
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,201 +190,423 @@ 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] = {
aggregated[name]!['j'] += 1; '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]!['pts'] += (row['pts'] ?? 0); aggregated[name]!['pts'] += (row['pts'] ?? 0);
aggregated[name]!['ast'] += (row['ast'] ?? 0); aggregated[name]!['ast'] += (row['ast'] ?? 0);
aggregated[name]!['rbs'] += (row['rbs'] ?? 0); aggregated[name]!['rbs'] += (row['rbs'] ?? 0);
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);
},
);
},
);
},
),
);
} }
} }

View 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);
}
}

File diff suppressed because it is too large Load Diff

View 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'),
),
],
);
}
}

View File

@@ -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"))

View File

@@ -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"

View File

@@ -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:

View File

@@ -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';

View File

@@ -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"));
} }

View File

@@ -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
) )