JOGO
This commit is contained in:
108
create_tables.sql
Normal file
108
create_tables.sql
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- GARANTIR QUE A TABELA PROFILES TEM TODAS AS COLUNAS
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Verifica e adiciona colunas que podem estar em falta
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='id'
|
||||||
|
) THEN
|
||||||
|
CREATE TABLE profiles (
|
||||||
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
username VARCHAR(255),
|
||||||
|
full_name VARCHAR(255),
|
||||||
|
avatar_url TEXT,
|
||||||
|
selected_team_id UUID,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Adiciona colunas em falta se necessário
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='username'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN username VARCHAR(255);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='full_name'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN full_name VARCHAR(255);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='avatar_url'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN avatar_url TEXT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='profiles' AND column_name='selected_team_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE profiles ADD COLUMN selected_team_id UUID;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CRIAR TABELAS DE COMPARTILHAMENTO DE JOGO
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS game_sync_events;
|
||||||
|
DROP TABLE IF EXISTS game_sessions;
|
||||||
|
|
||||||
|
CREATE TABLE game_sessions (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||||
|
created_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
|
shared_with_user_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
|
||||||
|
share_code VARCHAR(10) UNIQUE NOT NULL,
|
||||||
|
status VARCHAR(20) DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE game_sync_events (
|
||||||
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
session_id UUID NOT NULL REFERENCES game_sessions(id) ON DELETE CASCADE,
|
||||||
|
action_type VARCHAR(50) NOT NULL,
|
||||||
|
action_data JSONB,
|
||||||
|
triggered_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CRIAR ÍNDICES PARA PERFORMANCE
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sessions_game_id ON game_sessions(game_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sessions_share_code ON game_sessions(share_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sessions_status ON game_sessions(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_game_sync_events_session_id ON game_sync_events(session_id);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CRIAR TRIGGER AUTOMÁTICO PARA NOVOS UTILIZADORES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.profiles (id, username)
|
||||||
|
VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.email))
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Drop the trigger if it exists and create it
|
||||||
|
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||||
|
CREATE TRIGGER on_auth_user_created
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user();
|
||||||
256
lib/controllers/game_sharing_controller.dart
Normal file
256
lib/controllers/game_sharing_controller.dart
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
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)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
print("👤 Criador: ${creatorData['full_name'] ?? creatorData['username']}");
|
||||||
|
|
||||||
|
return {
|
||||||
|
'session_id': session['id'],
|
||||||
|
'game_id': gameId,
|
||||||
|
'creator_name': creatorData['full_name'] ?? creatorData['username'] ?? 'Utilizador',
|
||||||
|
'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,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _supabase.from('game_sync_events').insert({
|
||||||
|
'session_id': sessionId,
|
||||||
|
'action_type': actionType,
|
||||||
|
'action_data': actionData,
|
||||||
|
'triggered_by': myUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:playmaker/icons.dart/resaltosicon.dart';
|
import 'package:playmaker/icons.dart/resaltosicon.dart';
|
||||||
import 'package:playmaker/widgets/placar_widgets.dart'; // Mantém este import
|
import 'package:playmaker/widgets/placar_widgets.dart'; // Mantém este import
|
||||||
import 'dart:math' as math;
|
import 'package:playmaker/widgets/share_game_dialog.dart';
|
||||||
import '../classe/theme.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../controllers/placar_controller.dart';
|
|
||||||
import 'package:playmaker/zone_map_dialog.dart';
|
import '../classe/theme.dart';
|
||||||
|
import '../controllers/game_sharing_controller.dart';
|
||||||
|
import '../controllers/placar_controller.dart';
|
||||||
|
|
||||||
class PlacarPage extends StatefulWidget {
|
class PlacarPage extends StatefulWidget {
|
||||||
final String gameId, myTeam, opponentTeam;
|
final String gameId, myTeam, opponentTeam;
|
||||||
|
|
||||||
const PlacarPage({
|
const PlacarPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.gameId,
|
required this.gameId,
|
||||||
required this.myTeam,
|
required this.myTeam,
|
||||||
required this.opponentTeam
|
required this.opponentTeam,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -23,6 +28,12 @@ class PlacarPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _PlacarPageState extends State<PlacarPage> {
|
class _PlacarPageState extends State<PlacarPage> {
|
||||||
late PlacarController _controller;
|
late PlacarController _controller;
|
||||||
|
final GameSharingController _sharingController = GameSharingController();
|
||||||
|
String? _sessionId;
|
||||||
|
String? _shareCode;
|
||||||
|
String _sharedWithName = '';
|
||||||
|
StreamSubscription? _syncSubscription;
|
||||||
|
bool _isApplyingRemoteSync = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -31,23 +42,33 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
DeviceOrientation.landscapeRight,
|
DeviceOrientation.landscapeRight,
|
||||||
DeviceOrientation.landscapeLeft,
|
DeviceOrientation.landscapeLeft,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
_controller = PlacarController(
|
_controller = PlacarController(
|
||||||
gameId: widget.gameId,
|
gameId: widget.gameId,
|
||||||
myTeam: widget.myTeam,
|
myTeam: widget.myTeam,
|
||||||
opponentTeam: widget.opponentTeam,
|
opponentTeam: widget.opponentTeam,
|
||||||
);
|
);
|
||||||
_controller.loadPlayers();
|
_controller.loadPlayers().then((_) => _initializeShareForGame());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_syncSubscription?.cancel();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
|
Widget _buildFloatingFoulBtn(
|
||||||
|
String label,
|
||||||
|
Color color,
|
||||||
|
String action,
|
||||||
|
IconData icon,
|
||||||
|
double left,
|
||||||
|
double right,
|
||||||
|
double top,
|
||||||
|
double sf,
|
||||||
|
) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: top,
|
top: top,
|
||||||
left: left > 0 ? left : null,
|
left: left > 0 ? left : null,
|
||||||
@@ -57,39 +78,62 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
feedback: Material(
|
feedback: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 30 * sf,
|
radius: 30 * sf,
|
||||||
backgroundColor: color.withOpacity(0.8),
|
backgroundColor: color.withOpacity(0.8),
|
||||||
child: Icon(icon, color: Colors.white, size: 30 * sf)
|
child: Icon(icon, color: Colors.white, size: 30 * sf),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 27 * sf,
|
radius: 27 * sf,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
child: Icon(icon, color: Colors.white, size: 28 * sf),
|
child: Icon(icon, color: Colors.white, size: 28 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5 * sf),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12 * sf,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 5 * sf),
|
|
||||||
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback? onTap, required double size, bool isLoading = false}) {
|
Widget _buildCornerBtn({
|
||||||
|
required String heroTag,
|
||||||
|
required IconData icon,
|
||||||
|
required Color color,
|
||||||
|
required VoidCallback? onTap,
|
||||||
|
required double size,
|
||||||
|
bool isLoading = false,
|
||||||
|
}) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
heroTag: heroTag,
|
heroTag: heroTag,
|
||||||
backgroundColor: onTap == null ? Colors.grey : color,
|
backgroundColor: onTap == null ? Colors.grey : color,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
|
shape: RoundedRectangleBorder(
|
||||||
elevation: 5,
|
borderRadius: BorderRadius.circular(14 * (size / 50)),
|
||||||
|
),
|
||||||
|
elevation: 5,
|
||||||
onPressed: isLoading ? null : onTap,
|
onPressed: isLoading ? null : onTap,
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? SizedBox(width: size * 0.45, height: size * 0.45, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
|
? SizedBox(
|
||||||
: Icon(icon, color: Colors.white, size: size * 0.55),
|
width: size * 0.45,
|
||||||
|
height: size * 0.45,
|
||||||
|
child: const CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(icon, color: Colors.white, size: size * 0.55),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -109,25 +153,183 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeShareForGame() async {
|
||||||
|
final activeSession = await _sharingController.getActiveSessionForGame(
|
||||||
|
widget.gameId,
|
||||||
|
);
|
||||||
|
if (activeSession == null) return;
|
||||||
|
|
||||||
|
_sessionId = activeSession['id']?.toString();
|
||||||
|
_shareCode = activeSession['share_code']?.toString();
|
||||||
|
final sharedWith = activeSession['shared_with_user_id']?.toString();
|
||||||
|
|
||||||
|
if (sharedWith != null && sharedWith.isNotEmpty) {
|
||||||
|
_sharedWithName = await _resolveUserName(sharedWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupSyncListener();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _resolveUserName(String userId) async {
|
||||||
|
try {
|
||||||
|
final profile = await Supabase.instance.client
|
||||||
|
.from('profiles')
|
||||||
|
.select('username, full_name')
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return profile['full_name']?.toString() ??
|
||||||
|
profile['username']?.toString() ??
|
||||||
|
'Parceiro';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Parceiro';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupSyncListener() {
|
||||||
|
if (_sessionId == null) return;
|
||||||
|
_syncSubscription?.cancel();
|
||||||
|
_syncSubscription = _sharingController.listenToGameSync(_sessionId!).listen(
|
||||||
|
(event) {
|
||||||
|
if (event is List && event.isNotEmpty) {
|
||||||
|
final record = event.last as Map<String, dynamic>?;
|
||||||
|
if (record != null) {
|
||||||
|
_handleSyncRecords(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSyncRecords(Map<String, dynamic> record) {
|
||||||
|
final triggeredBy = record['triggered_by']?.toString();
|
||||||
|
final currentUserId = Supabase.instance.client.auth.currentUser?.id;
|
||||||
|
if (triggeredBy == null || triggeredBy == currentUserId) return;
|
||||||
|
|
||||||
|
_applyRemoteSyncEvent(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
|
||||||
|
final actionType = record['action_type']?.toString();
|
||||||
|
final actionData = Map<String, dynamic>.from(record['action_data'] ?? {});
|
||||||
|
if (actionType == 'toggle_timer') {
|
||||||
|
final paused = actionData['paused'] == true;
|
||||||
|
final remainingSeconds =
|
||||||
|
int.tryParse(actionData['remaining_seconds']?.toString() ?? '') ??
|
||||||
|
_controller.durationNotifier.value.inSeconds;
|
||||||
|
_controller.durationNotifier.value = Duration(seconds: remainingSeconds);
|
||||||
|
|
||||||
|
if (paused && _controller.isRunning) {
|
||||||
|
_isApplyingRemoteSync = true;
|
||||||
|
_controller.toggleTimer(context);
|
||||||
|
_isApplyingRemoteSync = false;
|
||||||
|
} else if (!paused && !_controller.isRunning) {
|
||||||
|
_isApplyingRemoteSync = true;
|
||||||
|
_controller.toggleTimer(context);
|
||||||
|
_isApplyingRemoteSync = false;
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTimerButton(BuildContext context) {
|
||||||
|
_controller.toggleTimer(context);
|
||||||
|
|
||||||
|
if (_sessionId != null && !_isApplyingRemoteSync) {
|
||||||
|
_sharingController.sendSyncEvent(_sessionId!, 'toggle_timer', {
|
||||||
|
'paused': !_controller.isRunning,
|
||||||
|
'remaining_seconds': _controller.durationNotifier.value.inSeconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openShareDialog(BuildContext context) async {
|
||||||
|
final result = await showDialog<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => ShareGameDialog(
|
||||||
|
gameId: widget.gameId,
|
||||||
|
controller: _sharingController,
|
||||||
|
activeSessionId: _sessionId,
|
||||||
|
activeShareCode: _shareCode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
_sessionId = result['session_id']?.toString();
|
||||||
|
_shareCode = result['share_code']?.toString();
|
||||||
|
_setupSyncListener();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openJoinDialog(BuildContext context) async {
|
||||||
|
final result = await showDialog<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => JoinGameDialog(controller: _sharingController),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
_sessionId = result['session_id']?.toString();
|
||||||
|
_shareCode = result['share_code']?.toString();
|
||||||
|
_sharedWithName = result['creator_name']?.toString() ?? '';
|
||||||
|
_setupSyncListener();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShareStatus(double sf) {
|
||||||
|
if (_sessionId == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final text = _sharedWithName.isNotEmpty
|
||||||
|
? 'Partilhado com $_sharedWithName'
|
||||||
|
: 'Sessão partilhada: $_shareCode';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.55),
|
||||||
|
borderRadius: BorderRadius.circular(14 * sf),
|
||||||
|
border: Border.all(color: Colors.white24),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13 * sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
final double wScreen = MediaQuery.of(context).size.width;
|
||||||
final double hScreen = MediaQuery.of(context).size.height;
|
final double hScreen = MediaQuery.of(context).size.height;
|
||||||
final double sf = math.min(wScreen / 1150, hScreen / 720);
|
final double sf = math.min(wScreen / 1150, hScreen / 720);
|
||||||
final double cornerBtnSize = 48 * sf;
|
final double cornerBtnSize = 48 * sf;
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
if (_controller.isLoading) {
|
if (_controller.isLoading) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppTheme.placarDarkSurface,
|
backgroundColor: AppTheme.placarDarkSurface,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
Text(
|
||||||
SizedBox(height: 35 * sf),
|
"PREPARANDO O PAVILHÃO",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white24,
|
||||||
|
fontSize: 45 * sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 35 * sf),
|
||||||
const CircularProgressIndicator(color: Colors.orangeAccent),
|
const CircularProgressIndicator(color: Colors.orangeAccent),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -136,16 +338,23 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppTheme.placarBackground,
|
backgroundColor: AppTheme.placarBackground,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false, bottom: false,
|
top: false,
|
||||||
|
bottom: false,
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
ignoring: _controller.isSaving,
|
ignoring: _controller.isSaving,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
|
margin: EdgeInsets.only(
|
||||||
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
|
left: 65 * sf,
|
||||||
|
right: 65 * sf,
|
||||||
|
bottom: 55 * sf,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.white, width: 2.5),
|
||||||
|
),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final w = constraints.maxWidth;
|
final w = constraints.maxWidth;
|
||||||
@@ -155,70 +364,254 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTapDown: (details) {
|
onTapDown: (details) {
|
||||||
if (_controller.isSelectingShotLocation) {
|
if (_controller.isSelectingShotLocation) {
|
||||||
bool isMake = _controller.pendingAction?.startsWith("add_pts_") ?? false;
|
bool isMake =
|
||||||
|
_controller.pendingAction?.startsWith(
|
||||||
|
"add_pts_",
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
String? pData = _controller.pendingPlayerId;
|
String? pData = _controller.pendingPlayerId;
|
||||||
|
|
||||||
_controller.registerShotLocation(context, details.localPosition, Size(w, h));
|
_controller.registerShotLocation(
|
||||||
|
context,
|
||||||
|
details.localPosition,
|
||||||
|
Size(w, h),
|
||||||
|
);
|
||||||
|
|
||||||
if (isMake && pData != null) {
|
if (isMake && pData != null) {
|
||||||
bool isOpp = pData.startsWith("player_opp_");
|
bool isOpp = pData.startsWith(
|
||||||
String pId = pData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
"player_opp_",
|
||||||
showAssistDialog(context, _controller, isOpp, pId, sf);
|
);
|
||||||
|
String pId = pData
|
||||||
|
.replaceAll("player_my_", "")
|
||||||
|
.replaceAll("player_opp_", "");
|
||||||
|
showAssistDialog(
|
||||||
|
context,
|
||||||
|
_controller,
|
||||||
|
isOpp,
|
||||||
|
pId,
|
||||||
|
sf,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('assets/campo.png'),
|
image: AssetImage('assets/campo.png'),
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_controller.isSelectingShotLocation && _controller.myCourt.length >= 5 && _controller.oppCourt.length >= 5) ...[
|
|
||||||
Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[0], isOpponent: false, sf: sf)),
|
|
||||||
Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[1], isOpponent: false, sf: sf)),
|
|
||||||
Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[2], isOpponent: false, sf: sf)),
|
|
||||||
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[3], isOpponent: false, sf: sf)),
|
|
||||||
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[4], isOpponent: false, sf: sf)),
|
|
||||||
|
|
||||||
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[0], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[1], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[2], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[3], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[4], isOpponent: true, sf: sf)),
|
|
||||||
],
|
|
||||||
if (!_controller.isSelectingShotLocation) ...[
|
|
||||||
_buildFloatingFoulBtn("FALTA +", AppTheme.actionPoints, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
|
|
||||||
_buildFloatingFoulBtn("FALTA -", AppTheme.actionMiss, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
|
|
||||||
],
|
|
||||||
if (!_controller.isSelectingShotLocation)
|
|
||||||
Positioned(
|
|
||||||
top: (h * 0.32) + (40 * sf),
|
|
||||||
left: 0, right: 0,
|
|
||||||
child: Center(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => _controller.toggleTimer(context),
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 68 * sf,
|
|
||||||
backgroundColor: Colors.grey.withOpacity(0.5),
|
|
||||||
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf)
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
|
),
|
||||||
|
if (!_controller.isSelectingShotLocation &&
|
||||||
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
|
_controller.myCourt.length >= 5 &&
|
||||||
|
_controller.oppCourt.length >= 5) ...[
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.25,
|
||||||
|
left: w * 0.02,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.myCourt[0],
|
||||||
|
isOpponent: false,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.68,
|
||||||
|
left: w * 0.02,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.myCourt[1],
|
||||||
|
isOpponent: false,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.45,
|
||||||
|
left: w * 0.25,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.myCourt[2],
|
||||||
|
isOpponent: false,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.15,
|
||||||
|
left: w * 0.20,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.myCourt[3],
|
||||||
|
isOpponent: false,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.80,
|
||||||
|
left: w * 0.20,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.myCourt[4],
|
||||||
|
isOpponent: false,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.25,
|
||||||
|
right: w * 0.02,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.oppCourt[0],
|
||||||
|
isOpponent: true,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.68,
|
||||||
|
right: w * 0.02,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.oppCourt[1],
|
||||||
|
isOpponent: true,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.45,
|
||||||
|
right: w * 0.25,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.oppCourt[2],
|
||||||
|
isOpponent: true,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.15,
|
||||||
|
right: w * 0.20,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.oppCourt[3],
|
||||||
|
isOpponent: true,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: h * 0.80,
|
||||||
|
right: w * 0.20,
|
||||||
|
child: PlayerCourtCard(
|
||||||
|
controller: _controller,
|
||||||
|
playerId: _controller.oppCourt[4],
|
||||||
|
isOpponent: true,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!_controller.isSelectingShotLocation) ...[
|
||||||
|
_buildFloatingFoulBtn(
|
||||||
|
"FALTA +",
|
||||||
|
AppTheme.actionPoints,
|
||||||
|
"add_foul",
|
||||||
|
Icons.sports,
|
||||||
|
w * 0.39,
|
||||||
|
0.0,
|
||||||
|
h * 0.31,
|
||||||
|
sf,
|
||||||
|
),
|
||||||
|
_buildFloatingFoulBtn(
|
||||||
|
"FALTA -",
|
||||||
|
AppTheme.actionMiss,
|
||||||
|
"sub_foul",
|
||||||
|
Icons.block,
|
||||||
|
0.0,
|
||||||
|
w * 0.39,
|
||||||
|
h * 0.31,
|
||||||
|
sf,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!_controller.isSelectingShotLocation)
|
||||||
|
Positioned(
|
||||||
|
top: (h * 0.32) + (40 * sf),
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _handleTimerButton(context),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 68 * sf,
|
||||||
|
backgroundColor: Colors.grey.withOpacity(
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_controller.isRunning
|
||||||
|
? Icons.pause
|
||||||
|
: Icons.play_arrow,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 58 * sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: TopScoreboard(
|
||||||
|
controller: _controller,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_sessionId != null)
|
||||||
|
Positioned(
|
||||||
|
top: 90 * sf,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(child: _buildShareStatus(sf)),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (!_controller.isSelectingShotLocation)
|
||||||
|
Positioned(
|
||||||
|
bottom: -10 * sf,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: ActionButtonsPanel(
|
||||||
|
controller: _controller,
|
||||||
|
sf: sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (_controller.isSelectingShotLocation)
|
if (_controller.isSelectingShotLocation)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: h * 0.4, left: 0, right: 0,
|
top: h * 0.4,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf),
|
padding: EdgeInsets.symmetric(
|
||||||
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * sf)),
|
horizontal: 35 * sf,
|
||||||
child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL", style: TextStyle(color: Colors.white, fontSize: 22 * sf, fontWeight: FontWeight.bold)),
|
vertical: 18 * sf,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black87,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
11 * sf,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 1.5 * sf,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"TOQUE NO CAMPO PARA MARCAR O LOCAL",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22 * sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -229,30 +622,77 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 50 * sf, left: 12 * sf,
|
top: 50 * sf,
|
||||||
|
left: 12 * sf,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildCornerBtn(heroTag: 'btn_save_exit', icon: Icons.save_alt, color: AppTheme.oppTeamRed, size: cornerBtnSize, isLoading: _controller.isSaving, onTap: () async { await _controller.saveGameStats(context); if (context.mounted) Navigator.pop(context); }),
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_save_exit',
|
||||||
|
icon: Icons.save_alt,
|
||||||
|
color: AppTheme.oppTeamRed,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
isLoading: _controller.isSaving,
|
||||||
|
onTap: () async {
|
||||||
|
await _controller.saveGameStats(context);
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
SizedBox(height: 10 * sf),
|
SizedBox(height: 10 * sf),
|
||||||
_buildCornerBtn(heroTag: 'btn_history', icon: Icons.history, color: Colors.blueGrey, size: cornerBtnSize, onTap: () => showDialog(context: context, builder: (ctx) => PlayByPlayDialog(controller: _controller))),
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_history',
|
||||||
|
icon: Icons.history,
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) =>
|
||||||
|
PlayByPlayDialog(controller: _controller),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 50 * sf, right: 12 * sf,
|
top: 50 * sf,
|
||||||
|
right: 12 * sf,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildCornerBtn(heroTag: 'btn_heatmap', icon: Icons.local_fire_department, color: Colors.orange.shade800, size: cornerBtnSize, onTap: () => _showHeatmap(context)),
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_heatmap',
|
||||||
|
icon: Icons.local_fire_department,
|
||||||
|
color: Colors.orange.shade800,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
onTap: () => _showHeatmap(context),
|
||||||
|
),
|
||||||
SizedBox(height: 10 * sf),
|
SizedBox(height: 10 * sf),
|
||||||
_buildCornerBtn(heroTag: 'btn_boxscore', icon: Icons.table_chart, color: Colors.indigo, size: cornerBtnSize, onTap: () => showDialog(context: context, builder: (ctx) => BoxScoreDialog(controller: _controller, sf: sf))),
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_boxscore',
|
||||||
|
icon: Icons.table_chart,
|
||||||
|
color: Colors.indigo,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) =>
|
||||||
|
BoxScoreDialog(controller: _controller, sf: sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10 * sf),
|
||||||
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_share',
|
||||||
|
icon: Icons.share,
|
||||||
|
color: Colors.green,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
onTap: () => _openShareDialog(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// BOTÕES INFERIORES: SUBSTITUIÇÕES E TIMEOUTS
|
// BOTÕES INFERIORES: SUBSTITUIÇÕES E TIMEOUTS
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 55 * sf, left: 12 * sf,
|
bottom: 55 * sf,
|
||||||
|
left: 12 * sf,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -270,14 +710,23 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 12 * sf),
|
SizedBox(height: 12 * sf),
|
||||||
_buildCornerBtn(heroTag: 'btn_to_home', icon: Icons.timer, color: AppTheme.myTeamBlue, size: cornerBtnSize, onTap: _controller.myTimeoutsUsed >= 3 ? null : () => _controller.useTimeout(false)),
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_to_home',
|
||||||
|
icon: Icons.timer,
|
||||||
|
color: AppTheme.myTeamBlue,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
onTap: _controller.myTimeoutsUsed >= 3
|
||||||
|
? null
|
||||||
|
: () => _controller.useTimeout(false),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 55 * sf, right: 12 * sf,
|
bottom: 55 * sf,
|
||||||
|
right: 12 * sf,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -295,14 +744,29 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 12 * sf),
|
SizedBox(height: 12 * sf),
|
||||||
_buildCornerBtn(heroTag: 'btn_to_away', icon: Icons.timer, color: AppTheme.oppTeamRed, size: cornerBtnSize, onTap: _controller.opponentTimeoutsUsed >= 3 ? null : () => _controller.useTimeout(true)),
|
_buildCornerBtn(
|
||||||
|
heroTag: 'btn_to_away',
|
||||||
|
icon: Icons.timer,
|
||||||
|
color: AppTheme.oppTeamRed,
|
||||||
|
size: cornerBtnSize,
|
||||||
|
onTap: _controller.opponentTimeoutsUsed >= 3
|
||||||
|
? null
|
||||||
|
: () => _controller.useTimeout(true),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (_controller.isSaving)
|
if (_controller.isSaving)
|
||||||
Positioned.fill(child: Container(color: Colors.black.withOpacity(0.4), child: const Center(child: CircularProgressIndicator(color: Colors.white)))),
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -311,4 +775,4 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
||||||
import '../models/game_model.dart';
|
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 '../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';
|
||||||
@@ -281,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
|
||||||
@@ -318,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 {
|
||||||
@@ -337,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
@@ -33,13 +33,22 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _prefsKey(String key) {
|
||||||
|
final userId = supabase.auth.currentUser?.id ?? 'guest';
|
||||||
|
return '${key}_$userId';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadUserAvatar() async {
|
Future<void> _loadUserAvatar() async {
|
||||||
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) {
|
||||||
|
_uploadedImageUrl = savedUrl;
|
||||||
|
} else {
|
||||||
|
_uploadedImageUrl = null;
|
||||||
|
}
|
||||||
_isMemoryLoaded = true;
|
_isMemoryLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,7 +67,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final urlDoSupabase = data['avatar_url'];
|
final urlDoSupabase = data['avatar_url'];
|
||||||
|
|
||||||
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;
|
||||||
});
|
});
|
||||||
@@ -104,7 +113,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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(() {
|
||||||
@@ -354,13 +363,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR!
|
// 👇 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;
|
||||||
await prefs.remove('last_team_id');
|
if (userId != null) {
|
||||||
await prefs.remove('last_team_name');
|
await prefs.remove(_prefsKey('meu_avatar_guardado'));
|
||||||
await prefs.remove('last_team_logo');
|
await prefs.remove(_prefsKey('last_team_id'));
|
||||||
await prefs.remove('last_team_wins');
|
await prefs.remove(_prefsKey('last_team_name'));
|
||||||
await prefs.remove('last_team_losses');
|
await prefs.remove(_prefsKey('last_team_logo'));
|
||||||
await prefs.remove('last_team_draws');
|
await prefs.remove(_prefsKey('last_team_wins'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_losses'));
|
||||||
|
await prefs.remove(_prefsKey('last_team_draws'));
|
||||||
|
}
|
||||||
|
|
||||||
await Supabase.instance.client.auth.signOut();
|
await Supabase.instance.client.auth.signOut();
|
||||||
if (ctx.mounted) {
|
if (ctx.mounted) {
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
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';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class StatusPage extends StatefulWidget {
|
class StatusPage extends StatefulWidget {
|
||||||
const StatusPage({super.key});
|
final String? initialTeamId;
|
||||||
|
final String initialTeamName;
|
||||||
|
final String? initialTeamLogo;
|
||||||
|
|
||||||
|
const StatusPage({
|
||||||
|
super.key,
|
||||||
|
this.initialTeamId,
|
||||||
|
this.initialTeamName = "Selecionar Equipa",
|
||||||
|
this.initialTeamLogo,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatusPage> createState() => _StatusPageState();
|
State<StatusPage> createState() => _StatusPageState();
|
||||||
@@ -16,10 +25,10 @@ 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;
|
||||||
String? _selectedTeamLogo;
|
late String? _selectedTeamLogo;
|
||||||
|
|
||||||
String _sortColumn = 'pts';
|
String _sortColumn = 'pts';
|
||||||
bool _isAscending = false;
|
bool _isAscending = false;
|
||||||
@@ -27,34 +36,61 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadSelectedTeam();
|
_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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadSelectedTeam() async {
|
String _prefsKey(String key) {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final userId = _supabase.auth.currentUser?.id ?? 'guest';
|
||||||
final savedId = prefs.getString('last_team_id');
|
return '${key}_$userId';
|
||||||
|
}
|
||||||
if (savedId != null && mounted) {
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(StatusPage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// Quando a HomeScreen muda a equipa, a StatusPage atualiza automaticamente
|
||||||
|
if (widget.initialTeamId != oldWidget.initialTeamId) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedTeamId = savedId;
|
_selectedTeamId = widget.initialTeamId;
|
||||||
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
|
_selectedTeamName = widget.initialTeamName;
|
||||||
_selectedTeamLogo = prefs.getString('last_team_logo');
|
_selectedTeamLogo = widget.initialTeamLogo;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveSelectedTeam() async {
|
/// 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();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (_selectedTeamId != null) {
|
if (_selectedTeamId != null) {
|
||||||
await prefs.setString('last_team_id', _selectedTeamId!);
|
await prefs.setString(_prefsKey('last_team_id'), _selectedTeamId!);
|
||||||
await prefs.setString('last_team_name', _selectedTeamName);
|
await prefs.setString(_prefsKey('last_team_name'), _selectedTeamName);
|
||||||
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
||||||
await prefs.setString('last_team_logo', _selectedTeamLogo!);
|
await prefs.setString(_prefsKey('last_team_logo'), _selectedTeamLogo!);
|
||||||
} else {
|
} else {
|
||||||
await prefs.remove('last_team_logo');
|
await prefs.remove(_prefsKey('last_team_logo'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Também guarda no Supabase
|
||||||
final userId = _supabase.auth.currentUser?.id;
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
if (userId != null && _selectedTeamId != null) {
|
if (userId != null && _selectedTeamId != null) {
|
||||||
try {
|
try {
|
||||||
@@ -82,10 +118,12 @@ 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,
|
||||||
@@ -98,14 +136,23 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
width: 24 * context.sf,
|
width: 24 * context.sf,
|
||||||
height: 24 * context.sf,
|
height: 24 * context.sf,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
placeholder: (context, url) => Icon(Icons.shield,
|
||||||
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
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),
|
: Icon(Icons.shield,
|
||||||
|
color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
SizedBox(width: 10 * context.sf),
|
SizedBox(width: 10 * context.sf),
|
||||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
Text(_selectedTeamName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)),
|
||||||
]),
|
]),
|
||||||
Icon(Icons.arrow_drop_down, color: textColor),
|
Icon(Icons.arrow_drop_down, color: textColor),
|
||||||
],
|
],
|
||||||
@@ -116,106 +163,204 @@ 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);
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
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();
|
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, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES!
|
color: Colors.transparent,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã
|
physics: const ClampingScrollPhysics(),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
constraints:
|
||||||
|
BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
child: DataTable(
|
child: DataTable(
|
||||||
columnSpacing: 20 * context.sf,
|
columnSpacing: 20 * context.sf,
|
||||||
horizontalMargin: 16 * context.sf,
|
horizontalMargin: 16 * context.sf,
|
||||||
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
headingRowColor: WidgetStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.surface),
|
||||||
dataRowMaxHeight: 60 * context.sf,
|
dataRowMaxHeight: 60 * context.sf,
|
||||||
dataRowMinHeight: 60 * context.sf,
|
dataRowMinHeight: 60 * context.sf,
|
||||||
columns: [
|
columns: [
|
||||||
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
|
DataColumn(
|
||||||
|
label: Text('JOGADOR',
|
||||||
|
style: TextStyle(color: textColor))),
|
||||||
_buildSortableColumn(context, 'J', 'j', textColor),
|
_buildSortableColumn(context, 'J', 'j', textColor),
|
||||||
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
||||||
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
||||||
@@ -227,53 +372,83 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
],
|
],
|
||||||
rows: [
|
rows: [
|
||||||
...players.map((player) => DataRow(cells: [
|
...players.map((player) => DataRow(cells: [
|
||||||
DataCell(
|
DataCell(
|
||||||
Row(
|
Row(children: [
|
||||||
children: [
|
ClipOval(
|
||||||
ClipOval(
|
child: Container(
|
||||||
child: Container(
|
width: 30 * context.sf,
|
||||||
width: 30 * context.sf,
|
height: 30 * context.sf,
|
||||||
height: 30 * context.sf,
|
color: Colors.grey.withOpacity(0.2),
|
||||||
color: Colors.grey.withOpacity(0.2),
|
child: (player['image_url'] != null &&
|
||||||
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
|
player['image_url']
|
||||||
? CachedNetworkImage(
|
.toString()
|
||||||
imageUrl: player['image_url'],
|
.isNotEmpty)
|
||||||
fit: BoxFit.cover,
|
? CachedNetworkImage(
|
||||||
fadeInDuration: Duration.zero,
|
imageUrl: player['image_url'],
|
||||||
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
fit: BoxFit.cover,
|
||||||
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
fadeInDuration: Duration.zero,
|
||||||
)
|
placeholder: (context, url) => Icon(
|
||||||
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
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),
|
||||||
SizedBox(width: 10 * context.sf),
|
Text(player['name'],
|
||||||
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
|
style: TextStyle(
|
||||||
]
|
fontWeight: FontWeight.bold,
|
||||||
)
|
fontSize: 13 * context.sf,
|
||||||
),
|
color: textColor)),
|
||||||
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
]),
|
||||||
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
),
|
||||||
_buildStatCell(context, player['ast'], textColor),
|
DataCell(Center(
|
||||||
_buildStatCell(context, player['rbs'], textColor),
|
child: Text(player['j'].toString(),
|
||||||
_buildStatCell(context, player['stl'], textColor),
|
style: TextStyle(color: textColor)))),
|
||||||
_buildStatCell(context, player['blk'], textColor),
|
_buildStatCell(context, player['pts'], textColor,
|
||||||
_buildStatCell(context, player['def'], textColor, isBlue: true),
|
isHighlight: true),
|
||||||
_buildStatCell(context, player['mvp'], textColor, isGold: 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(
|
DataRow(
|
||||||
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
color: WidgetStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
||||||
cells: [
|
cells: [
|
||||||
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
|
DataCell(Text('TOTAL EQUIPA',
|
||||||
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
|
style: TextStyle(
|
||||||
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
|
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['ast'], textColor),
|
||||||
_buildStatCell(context, teamTotals['rbs'], textColor),
|
_buildStatCell(context, teamTotals['rbs'], textColor),
|
||||||
_buildStatCell(context, teamTotals['stl'], textColor),
|
_buildStatCell(context, teamTotals['stl'], textColor),
|
||||||
_buildStatCell(context, teamTotals['blk'], textColor),
|
_buildStatCell(context, teamTotals['blk'], textColor),
|
||||||
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
|
_buildStatCell(context, teamTotals['def'], textColor,
|
||||||
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
|
isBlue: true),
|
||||||
]
|
_buildStatCell(context, teamTotals['mvp'], textColor,
|
||||||
)
|
isGold: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -282,70 +457,123 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
stream: _teamController.teamsStream,
|
||||||
final team = teams[i];
|
builder: (context, snapshot) {
|
||||||
final logoUrl = team['image_url'];
|
final teams = snapshot.data ?? [];
|
||||||
|
return ListView.builder(
|
||||||
return ListTile(
|
itemCount: teams.length,
|
||||||
leading: ClipOval(
|
itemBuilder: (context, i) {
|
||||||
child: Container(
|
final team = teams[i];
|
||||||
width: 36 * context.sf,
|
final logoUrl = team['image_url'];
|
||||||
height: 36 * context.sf,
|
|
||||||
color: AppTheme.primaryRed.withOpacity(0.1),
|
return ListTile(
|
||||||
child: (logoUrl != null && logoUrl.isNotEmpty)
|
leading: ClipOval(
|
||||||
? CachedNetworkImage(
|
child: Container(
|
||||||
imageUrl: logoUrl,
|
width: 36 * context.sf,
|
||||||
fit: BoxFit.cover,
|
height: 36 * context.sf,
|
||||||
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
color: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
)
|
? CachedNetworkImage(
|
||||||
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
imageUrl: logoUrl,
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
),
|
placeholder: (context, url) => Icon(Icons.shield,
|
||||||
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
color: AppTheme.primaryRed,
|
||||||
onTap: () async {
|
size: 20 * context.sf),
|
||||||
setState(() {
|
errorWidget: (context, url, error) => Icon(
|
||||||
_selectedTeamId = team['id'].toString();
|
Icons.shield,
|
||||||
_selectedTeamName = team['name'];
|
color: AppTheme.primaryRed,
|
||||||
_selectedTeamLogo = logoUrl;
|
size: 20 * context.sf),
|
||||||
});
|
)
|
||||||
|
: Icon(Icons.shield,
|
||||||
await _saveSelectedTeam();
|
color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
),
|
||||||
if (context.mounted) Navigator.pop(context);
|
),
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
},
|
),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
239
lib/widgets/share_game_dialog.dart
Normal file
239
lib/widgets/share_game_dialog.dart
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:playmaker/controllers/game_sharing_controller.dart';
|
||||||
|
|
||||||
|
class ShareGameDialog extends StatefulWidget {
|
||||||
|
final String gameId;
|
||||||
|
final GameSharingController controller;
|
||||||
|
final String? activeSessionId;
|
||||||
|
final String? activeShareCode;
|
||||||
|
|
||||||
|
const ShareGameDialog({
|
||||||
|
super.key,
|
||||||
|
required this.gameId,
|
||||||
|
required this.controller,
|
||||||
|
this.activeSessionId,
|
||||||
|
this.activeShareCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShareGameDialog> createState() => _ShareGameDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareGameDialogState extends State<ShareGameDialog> {
|
||||||
|
String? _shareCode;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_shareCode = widget.activeShareCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createSession() async {
|
||||||
|
setState(() {
|
||||||
|
_error = null;
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final code = await widget.controller.createShareSession(widget.gameId);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
if (code == null) {
|
||||||
|
_error = 'Erro ao criar sessão. Tenta novamente.';
|
||||||
|
} else {
|
||||||
|
_shareCode = code;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_shareCode != null && widget.activeSessionId == null) {
|
||||||
|
final session = await widget.controller.getActiveSessionForGame(
|
||||||
|
widget.gameId,
|
||||||
|
);
|
||||||
|
if (session != null && mounted) {
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop({'session_id': session['id'], 'share_code': _shareCode});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
title: Text(
|
||||||
|
'Partilhar Jogo',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_shareCode != null) ...[
|
||||||
|
Text(
|
||||||
|
'Código de partilha:',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SelectableText(
|
||||||
|
_shareCode!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Envie este código ao seu parceiro e peça que ele entre no jogo.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Text(
|
||||||
|
'Crie um código e partilhe o jogo com outro utilizador.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (_error != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Fechar'),
|
||||||
|
),
|
||||||
|
if (_shareCode == null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _createSession,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Criar código'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop({
|
||||||
|
'session_id': widget.activeSessionId,
|
||||||
|
'share_code': _shareCode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JoinGameDialog extends StatefulWidget {
|
||||||
|
final GameSharingController controller;
|
||||||
|
|
||||||
|
const JoinGameDialog({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<JoinGameDialog> createState() => _JoinGameDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _JoinGameDialogState extends State<JoinGameDialog> {
|
||||||
|
final TextEditingController _codeController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
Future<void> _joinSession() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await widget.controller.joinGameByCode(
|
||||||
|
_codeController.text.trim(),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Código inválido ou sessão não disponível.';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
title: Text(
|
||||||
|
'Entrar em Jogo Partilhado',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Digite o código enviado pelo outro utilizador.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _codeController,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Código',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'ABC123',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_error != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _joinSession,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Entrar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
@@ -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:
|
||||||
@@ -516,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:
|
||||||
@@ -953,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:
|
||||||
|
|||||||
Reference in New Issue
Block a user