Compare commits
29 Commits
dev
...
60656d77e8
| Author | SHA1 | Date | |
|---|---|---|---|
| 60656d77e8 | |||
| c3a90f2816 | |||
| 648fae99b1 | |||
| 6d51c3f56e | |||
| 309a2f98bc | |||
| 4075c56d44 | |||
| 59927521f1 | |||
| e1d1babb07 | |||
| bc93bb748f | |||
| cc753b395d | |||
| ce25fe6499 | |||
| 4f2a220cd6 | |||
| fb85566e3f | |||
| 2544e52636 | |||
| 1b08ed7d07 | |||
| c6255759c5 | |||
| 9cf7915d12 | |||
| be103c66b0 | |||
| 00fee30792 | |||
| 6c89b7ab8c | |||
| 8adea3f7b6 | |||
| b77ae2eac6 | |||
| ed4cff34f6 | |||
| 2a987e517b | |||
| ec5bdc4867 | |||
| a4ef651d64 | |||
| cf0a9a9890 | |||
| c2619fe6d6 | |||
| 3dbccdc823 |
25
.metadata
@@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
|
revision: "b45fa18946ecc2d9b4009952c636ba7e2ffbb787"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@@ -13,26 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
|
||||||
- platform: android
|
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
- platform: ios
|
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
- platform: linux
|
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
- platform: macos
|
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
- platform: web
|
- platform: web
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
|
||||||
- platform: windows
|
|
||||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ android {
|
|||||||
namespace = "com.example.playmaker"
|
namespace = "com.example.playmaker"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
//ndkVersion = flutter.ndkVersion
|
//ndkVersion = flutter.ndkVersion
|
||||||
ndkVersion = "27.0.12077973"
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
android:label="playmaker"
|
android:label="playmaker"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.yalantis.ucrop.UCropActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
BIN
assets/assit.png
Normal file
|
After Width: | Height: | Size: 607 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 1.8 MiB |
BIN
assets/stl.png
Normal file
|
After Width: | Height: | Size: 554 KiB |
BIN
assets/tov.png
Normal file
|
After Width: | Height: | Size: 630 KiB |
@@ -45,5 +45,12 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>A PlayMaker precisa de aceder à tua galeria para poderes escolher uma foto de perfil.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>A PlayMaker precisa de aceder à câmara para poderes tirar uma foto de perfil.</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
class CalibradorPage extends StatefulWidget {
|
|
||||||
const CalibradorPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CalibradorPage> createState() => _CalibradorPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CalibradorPageState extends State<CalibradorPage> {
|
|
||||||
// --- 👇 VALORES INICIAIS 👇 ---
|
|
||||||
double hoopBaseX = 0.08;
|
|
||||||
double arcRadius = 0.28;
|
|
||||||
double cornerY = 0.40;
|
|
||||||
// -----------------------------------------------------
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
SystemChrome.setPreferredOrientations([
|
|
||||||
DeviceOrientation.landscapeRight,
|
|
||||||
DeviceOrientation.landscapeLeft,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
|
||||||
final double hScreen = MediaQuery.of(context).size.height;
|
|
||||||
|
|
||||||
// O MESMO CÁLCULO EXATO DO PLACAR
|
|
||||||
final double sf = math.min(wScreen / 1150, hScreen / 720);
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFF266174),
|
|
||||||
body: SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
// 👇 1. O CAMPO COM AS MARGENS EXATAS DO PLACAR 👇
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.white, width: 2.5),
|
|
||||||
image: const DecorationImage(
|
|
||||||
image: AssetImage('assets/campo.png'),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
return CustomPaint(
|
|
||||||
painter: LinePainter(
|
|
||||||
hoopBaseX: hoopBaseX,
|
|
||||||
arcRadius: arcRadius,
|
|
||||||
cornerY: cornerY,
|
|
||||||
color: Colors.redAccent,
|
|
||||||
width: constraints.maxWidth,
|
|
||||||
height: constraints.maxHeight,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 👇 2. TOPO: MOSTRADORES DE VALORES COM FITTEDBOX (Não transborda) 👇
|
|
||||||
Positioned(
|
|
||||||
top: 0, left: 0, right: 0,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black87.withOpacity(0.8),
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 5 * sf, horizontal: 15 * sf),
|
|
||||||
child: FittedBox( // Isto impede o ecrã de dar o erro dos 179 pixels!
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildValueDisplay("Aro X", hoopBaseX, sf),
|
|
||||||
SizedBox(width: 20 * sf),
|
|
||||||
_buildValueDisplay("Raio", arcRadius, sf),
|
|
||||||
SizedBox(width: 20 * sf),
|
|
||||||
_buildValueDisplay("Canto", cornerY, sf),
|
|
||||||
SizedBox(width: 30 * sf),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
icon: Icon(Icons.check, size: 18 * sf),
|
|
||||||
label: Text("FECHAR", style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold)),
|
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 👇 3. FUNDO: SLIDERS (Com altura fixa para não dar o erro "hasSize") 👇
|
|
||||||
Positioned(
|
|
||||||
bottom: 0, left: 0, right: 0,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black87.withOpacity(0.8),
|
|
||||||
height: 80 * sf, // Altura segura para os sliders
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildSlider("Pos. do Aro", hoopBaseX, 0.0, 0.25, (val) => setState(() => hoopBaseX = val), sf)),
|
|
||||||
Expanded(child: _buildSlider("Tam. da Curva", arcRadius, 0.1, 0.5, (val) => setState(() => arcRadius = val), sf)),
|
|
||||||
Expanded(child: _buildSlider("Pos. do Canto", cornerY, 0.2, 0.5, (val) => setState(() => cornerY = val), sf)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildValueDisplay(String label, double value, double sf) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Text("$label: ", style: TextStyle(color: Colors.white70, fontSize: 16 * sf)),
|
|
||||||
Text(value.toStringAsFixed(3), style: TextStyle(color: Colors.yellow, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSlider(String label, double value, double min, double max, ValueChanged<double> onChanged, double sf) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(label, style: TextStyle(color: Colors.white, fontSize: 12 * sf)),
|
|
||||||
SizedBox(
|
|
||||||
height: 40 * sf, // Altura exata para o Slider não crashar
|
|
||||||
child: Slider(
|
|
||||||
value: value, min: min, max: max,
|
|
||||||
activeColor: Colors.yellow, inactiveColor: Colors.white24,
|
|
||||||
onChanged: onChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==============================================================
|
|
||||||
// 📐 PINTOR: DESENHA A LINHA MATEMÁTICA NA TELA
|
|
||||||
// ==============================================================
|
|
||||||
class LinePainter extends CustomPainter {
|
|
||||||
final double hoopBaseX;
|
|
||||||
final double arcRadius;
|
|
||||||
final double cornerY;
|
|
||||||
final Color color;
|
|
||||||
final double width;
|
|
||||||
final double height;
|
|
||||||
|
|
||||||
LinePainter({
|
|
||||||
required this.hoopBaseX, required this.arcRadius, required this.cornerY,
|
|
||||||
required this.color, required this.width, required this.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = color
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeWidth = 4;
|
|
||||||
|
|
||||||
double aspectRatio = width / height;
|
|
||||||
double hoopY = 0.50 * height;
|
|
||||||
|
|
||||||
// O cornerY controla a que distância do meio (50%) estão as linhas retas
|
|
||||||
double cornerDistY = cornerY * height;
|
|
||||||
|
|
||||||
// --- CESTO ESQUERDO ---
|
|
||||||
double hoopLX = hoopBaseX * width;
|
|
||||||
|
|
||||||
canvas.drawLine(Offset(0, hoopY - cornerDistY), Offset(width * 0.35, hoopY - cornerDistY), paint); // Cima
|
|
||||||
canvas.drawLine(Offset(0, hoopY + cornerDistY), Offset(width * 0.35, hoopY + cornerDistY), paint); // Baixo
|
|
||||||
|
|
||||||
canvas.drawArc(
|
|
||||||
Rect.fromCenter(center: Offset(hoopLX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2),
|
|
||||||
-math.pi / 2, math.pi, false, paint,
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- CESTO DIREITO ---
|
|
||||||
double hoopRX = (1.0 - hoopBaseX) * width;
|
|
||||||
|
|
||||||
canvas.drawLine(Offset(width, hoopY - cornerDistY), Offset(width * 0.65, hoopY - cornerDistY), paint); // Cima
|
|
||||||
canvas.drawLine(Offset(width, hoopY + cornerDistY), Offset(width * 0.65, hoopY + cornerDistY), paint); // Baixo
|
|
||||||
|
|
||||||
canvas.drawArc(
|
|
||||||
Rect.fromCenter(center: Offset(hoopRX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2),
|
|
||||||
math.pi / 2, math.pi, false, paint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant LinePainter oldDelegate) {
|
|
||||||
return oldDelegate.hoopBaseX != hoopBaseX || oldDelegate.arcRadius != arcRadius || oldDelegate.cornerY != cornerY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
115
lib/classe/theme.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
static const Color primaryRed = Color(0xFFE74C3C);
|
||||||
|
static const Color backgroundLight = Color(0xFFF5F7FA);
|
||||||
|
static const Color surfaceWhite = Colors.white;
|
||||||
|
static const Color successGreen = Color(0xFF00C853);
|
||||||
|
static const Color warningAmber = Colors.amber;
|
||||||
|
|
||||||
|
static const Color placarBackground = Color(0xFF266174);
|
||||||
|
static const Color placarDarkSurface = Color(0xFF16202C);
|
||||||
|
static const Color placarTimerBg = Color(0xFF2C3E50);
|
||||||
|
static const Color placarListCard = Color(0xFF263238);
|
||||||
|
|
||||||
|
static const Color myTeamBlue = Color(0xFF1E5BB2);
|
||||||
|
static const Color oppTeamRed = Color(0xFFD92C2C);
|
||||||
|
|
||||||
|
static const Color actionPoints = Colors.orange;
|
||||||
|
static const Color actionMiss = Colors.redAccent;
|
||||||
|
static const Color actionSteal = Colors.green;
|
||||||
|
static const Color actionAssist = Colors.blueGrey;
|
||||||
|
static const Color actionRebound = Color(0xFF1E2A38);
|
||||||
|
static const Color actionBlock = Colors.deepPurple;
|
||||||
|
|
||||||
|
static const Color statPtsBg = Color(0xFF1565C0);
|
||||||
|
static const Color statAstBg = Color(0xFF2E7D32);
|
||||||
|
static const Color statRebBg = Color(0xFF6A1B9A);
|
||||||
|
static const Color statPieBg = Color.fromARGB(255, 22, 32, 44);
|
||||||
|
static const Color coachBg = Color(0xFFFFF9C4);
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// ☀️ TEMA CLARO
|
||||||
|
// =========================================================
|
||||||
|
static ThemeData get lightTheme {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: primaryRed,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primary: primaryRed,
|
||||||
|
surface: backgroundLight,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: backgroundLight,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 👇 CORRETO: Classe CardThemeData
|
||||||
|
cardTheme: const CardThemeData(
|
||||||
|
color: surfaceWhite,
|
||||||
|
surfaceTintColor: Colors.transparent, // Evita o tom rosado do Material 3
|
||||||
|
elevation: 3.0,
|
||||||
|
margin: EdgeInsets.only(bottom: 12.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||||
|
side: BorderSide(color: Color(0xFFEEEEEE), width: 1.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: surfaceWhite,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 🌙 MODO ESCURO
|
||||||
|
// =========================================================
|
||||||
|
static ThemeData get darkTheme {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: primaryRed,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primary: primaryRed,
|
||||||
|
surface: const Color(0xFF1E1E1E),
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Color(0xFF121212),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 👇 CORRETO: Classe CardThemeData
|
||||||
|
cardTheme: const CardThemeData(
|
||||||
|
color: Color(0xFF1E1E1E),
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 3.0,
|
||||||
|
margin: EdgeInsets.only(bottom: 12.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||||
|
side: BorderSide(color: Color(0xFF2C2C2C), width: 1.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFF1E1E1E),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF2C2C2C)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
lib/controllers/active_team.dart
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
class ActiveTeam {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String? logo;
|
||||||
|
final int wins;
|
||||||
|
final int losses;
|
||||||
|
final int draws;
|
||||||
|
|
||||||
|
ActiveTeam({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.logo,
|
||||||
|
this.wins = 0,
|
||||||
|
this.losses = 0,
|
||||||
|
this.draws = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🟢 A MÁGICA: Esta variável avisa a Home e a StatusPage ao mesmo tempo quando a equipa muda!
|
||||||
|
final ValueNotifier<ActiveTeam?> globalActiveTeam = ValueNotifier(null);
|
||||||
|
|
||||||
|
// 🟢 FUNÇÃO PARA CARREGAR A EQUIPA AO ABRIR A APP (Lê da Memória e do Supabase)
|
||||||
|
Future<void> loadGlobalTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
// 1. Carrega rápido da memória (para não piscar o ecrã)
|
||||||
|
if (savedId != null) {
|
||||||
|
globalActiveTeam.value = ActiveTeam(
|
||||||
|
id: savedId,
|
||||||
|
name: prefs.getString('last_team_name') ?? "Selecionar Equipa",
|
||||||
|
logo: prefs.getString('last_team_logo'),
|
||||||
|
wins: prefs.getInt('last_team_wins') ?? 0,
|
||||||
|
losses: prefs.getInt('last_team_losses') ?? 0,
|
||||||
|
draws: prefs.getInt('last_team_draws') ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Vai confirmar no Supabase se entraste com esta conta noutro telemóvel!
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profile = await supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
|
||||||
|
if (profile != null && profile['selected_team_id'] != null) {
|
||||||
|
final dbTeamId = profile['selected_team_id'].toString();
|
||||||
|
final teamData = await supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
|
||||||
|
|
||||||
|
if (teamData != null) {
|
||||||
|
final newTeam = ActiveTeam(
|
||||||
|
id: teamData['id'].toString(),
|
||||||
|
name: teamData['name'] ?? 'Desconhecido',
|
||||||
|
logo: teamData['image_url'],
|
||||||
|
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
|
||||||
|
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
|
||||||
|
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
|
||||||
|
);
|
||||||
|
globalActiveTeam.value = newTeam;
|
||||||
|
|
||||||
|
// Atualiza a memória do telemóvel para a próxima vez ser rápido
|
||||||
|
await prefs.setString('last_team_id', newTeam.id);
|
||||||
|
await prefs.setString('last_team_name', newTeam.name);
|
||||||
|
if (newTeam.logo != null && newTeam.logo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', newTeam.logo!);
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', newTeam.wins);
|
||||||
|
await prefs.setInt('last_team_losses', newTeam.losses);
|
||||||
|
await prefs.setInt('last_team_draws', newTeam.draws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🟢 FUNÇÃO PARA GUARDAR A EQUIPA (Na Memória e no Supabase)
|
||||||
|
Future<void> saveGlobalTeam(ActiveTeam team) async {
|
||||||
|
globalActiveTeam.value = team; // Atualiza a app inteira!
|
||||||
|
|
||||||
|
// 1. Guarda no telemóvel
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString('last_team_id', team.id);
|
||||||
|
await prefs.setString('last_team_name', team.name);
|
||||||
|
if (team.logo != null && team.logo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', team.logo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', team.wins);
|
||||||
|
await prefs.setInt('last_team_losses', team.losses);
|
||||||
|
await prefs.setInt('last_team_draws', team.draws);
|
||||||
|
|
||||||
|
// 2. Guarda no Supabase!
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null) {
|
||||||
|
try {
|
||||||
|
await supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': team.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,37 +4,57 @@ import '../models/game_model.dart';
|
|||||||
class GameController {
|
class GameController {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// 1. LER JOGOS (Com Filtros Opcionais)
|
String get myUserId => _supabase.auth.currentUser?.id ?? '';
|
||||||
Stream<List<Game>> getFilteredGames({String? teamFilter, String? seasonFilter}) {
|
|
||||||
|
// LER JOGOS
|
||||||
|
Stream<List<Game>> get gamesStream {
|
||||||
return _supabase
|
return _supabase
|
||||||
.from('games')
|
.from('games')
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
|
.eq('user_id', myUserId)
|
||||||
.asyncMap((event) async {
|
.asyncMap((event) async {
|
||||||
|
final data = await _supabase
|
||||||
|
.from('games')
|
||||||
|
.select()
|
||||||
|
.eq('user_id', myUserId)
|
||||||
|
.order('game_date', ascending: false);
|
||||||
|
|
||||||
// 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games'
|
// O Game.fromMap agora faz o trabalho sujo todo!
|
||||||
var query = _supabase.from('games').select();
|
return data.map((json) => Game.fromMap(json)).toList();
|
||||||
|
|
||||||
// Aplica o filtro de Temporada
|
|
||||||
if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') {
|
|
||||||
query = query.eq('season', seasonFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplica o filtro de Equipa (Procura em casa ou fora)
|
|
||||||
if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') {
|
|
||||||
query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executa a query com a ordenação por data
|
|
||||||
final viewData = await query.order('game_date', ascending: false);
|
|
||||||
|
|
||||||
return viewData.map((json) => Game.fromMap(json)).toList();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CRIAR JOGO
|
// LER JOGOS COM FILTROS
|
||||||
|
Stream<List<Game>> getFilteredGames({required String teamFilter, required String seasonFilter}) {
|
||||||
|
return _supabase
|
||||||
|
.from('games')
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.eq('user_id', myUserId)
|
||||||
|
.asyncMap((event) async {
|
||||||
|
|
||||||
|
var query = _supabase.from('games').select().eq('user_id', myUserId);
|
||||||
|
|
||||||
|
if (seasonFilter != 'Todas') {
|
||||||
|
query = query.eq('season', seasonFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = await query.order('game_date', ascending: false);
|
||||||
|
|
||||||
|
List<Game> games = data.map((json) => Game.fromMap(json)).toList();
|
||||||
|
|
||||||
|
if (teamFilter != 'Todas') {
|
||||||
|
games = games.where((g) => g.myTeam == teamFilter || g.opponentTeam == teamFilter).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return games;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRIAR JOGO
|
||||||
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
||||||
try {
|
try {
|
||||||
final response = await _supabase.from('games').insert({
|
final response = await _supabase.from('games').insert({
|
||||||
|
'user_id': myUserId,
|
||||||
'my_team': myTeam,
|
'my_team': myTeam,
|
||||||
'opponent_team': opponent,
|
'opponent_team': opponent,
|
||||||
'season': season,
|
'season': season,
|
||||||
@@ -42,14 +62,36 @@ class GameController {
|
|||||||
'opponent_score': 0,
|
'opponent_score': 0,
|
||||||
'status': 'Decorrer',
|
'status': 'Decorrer',
|
||||||
'game_date': DateTime.now().toIso8601String(),
|
'game_date': DateTime.now().toIso8601String(),
|
||||||
|
// 👇 Preenchemos logo com os valores iniciais da tua Base de Dados
|
||||||
|
'remaining_seconds': 600, // Assume 10 minutos (600s)
|
||||||
|
'my_timeouts': 0,
|
||||||
|
'opp_timeouts': 0,
|
||||||
|
'current_quarter': 1,
|
||||||
|
'top_pts_name': '---',
|
||||||
|
'top_ast_name': '---',
|
||||||
|
'top_rbs_name': '---',
|
||||||
|
'top_def_name': '---',
|
||||||
|
'mvp_name': '---',
|
||||||
}).select().single();
|
}).select().single();
|
||||||
|
|
||||||
return response['id'];
|
return response['id']?.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erro ao criar jogo: $e");
|
print("Erro ao criar jogo: $e");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// ELIMINAR JOGO
|
||||||
|
Future<bool> deleteGame(String gameId) async {
|
||||||
|
try {
|
||||||
|
await _supabase.from('games').delete().eq('id', gameId);
|
||||||
|
// Como o Supabase tem Cascade Delete (se configurado), vai apagar também
|
||||||
|
// as stats e shot_locations associadas a este game_id automaticamente.
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print("Erro ao eliminar jogo: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class HomeController extends ChangeNotifier {
|
class HomeController extends ChangeNotifier {
|
||||||
// Se precisar de estado para a home screen
|
|
||||||
int _selectedCardIndex = 0;
|
int _selectedCardIndex = 0;
|
||||||
|
|
||||||
int get selectedCardIndex => _selectedCardIndex;
|
int get selectedCardIndex => _selectedCardIndex;
|
||||||
@@ -11,10 +10,8 @@ class HomeController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Métodos adicionais para lógica da home
|
|
||||||
void navigateToDetails(String playerName) {
|
void navigateToDetails(String playerName) {
|
||||||
print('Navegando para detalhes de $playerName');
|
print('Navegando para detalhes de $playerName');
|
||||||
// Implementar navegação
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshData() {
|
void refreshData() {
|
||||||
|
|||||||
@@ -1,50 +1,68 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
class TeamController {
|
class TeamController {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// 1. Variável fixa para guardar o Stream principal
|
// 1. STREAM (Realtime)
|
||||||
late final Stream<List<Map<String, dynamic>>> teamsStream;
|
Stream<List<Map<String, dynamic>>> get teamsStream {
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
|
||||||
// 2. Dicionário (Cache) para não recriar Streams de contagem repetidos
|
if (userId == null) return const Stream.empty();
|
||||||
final Map<String, Stream<int>> _playerCountStreams = {};
|
|
||||||
|
|
||||||
TeamController() {
|
return _supabase
|
||||||
// INICIALIZAÇÃO: O stream é criado APENAS UMA VEZ quando abres a página!
|
|
||||||
teamsStream = _supabase
|
|
||||||
.from('teams')
|
.from('teams')
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
.order('name', ascending: true)
|
.eq('user_id', userId); // ✅ Bem feito, este já estava certo!
|
||||||
.map((data) => List<Map<String, dynamic>>.from(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRIAR
|
// 2. CRIAR (Agora guarda o dono da equipa!)
|
||||||
Future<void> createTeam(String name, String season, String? imageUrl) async {
|
Future<void> createTeam(String name, String season, File? imageFile) async {
|
||||||
try {
|
try {
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) throw Exception("Utilizador não autenticado.");
|
||||||
|
|
||||||
|
String? uploadedImageUrl;
|
||||||
|
|
||||||
|
// Se o utilizador escolheu uma imagem, fazemos o upload primeiro
|
||||||
|
if (imageFile != null) {
|
||||||
|
final fileName = '${userId}_${DateTime.now().millisecondsSinceEpoch}.png';
|
||||||
|
final storagePath = 'teams/$fileName';
|
||||||
|
|
||||||
|
await _supabase.storage.from('avatars').upload(
|
||||||
|
storagePath,
|
||||||
|
imageFile,
|
||||||
|
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||||
|
);
|
||||||
|
|
||||||
|
uploadedImageUrl = _supabase.storage.from('avatars').getPublicUrl(storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agora insere a equipa na base de dados com o ID DO DONO!
|
||||||
await _supabase.from('teams').insert({
|
await _supabase.from('teams').insert({
|
||||||
|
'user_id': userId, // 👈 CRUCIAL: Diz à base de dados de quem é esta equipa!
|
||||||
'name': name,
|
'name': name,
|
||||||
'season': season,
|
'season': season,
|
||||||
'image_url': imageUrl,
|
'image_url': uploadedImageUrl ?? '',
|
||||||
'is_favorite': false,
|
'is_favorite': false,
|
||||||
});
|
});
|
||||||
print("✅ Equipa guardada no Supabase!");
|
print("✅ Equipa guardada no Supabase com dono associado!");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("❌ Erro ao criar: $e");
|
print("❌ Erro ao criar equipa: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ELIMINAR
|
// 3. ELIMINAR
|
||||||
Future<void> deleteTeam(String id) async {
|
Future<void> deleteTeam(String id) async {
|
||||||
try {
|
try {
|
||||||
|
// Como segurança extra, podemos garantir que só apaga se for o dono (opcional se tiveres RLS no Supabase)
|
||||||
await _supabase.from('teams').delete().eq('id', id);
|
await _supabase.from('teams').delete().eq('id', id);
|
||||||
// Limpa o cache deste teamId se a equipa for apagada
|
|
||||||
_playerCountStreams.remove(id);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("❌ Erro ao eliminar: $e");
|
print("❌ Erro ao eliminar: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FAVORITAR
|
// 4. FAVORITAR
|
||||||
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
||||||
try {
|
try {
|
||||||
await _supabase
|
await _supabase
|
||||||
@@ -56,27 +74,29 @@ class TeamController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONTAR JOGADORES (AGORA COM CACHE DE MEMÓRIA!)
|
// 5. CONTAR JOGADORES (LEITURA ÚNICA)
|
||||||
Stream<int> getPlayerCountStream(String teamId) {
|
Future<int> getPlayerCount(String teamId) async {
|
||||||
// Se já criámos um "Tubo de ligação" para esta equipa, REUTILIZA-O!
|
try {
|
||||||
if (_playerCountStreams.containsKey(teamId)) {
|
final count = await _supabase.from('members').count().eq('team_id', teamId);
|
||||||
return _playerCountStreams[teamId]!;
|
return count;
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se é a primeira vez que pede esta equipa, cria a ligação e guarda na memória
|
// 6. VIEW DAS EQUIPAS (AQUI ESTAVA O TEU ERRO DE LISTAGEM!)
|
||||||
final newStream = _supabase
|
Future<List<Map<String, dynamic>>> getTeamsWithStats() async {
|
||||||
.from('members')
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
.stream(primaryKey: ['id'])
|
if (userId == null) return []; // Retorna lista vazia se não houver login
|
||||||
.eq('team_id', teamId)
|
|
||||||
.map((data) => data.length);
|
|
||||||
|
|
||||||
_playerCountStreams[teamId] = newStream; // Guarda no dicionário
|
final data = await _supabase
|
||||||
return newStream;
|
.from('teams_with_stats')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId) // 👈 CRUCIAL: Só puxa as estatísticas das tuas equipas!
|
||||||
|
.order('name', ascending: true);
|
||||||
|
|
||||||
|
return List<Map<String, dynamic>>.from(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LIMPEZA FINAL QUANDO SAÍMOS DA PÁGINA
|
void dispose() {}
|
||||||
void dispose() {
|
|
||||||
// Limpamos o dicionário de streams para libertar memória RAM
|
|
||||||
_playerCountStreams.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import
|
import '../dados_grafico.dart';
|
||||||
|
|
||||||
class PieChartController extends ChangeNotifier {
|
class PieChartController extends ChangeNotifier {
|
||||||
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
|
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
|
||||||
@@ -10,7 +10,7 @@ class PieChartController extends ChangeNotifier {
|
|||||||
_chartData = PieChartData(
|
_chartData = PieChartData(
|
||||||
victories: victories ?? _chartData.victories,
|
victories: victories ?? _chartData.victories,
|
||||||
defeats: defeats ?? _chartData.defeats,
|
defeats: defeats ?? _chartData.defeats,
|
||||||
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES
|
draws: draws ?? _chartData.draws,
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class PieChartData {
|
class PieChartData {
|
||||||
final int victories;
|
final int victories;
|
||||||
final int defeats;
|
final int defeats;
|
||||||
final int draws; // 👇 AQUI ESTÃO OS EMPATES
|
final int draws;
|
||||||
|
|
||||||
const PieChartData({
|
const PieChartData({
|
||||||
required this.victories,
|
required this.victories,
|
||||||
@@ -9,7 +9,6 @@ class PieChartData {
|
|||||||
this.draws = 0,
|
this.draws = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 👇 MATEMÁTICA ATUALIZADA 👇
|
|
||||||
int get total => victories + defeats + draws;
|
int get total => victories + defeats + draws;
|
||||||
|
|
||||||
double get victoryPercentage => total > 0 ? victories / total : 0;
|
double get victoryPercentage => total > 0 ? victories / total : 0;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
|
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
|
||||||
import 'dados_grafico.dart';
|
import 'dados_grafico.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA ADICIONADO PARA USARMOS O primaryRed
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
class PieChartCard extends StatefulWidget {
|
class PieChartCard extends StatefulWidget {
|
||||||
final int victories;
|
final int victories;
|
||||||
@@ -8,7 +10,7 @@ class PieChartCard extends StatefulWidget {
|
|||||||
final int draws;
|
final int draws;
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final Color backgroundColor;
|
final Color? backgroundColor;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final double sf;
|
final double sf;
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ class PieChartCard extends StatefulWidget {
|
|||||||
this.title = 'DESEMPENHO',
|
this.title = 'DESEMPENHO',
|
||||||
this.subtitle = 'Temporada',
|
this.subtitle = 'Temporada',
|
||||||
this.onTap,
|
this.onTap,
|
||||||
required this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.sf = 1.0,
|
this.sf = 1.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,30 +61,31 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
|
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
|
||||||
|
|
||||||
|
// 👇 BLINDAGEM DO FUNDO E DO TEXTO PARA MODO CLARO/ESCURO
|
||||||
|
final Color cardColor = widget.backgroundColor ?? Theme.of(context).cardTheme.color ?? (Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white);
|
||||||
|
final Color textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _animation,
|
animation: _animation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Transform.scale(
|
return Transform.scale(
|
||||||
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
|
|
||||||
scale: 0.95 + (_animation.value * 0.05),
|
scale: 0.95 + (_animation.value * 0.05),
|
||||||
child: Opacity(
|
child: Opacity(opacity: _animation.value.clamp(0.0, 1.0), child: child),
|
||||||
// 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1
|
|
||||||
opacity: _animation.value.clamp(0.0, 1.0),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
elevation: 4,
|
elevation: 0, // Ajustado para não ter sombra dupla, já que o tema pode ter
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: BorderSide(color: Colors.grey.withOpacity(0.15)), // Borda suave igual ao resto da app
|
||||||
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(14),
|
color: cardColor, // 👇 APLICA A COR BLINDADA
|
||||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
|
|
||||||
),
|
),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@@ -90,29 +93,43 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
|
|||||||
final double cw = constraints.maxWidth;
|
final double cw = constraints.maxWidth;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.all(cw * 0.06),
|
padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 👇 TÍTULOS UM POUCO MAIS PRESENTES
|
// --- CABEÇALHO --- (👇 MANTIDO ALINHADO À ESQUERDA)
|
||||||
FittedBox(
|
FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)),
|
child: Text(widget.title.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: ch * 0.045,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.primaryRed, // 👇 USANDO O TEU primaryRed
|
||||||
|
letterSpacing: 1.2
|
||||||
|
)
|
||||||
),
|
),
|
||||||
FittedBox(
|
),
|
||||||
fit: BoxFit.scaleDown,
|
Text(widget.subtitle,
|
||||||
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)),
|
style: TextStyle(
|
||||||
|
fontSize: ch * 0.055,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.backgroundLight, // 👇 USANDO O TEU backgroundLight
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: ch * 0.03),
|
const Expanded(flex: 1, child: SizedBox()),
|
||||||
|
|
||||||
// MEIO (GRÁFICO + ESTATÍSTICAS)
|
// --- MIOLO (GRÁFICO MAIOR À ESQUERDA + STATS) ---
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: 9,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.end, // Changed from spaceBetween to end to push stats more to the right
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
// 1. Lado Esquerdo: Donut Chart
|
||||||
flex: 1,
|
// 👇 MUDANÇA AQUI: Gráfico ainda maior! cw * 0.52
|
||||||
|
SizedBox(
|
||||||
|
width: cw * 0.52,
|
||||||
|
height: cw * 0.52,
|
||||||
child: PieChartWidget(
|
child: PieChartWidget(
|
||||||
victoryPercentage: data.victoryPercentage,
|
victoryPercentage: data.victoryPercentage,
|
||||||
defeatPercentage: data.defeatPercentage,
|
defeatPercentage: data.defeatPercentage,
|
||||||
@@ -120,59 +137,60 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
|
|||||||
sf: widget.sf,
|
sf: widget.sf,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: cw * 0.05),
|
|
||||||
|
SizedBox(width: cw * 0.005), // Reduzi o espaço no meio para dar lugar ao gráfico
|
||||||
|
|
||||||
|
// 2. Lado Direito: Números Dinâmicos
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 1,
|
child: FittedBox(
|
||||||
|
alignment: Alignment.centerRight, // Encosta os números à direita
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.end, // Alinha os números à direita para ficar arrumado
|
||||||
children: [
|
children: [
|
||||||
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch),
|
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, textColor, ch, cw),
|
||||||
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch),
|
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.amber, textColor, ch, cw),
|
||||||
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch),
|
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, textColor, ch, cw),
|
||||||
_buildDynDivider(ch),
|
_buildDynDivider(cw, textColor),
|
||||||
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch),
|
_buildDynStatRow("TOT", data.total.toString(), "100", textColor, textColor, ch, cw),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 👇 RODAPÉ AJUSTADO
|
const Expanded(flex: 1, child: SizedBox()),
|
||||||
SizedBox(height: ch * 0.03),
|
|
||||||
|
// --- RODAPÉ: BOTÃO WIN RATE GIGANTE --- (👇 MUDANÇA AQUI: Alinhado à esquerda)
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
|
padding: EdgeInsets.symmetric(vertical: ch * 0.025),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white24, // Igual ao fundo do botão detalhes
|
color: textColor.withOpacity(0.05), // 👇 Fundo adaptável
|
||||||
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Center(
|
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.start, // 👇 MUDANÇA AQUI: Letras mais para a esquerda!
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.stars, color: Colors.green, size: ch * 0.075),
|
||||||
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down,
|
const SizedBox(width: 10),
|
||||||
color: Colors.green,
|
Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
|
||||||
size: ch * 0.09
|
|
||||||
),
|
|
||||||
SizedBox(width: cw * 0.02),
|
|
||||||
Text(
|
|
||||||
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: ch * 0.05,
|
color: AppTheme.backgroundLight,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white
|
letterSpacing: 1.0,
|
||||||
)
|
fontSize: ch * 0.06
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -183,34 +201,39 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
|
|
||||||
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) {
|
// 👇 Ajustei a linha de stats para alinhar melhor agora que os números estão encostados à direita
|
||||||
|
Widget _buildDynStatRow(String label, String number, String percent, Color statColor, Color textColor, double ch, double cw) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: ch * 0.01),
|
padding: EdgeInsets.symmetric(vertical: ch * 0.005),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Número subiu para 0.10
|
SizedBox(
|
||||||
Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))),
|
width: cw * 0.12,
|
||||||
SizedBox(width: ch * 0.02),
|
child: Column(
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
flex: 3,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
children: [
|
||||||
Row(children: [
|
Text(label, style: TextStyle(fontSize: ch * 0.045, color: textColor.withOpacity(0.6), fontWeight: FontWeight.bold)), // 👇 TEXTO ADAPTÁVEL (increased from 0.035)
|
||||||
Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
Text('$percent%', style: TextStyle(fontSize: ch * 0.05, color: statColor, fontWeight: FontWeight.bold)), // (increased from 0.04)
|
||||||
// Label subiu para 0.045
|
],
|
||||||
Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600))))
|
|
||||||
]),
|
|
||||||
// Percentagem subiu para 0.05
|
|
||||||
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: cw * 0.03),
|
||||||
|
Text(number, style: TextStyle(fontSize: ch * 0.15, fontWeight: FontWeight.w900, color: statColor, height: 1)), // (increased from 0.125)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDynDivider(double ch) {
|
Widget _buildDynDivider(double cw, Color textColor) {
|
||||||
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01));
|
return Container(
|
||||||
|
width: cw * 0.35,
|
||||||
|
height: 1.5,
|
||||||
|
color: textColor.withOpacity(0.2), // 👇 LINHA ADAPTÁVEL
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,12 +19,9 @@ class PieChartWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
// 👇 MAGIA ANTI-DESAPARECIMENTO 👇
|
|
||||||
// Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir.
|
|
||||||
final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
|
final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
|
||||||
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight;
|
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight;
|
||||||
|
|
||||||
// Pega no menor valor para garantir que o círculo não é cortado
|
|
||||||
final double size = math.min(w, h);
|
final double size = math.min(w, h);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
@@ -32,7 +29,7 @@ class PieChartWidget extends StatelessWidget {
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: _PieChartPainter(
|
painter: _DonutChartPainter(
|
||||||
victoryPercentage: victoryPercentage,
|
victoryPercentage: victoryPercentage,
|
||||||
defeatPercentage: defeatPercentage,
|
defeatPercentage: defeatPercentage,
|
||||||
drawPercentage: drawPercentage,
|
drawPercentage: drawPercentage,
|
||||||
@@ -48,24 +45,27 @@ class PieChartWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCenterLabels(double size) {
|
Widget _buildCenterLabels(double size) {
|
||||||
|
final bool hasGames = victoryPercentage > 0 || defeatPercentage > 0 || drawPercentage > 0;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
|
// 👇 Casa decimal aplicada aqui!
|
||||||
|
hasGames ? '${(victoryPercentage * 100).toStringAsFixed(1)}%' : '---',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo
|
fontSize: size * (hasGames ? 0.20 : 0.15),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: hasGames ? Colors.white : Colors.white54,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: size * 0.02),
|
SizedBox(height: size * 0.02),
|
||||||
Text(
|
Text(
|
||||||
'Vitórias',
|
hasGames ? 'Vitórias' : 'Sem Jogos',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: size * 0.10,
|
fontSize: size * 0.08,
|
||||||
color: Colors.white.withOpacity(0.8),
|
color: hasGames ? Colors.white70 : Colors.white38,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -73,12 +73,12 @@ class PieChartWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PieChartPainter extends CustomPainter {
|
class _DonutChartPainter extends CustomPainter {
|
||||||
final double victoryPercentage;
|
final double victoryPercentage;
|
||||||
final double defeatPercentage;
|
final double defeatPercentage;
|
||||||
final double drawPercentage;
|
final double drawPercentage;
|
||||||
|
|
||||||
_PieChartPainter({
|
_DonutChartPainter({
|
||||||
required this.victoryPercentage,
|
required this.victoryPercentage,
|
||||||
required this.defeatPercentage,
|
required this.defeatPercentage,
|
||||||
required this.drawPercentage,
|
required this.drawPercentage,
|
||||||
@@ -87,59 +87,40 @@ class _PieChartPainter extends CustomPainter {
|
|||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final center = Offset(size.width / 2, size.height / 2);
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
// Margem de 5% para a linha de fora não ser cortada
|
final radius = (size.width / 2) - (size.width * 0.1);
|
||||||
final radius = (size.width / 2) - (size.width * 0.05);
|
final strokeWidth = size.width * 0.2;
|
||||||
|
|
||||||
|
if (victoryPercentage == 0 && defeatPercentage == 0 && drawPercentage == 0) {
|
||||||
|
final bgPaint = Paint()
|
||||||
|
..color = Colors.white.withOpacity(0.05)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth;
|
||||||
|
canvas.drawCircle(center, radius, bgPaint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const victoryColor = Colors.green;
|
const victoryColor = Colors.green;
|
||||||
const defeatColor = Colors.red;
|
const defeatColor = Colors.red;
|
||||||
const drawColor = Colors.yellow;
|
const drawColor = Colors.amber;
|
||||||
const borderColor = Colors.white30;
|
|
||||||
|
|
||||||
double startAngle = -math.pi / 2;
|
double startAngle = -math.pi / 2;
|
||||||
|
|
||||||
if (victoryPercentage > 0) {
|
void drawDonutSector(double percentage, Color color) {
|
||||||
final sweepAngle = 2 * math.pi * victoryPercentage;
|
if (percentage <= 0) return;
|
||||||
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width);
|
final sweepAngle = 2 * math.pi * percentage;
|
||||||
startAngle += sweepAngle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawPercentage > 0) {
|
|
||||||
final sweepAngle = 2 * math.pi * drawPercentage;
|
|
||||||
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
|
|
||||||
startAngle += sweepAngle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defeatPercentage > 0) {
|
|
||||||
final sweepAngle = 2 * math.pi * defeatPercentage;
|
|
||||||
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor, size.width);
|
|
||||||
}
|
|
||||||
|
|
||||||
final borderPaint = Paint()
|
|
||||||
..color = borderColor
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeWidth = size.width * 0.02;
|
|
||||||
|
|
||||||
canvas.drawCircle(center, radius, borderPaint);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) {
|
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = color
|
..color = color
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
|
|
||||||
|
|
||||||
if (sweepAngle < 2 * math.pi) {
|
|
||||||
final linePaint = Paint()
|
|
||||||
..color = Colors.white.withOpacity(0.5)
|
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = totalWidth * 0.015;
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.butt;
|
||||||
|
|
||||||
final lineX = center.dx + radius * math.cos(startAngle);
|
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint);
|
||||||
final lineY = center.dy + radius * math.sin(startAngle);
|
startAngle += sweepAngle;
|
||||||
|
|
||||||
canvas.drawLine(center, Offset(lineX, lineY), linePaint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawDonutSector(victoryPercentage, victoryColor);
|
||||||
|
drawDonutSector(drawPercentage, drawColor);
|
||||||
|
drawDonutSector(defeatPercentage, defeatColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,27 +1,314 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:playmaker/controllers/placar_controller.dart';
|
||||||
|
|
||||||
void main() {
|
class ActionButtonsPanel extends StatelessWidget {
|
||||||
runApp(const MyApp());
|
final PlacarController controller;
|
||||||
}
|
final double sf;
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
const ActionButtonsPanel({super.key, required this.controller, required this.sf});
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
final double baseSize = 58 * sf;
|
||||||
home: Scaffold(
|
final double feedSize = 73 * sf;
|
||||||
appBar: AppBar(
|
final double gap = 5 * sf;
|
||||||
title: const Text('Ícone de Basquete'),
|
|
||||||
),
|
return Padding(
|
||||||
body: const Center(
|
padding: EdgeInsets.only(bottom: 12 * sf),
|
||||||
child: Icon(
|
child: Row(
|
||||||
Icons.sports_basketball,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
size: 100.0, // Tamanho do ícone
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
color: Colors.black, // Cor do ícone (preto, como na imagem)
|
children: [
|
||||||
),
|
_columnBtn([
|
||||||
),
|
// 🔴 1ª Linha: Agora "sub_pts_1" (Anular pontos)
|
||||||
|
_dragAndTargetBtn("-1", AppTheme.actionMiss, "sub_pts_1", baseSize, feedSize, sf, badge: ""),
|
||||||
|
_dragAndTargetBtn("1", AppTheme.actionPoints, "add_pts_1", baseSize, feedSize, sf, badge: "FTM"),
|
||||||
|
// ❌ 3ª Linha: Agora "miss_1" (Falhados)
|
||||||
|
_dragAndTargetBtn("1", AppTheme.actionPoints, "miss_1", baseSize, feedSize, sf, badge: "FTA", isX: true),
|
||||||
|
_dragAndTargetBtn("STL", AppTheme.actionSteal, "add_stl", baseSize, feedSize, sf, badge: "STL"),
|
||||||
|
], gap),
|
||||||
|
SizedBox(width: gap),
|
||||||
|
_columnBtn([
|
||||||
|
// 🔴 1ª Linha: Agora "sub_pts_2" (Anular pontos)
|
||||||
|
_dragAndTargetBtn("-2", AppTheme.actionMiss, "sub_pts_2", baseSize, feedSize, sf, badge: ""),
|
||||||
|
_dragAndTargetBtn("2", AppTheme.actionPoints, "add_pts_2", baseSize, feedSize, sf, badge: "2PM"),
|
||||||
|
// ❌ 3ª Linha: Agora "miss_2" (Falhados)
|
||||||
|
_dragAndTargetBtn("2", AppTheme.actionPoints, "miss_2", baseSize, feedSize, sf, badge: "2PA", isX: true),
|
||||||
|
_dragAndTargetBtn("AST", AppTheme.actionAssist, "add_ast", baseSize, feedSize, sf, badge: "AST"),
|
||||||
|
], gap),
|
||||||
|
SizedBox(width: gap),
|
||||||
|
_columnBtn([
|
||||||
|
// 🔴 1ª Linha: Agora "sub_pts_3" (Anular pontos)
|
||||||
|
_dragAndTargetBtn("-3", AppTheme.actionMiss, "sub_pts_3", baseSize, feedSize, sf, badge: ""),
|
||||||
|
_dragAndTargetBtn("3", AppTheme.actionPoints, "add_pts_3", baseSize, feedSize, sf, badge: "3PM"),
|
||||||
|
// ❌ 3ª Linha: Agora "miss_3" (Falhados)
|
||||||
|
_dragAndTargetBtn("3", AppTheme.actionPoints, "miss_3", baseSize, feedSize, sf, badge: "3PA", isX: true),
|
||||||
|
_dragAndTargetBtn("TOV", AppTheme.actionMiss, "add_tov", baseSize, feedSize, sf, badge: "TOV"),
|
||||||
|
], gap),
|
||||||
|
SizedBox(width: gap),
|
||||||
|
_columnBtn([
|
||||||
|
_dragAndTargetBtn("O", AppTheme.actionRebound, "add_orb", baseSize, feedSize, sf, badge: "OREB"),
|
||||||
|
_dragAndTargetBtn("D", AppTheme.actionRebound, "add_drb", baseSize, feedSize, sf, badge: "DREB"),
|
||||||
|
_dragAndTargetBtn("BLK", AppTheme.actionBlock, "add_blk", baseSize, feedSize, sf, badge: "BLK"),
|
||||||
|
], gap),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Widget _columnBtn(List<Widget> children, double gap) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children.map((c) => Padding(padding: EdgeInsets.only(bottom: gap), child: c)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _dragAndTargetBtn(String label, Color color, String actionData, double baseSize, double feedSize, double sf,
|
||||||
|
{IconData? icon, bool isX = false, String badge = ""}) {
|
||||||
|
return Draggable<String>(
|
||||||
|
data: actionData,
|
||||||
|
feedback: _circle(label, color, icon, true, baseSize, feedSize, sf, isX: isX, badge: badge),
|
||||||
|
childWhenDragging: Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX, badge: badge),
|
||||||
|
),
|
||||||
|
child: DragTarget<String>(
|
||||||
|
onAcceptWithDetails: (details) {},
|
||||||
|
builder: (context, candidateData, rejectedData) {
|
||||||
|
bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_"));
|
||||||
|
return Transform.scale(
|
||||||
|
scale: isHovered ? 1.15 : 1.0,
|
||||||
|
child: Container(
|
||||||
|
decoration: isHovered
|
||||||
|
? BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10 * sf, spreadRadius: 3 * sf)])
|
||||||
|
: null,
|
||||||
|
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX, badge: badge),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _basketball(Color color, double size) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: size * 0.78,
|
||||||
|
height: size * 0.78,
|
||||||
|
decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle),
|
||||||
|
),
|
||||||
|
Icon(Icons.sports_basketball, color: color, size: size),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _circle(String label, Color color, IconData? icon, bool isFeed, double baseSize, double feedSize, double sf,
|
||||||
|
{bool isX = false, String badge = ""}) {
|
||||||
|
double size = isFeed ? feedSize : baseSize;
|
||||||
|
Widget content;
|
||||||
|
bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "-1" || label == "-2" || label == "-3";
|
||||||
|
bool isRebBtn = label == "O" || label == "D";
|
||||||
|
bool isBlkBtn = label == "BLK";
|
||||||
|
bool isStlBtn = label == "STL";
|
||||||
|
bool isAstBtn = label == "AST";
|
||||||
|
bool isTovBtn = label == "TOV";
|
||||||
|
|
||||||
|
if (isPointBtn) {
|
||||||
|
content = Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
_basketball(color, size * 0.9),
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size * 0.38,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
foreground: Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = size * 0.05
|
||||||
|
..color = Colors.white,
|
||||||
|
decoration: TextDecoration.none)),
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size * 0.38,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.black,
|
||||||
|
decoration: TextDecoration.none)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (isRebBtn) {
|
||||||
|
content = Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: size * 0.75,
|
||||||
|
height: size * 0.75,
|
||||||
|
child: CustomPaint(painter: HoopIconPainter()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: size * -0.05,
|
||||||
|
right: size * 0.05,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size * 0.48,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
foreground: Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = size * 0.07
|
||||||
|
..color = Colors.black,
|
||||||
|
decoration: TextDecoration.none)),
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size * 0.48,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
decoration: TextDecoration.none)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (isBlkBtn) {
|
||||||
|
content = Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: size * 0.75);
|
||||||
|
} else if (isStlBtn) {
|
||||||
|
content = Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Positioned(top: size * 0.05, child: _basketball(color, size * 0.60)),
|
||||||
|
Positioned(bottom: size * 0.10, child: Icon(Icons.pan_tool, color: color, size: size * 0.50)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (isAstBtn) {
|
||||||
|
content = Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Positioned(top: size * 0.05, child: _basketball(color, size * 0.60)),
|
||||||
|
Positioned(
|
||||||
|
bottom: size * 0.08,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationY(3.14159),
|
||||||
|
child: Icon(Icons.back_hand, color: color, size: size * 0.46)),
|
||||||
|
SizedBox(width: size * 0.04),
|
||||||
|
Icon(Icons.back_hand, color: color, size: size * 0.46),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (isTovBtn) {
|
||||||
|
content = Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Positioned(top: size * 0.05, child: _basketball(color, size * 0.60)),
|
||||||
|
Positioned(bottom: size * 0.10, child: Icon(Icons.swap_horiz_rounded, color: color, size: size * 0.55)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontWeight: FontWeight.bold, fontSize: size * 0.35, decoration: TextDecoration.none));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: (isPointBtn || isBlkBtn || isRebBtn || isStlBtn || isAstBtn || isTovBtn)
|
||||||
|
? const BoxDecoration(color: Colors.transparent)
|
||||||
|
: BoxDecoration(
|
||||||
|
gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
// Badge só aparece se não for vazio
|
||||||
|
if (badge.isNotEmpty)
|
||||||
|
Positioned(
|
||||||
|
bottom: size * -0.04,
|
||||||
|
left: size * -0.04,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: size * 0.09, vertical: size * 0.02),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
borderRadius: BorderRadius.circular(size * 0.07),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black45, blurRadius: 2, offset: const Offset(0, 1))],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
badge,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: size * 0.16,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isX)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.cancel, color: Colors.red, size: size * 0.4))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HoopIconPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final double w = size.width;
|
||||||
|
final double h = size.height;
|
||||||
|
|
||||||
|
final Paint paint = Paint()
|
||||||
|
..color = Colors.black
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = w * 0.08
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round;
|
||||||
|
|
||||||
|
canvas.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(Rect.fromLTRB(w * 0.02, h * 0.02, w * 0.98, h * 0.70), Radius.circular(w * 0.05)), paint);
|
||||||
|
canvas.drawRect(Rect.fromLTRB(w * 0.28, h * 0.30, w * 0.72, h * 0.70), paint);
|
||||||
|
|
||||||
|
final Paint rimPaint = Paint()
|
||||||
|
..color = Colors.orange
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = w * 0.09
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
canvas.drawLine(Offset(w * 0.20, h * 0.75), Offset(w * 0.80, h * 0.75), rimPaint);
|
||||||
|
|
||||||
|
final Paint netPaint = Paint()
|
||||||
|
..color = Colors.black
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = w * 0.05;
|
||||||
|
|
||||||
|
canvas.drawLine(Offset(w * 0.25, h * 0.75), Offset(w * 0.40, h * 0.98), netPaint);
|
||||||
|
canvas.drawLine(Offset(w * 0.75, h * 0.75), Offset(w * 0.60, h * 0.98), netPaint);
|
||||||
|
canvas.drawLine(Offset(w * 0.40, h * 0.75), Offset(w * 0.30, h * 0.98), netPaint);
|
||||||
|
canvas.drawLine(Offset(w * 0.55, h * 0.75), Offset(w * 0.65, h * 0.98), netPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
@@ -1,35 +1,47 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart'; // Para as orientações
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'pages/login.dart';
|
import 'pages/login.dart';
|
||||||
|
|
||||||
|
// Variável global para controlar o Tema
|
||||||
|
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
// 1. Inicializa os bindings do Flutter
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// 2. Inicializa o Supabase
|
||||||
await Supabase.initialize(
|
await Supabase.initialize(
|
||||||
url: 'https://sihwjdshexjyvsbettcd.supabase.co',
|
url: 'https://sihwjdshexjyvsbettcd.supabase.co',
|
||||||
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaHdqZHNoZXhqeXZzYmV0dGNkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg5MTQxMjgsImV4cCI6MjA4NDQ5MDEyOH0.gW3AvTJVNyE1Dqa72OTnhrUIKsndexrY3pKxMIAaAy8', // Uma string longa
|
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaHdqZHNoZXhqeXZzYmV0dGNkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg5MTQxMjgsImV4cCI6MjA4NDQ5MDEyOH0.gW3AvTJVNyE1Dqa72OTnhrUIKsndexrY3pKxMIAaAy8',
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. Deixa a orientação livre (Portrait) para o arranque da App
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
]);
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<ThemeMode>(
|
||||||
|
valueListenable: themeNotifier,
|
||||||
|
builder: (_, ThemeMode currentMode, __) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'PlayMaker',
|
title: 'PlayMaker',
|
||||||
theme: ThemeData(
|
theme: AppTheme.lightTheme,
|
||||||
colorScheme: ColorScheme.fromSeed(
|
darkTheme: AppTheme.darkTheme,
|
||||||
seedColor: const Color(0xFFE74C3C),
|
themeMode: currentMode,
|
||||||
),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: const LoginPage(),
|
home: const LoginPage(),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,71 @@
|
|||||||
class Game {
|
class Game {
|
||||||
final String id;
|
final String id;
|
||||||
|
final String userId;
|
||||||
final String myTeam;
|
final String myTeam;
|
||||||
final String opponentTeam;
|
final String opponentTeam;
|
||||||
final String myScore;
|
final String myScore;
|
||||||
final String opponentScore;
|
final String opponentScore;
|
||||||
final String status;
|
|
||||||
final String season;
|
final String season;
|
||||||
|
final String status;
|
||||||
|
final DateTime gameDate;
|
||||||
|
|
||||||
|
// Novos campos que estão na tua base de dados
|
||||||
|
final int remainingSeconds;
|
||||||
|
final int myTimeouts;
|
||||||
|
final int oppTimeouts;
|
||||||
|
final int currentQuarter;
|
||||||
|
final String topPtsName;
|
||||||
|
final String topAstName;
|
||||||
|
final String topRbsName;
|
||||||
|
final String topDefName;
|
||||||
|
final String mvpName;
|
||||||
|
|
||||||
Game({
|
Game({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
required this.myTeam,
|
required this.myTeam,
|
||||||
required this.opponentTeam,
|
required this.opponentTeam,
|
||||||
required this.myScore,
|
required this.myScore,
|
||||||
required this.opponentScore,
|
required this.opponentScore,
|
||||||
required this.status,
|
|
||||||
required this.season,
|
required this.season,
|
||||||
|
required this.status,
|
||||||
|
required this.gameDate,
|
||||||
|
required this.remainingSeconds,
|
||||||
|
required this.myTimeouts,
|
||||||
|
required this.oppTimeouts,
|
||||||
|
required this.currentQuarter,
|
||||||
|
required this.topPtsName,
|
||||||
|
required this.topAstName,
|
||||||
|
required this.topRbsName,
|
||||||
|
required this.topDefName,
|
||||||
|
required this.mvpName,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Game.fromMap(Map<String, dynamic> map) {
|
// 👇 A MÁGICA ACONTECE AQUI: Lemos os dados e protegemos os NULLs
|
||||||
|
factory Game.fromMap(Map<String, dynamic> json) {
|
||||||
return Game(
|
return Game(
|
||||||
// O "?." converte para texto com segurança, e o "?? '...'" diz o que mostrar se for nulo (vazio)
|
id: json['id']?.toString() ?? '',
|
||||||
id: map['id']?.toString() ?? '',
|
userId: json['user_id']?.toString() ?? '',
|
||||||
myTeam: map['my_team']?.toString() ?? 'Desconhecida',
|
myTeam: json['my_team']?.toString() ?? 'Minha Equipa',
|
||||||
opponentTeam: map['opponent_team']?.toString() ?? 'Adversário',
|
opponentTeam: json['opponent_team']?.toString() ?? 'Adversário',
|
||||||
myScore: map['my_score']?.toString() ?? '0',
|
myScore: (json['my_score'] ?? 0).toString(), // Protege NULL e converte Int4 para String
|
||||||
opponentScore: map['opponent_score']?.toString() ?? '0',
|
opponentScore: (json['opponent_score'] ?? 0).toString(),
|
||||||
status: map['status']?.toString() ?? 'Terminado',
|
season: json['season']?.toString() ?? '---',
|
||||||
season: map['season']?.toString() ?? 'Sem Época',
|
status: json['status']?.toString() ?? 'Decorrer',
|
||||||
|
gameDate: json['game_date'] != null ? DateTime.tryParse(json['game_date']) ?? DateTime.now() : DateTime.now(),
|
||||||
|
|
||||||
|
// Proteção para os Inteiros (se for NULL, assume 0)
|
||||||
|
remainingSeconds: json['remaining_seconds'] as int? ?? 600, // 600s = 10 minutos
|
||||||
|
myTimeouts: json['my_timeouts'] as int? ?? 0,
|
||||||
|
oppTimeouts: json['opp_timeouts'] as int? ?? 0,
|
||||||
|
currentQuarter: json['current_quarter'] as int? ?? 1,
|
||||||
|
|
||||||
|
// Proteção para os Nomes (se for NULL, assume '---')
|
||||||
|
topPtsName: json['top_pts_name']?.toString() ?? '---',
|
||||||
|
topAstName: json['top_ast_name']?.toString() ?? '---',
|
||||||
|
topRbsName: json['top_rbs_name']?.toString() ?? '---',
|
||||||
|
topDefName: json['top_def_name']?.toString() ?? '---',
|
||||||
|
mvpName: json['mvp_name']?.toString() ?? '---',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,24 +3,43 @@ class Person {
|
|||||||
final String teamId;
|
final String teamId;
|
||||||
final String name;
|
final String name;
|
||||||
final String type; // 'Jogador' ou 'Treinador'
|
final String type; // 'Jogador' ou 'Treinador'
|
||||||
final String number;
|
final String? number; // O número é opcional (Treinadores não têm)
|
||||||
|
|
||||||
|
// 👇 A NOVA PROPRIEDADE AQUI!
|
||||||
|
final String? imageUrl;
|
||||||
|
|
||||||
Person({
|
Person({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.teamId,
|
required this.teamId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.number,
|
this.number,
|
||||||
|
this.imageUrl, // 👇 ADICIONADO AO CONSTRUTOR
|
||||||
});
|
});
|
||||||
|
|
||||||
// Converte o JSON do Supabase para o objeto Person
|
// Lê os dados do Supabase e converte para a classe Person
|
||||||
factory Person.fromMap(Map<String, dynamic> map) {
|
factory Person.fromMap(Map<String, dynamic> map) {
|
||||||
return Person(
|
return Person(
|
||||||
id: map['id'] ?? '',
|
id: map['id']?.toString() ?? '',
|
||||||
teamId: map['team_id'] ?? '',
|
teamId: map['team_id']?.toString() ?? '',
|
||||||
name: map['name'] ?? '',
|
name: map['name']?.toString() ?? 'Desconhecido',
|
||||||
type: map['type'] ?? 'Jogador',
|
type: map['type']?.toString() ?? 'Jogador',
|
||||||
number: map['number']?.toString() ?? '',
|
number: map['number']?.toString(),
|
||||||
|
|
||||||
|
// 👇 AGORA ELE JÁ SABE LER O LINK DA IMAGEM DA TUA BASE DE DADOS!
|
||||||
|
imageUrl: map['image_url']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepara os dados para enviar para o Supabase (se necessário)
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'team_id': teamId,
|
||||||
|
'name': name,
|
||||||
|
'type': type,
|
||||||
|
'number': number,
|
||||||
|
'image_url': imageUrl, // 👇 TAMBÉM GUARDA A IMAGEM
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,26 +4,33 @@ class Team {
|
|||||||
final String season;
|
final String season;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
final String createdAt;
|
||||||
|
final int playerCount; // 👇 NOVA VARIÁVEL AQUI
|
||||||
|
|
||||||
Team({
|
Team({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.season,
|
required this.season,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
this.isFavorite = false
|
required this.isFavorite,
|
||||||
|
required this.createdAt,
|
||||||
|
this.playerCount = 0, // 👇 VALOR POR DEFEITO
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mapeia o JSON que vem do Supabase (id costuma ser UUID ou String)
|
|
||||||
factory Team.fromMap(Map<String, dynamic> map) {
|
factory Team.fromMap(Map<String, dynamic> map) {
|
||||||
return Team(
|
return Team(
|
||||||
id: map['id']?.toString() ?? '',
|
id: map['id']?.toString() ?? '',
|
||||||
name: map['name'] ?? '',
|
name: map['name']?.toString() ?? 'Sem Nome',
|
||||||
season: map['season'] ?? '',
|
season: map['season']?.toString() ?? '',
|
||||||
imageUrl: map['image_url'] ?? '',
|
imageUrl: map['image_url']?.toString() ?? '',
|
||||||
isFavorite: map['is_favorite'] ?? false,
|
isFavorite: map['is_favorite'] ?? false,
|
||||||
|
createdAt: map['created_at']?.toString() ?? '',
|
||||||
|
// 👇 AGORA ELE LÊ A CONTAGEM DA TUA NOVA VIEW!
|
||||||
|
playerCount: map['player_count'] != null ? int.tryParse(map['player_count'].toString()) ?? 0 : 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:playmaker/controllers/placar_controller.dart';
|
import 'package:playmaker/icons.dart/resaltosicon.dart';
|
||||||
import 'package:playmaker/utils/size_extension.dart';
|
import 'package:playmaker/widgets/placar_widgets.dart'; // Mantém este import
|
||||||
import 'package:playmaker/widgets/placar_widgets.dart';
|
import 'dart:math' as math;
|
||||||
import 'dart:math' as math;
|
import '../classe/theme.dart';
|
||||||
|
import '../controllers/placar_controller.dart';
|
||||||
|
import 'package:playmaker/zone_map_dialog.dart';
|
||||||
|
|
||||||
class PlacarPage extends StatefulWidget {
|
class PlacarPage extends StatefulWidget {
|
||||||
final String gameId, myTeam, opponentTeam;
|
final String gameId, myTeam, opponentTeam;
|
||||||
const PlacarPage({super.key, required this.gameId, required this.myTeam, required this.opponentTeam});
|
|
||||||
|
const PlacarPage({
|
||||||
|
super.key,
|
||||||
|
required this.gameId,
|
||||||
|
required this.myTeam,
|
||||||
|
required this.opponentTeam
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlacarPage> createState() => _PlacarPageState();
|
State<PlacarPage> createState() => _PlacarPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlacarPageState extends State<PlacarPage> {
|
class _PlacarPageState extends State<PlacarPage> {
|
||||||
late PlacarController _controller;
|
late PlacarController _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,9 +36,6 @@ import 'package:playmaker/utils/size_extension.dart';
|
|||||||
gameId: widget.gameId,
|
gameId: widget.gameId,
|
||||||
myTeam: widget.myTeam,
|
myTeam: widget.myTeam,
|
||||||
opponentTeam: widget.opponentTeam,
|
opponentTeam: widget.opponentTeam,
|
||||||
onUpdate: () {
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
_controller.loadPlayers();
|
_controller.loadPlayers();
|
||||||
}
|
}
|
||||||
@@ -42,7 +47,6 @@ import 'package:playmaker/utils/size_extension.dart';
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BOTÕES FLUTUANTES DE FALTA ---
|
|
||||||
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,
|
||||||
@@ -73,57 +77,58 @@ import 'package:playmaker/utils/size_extension.dart';
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BOTÕES LATERAIS QUADRADOS ---
|
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: color,
|
backgroundColor: onTap == null ? Colors.grey : color,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
|
||||||
elevation: 5,
|
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(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),
|
: Icon(icon, color: Colors.white, size: size * 0.55),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showHeatmap(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => HeatmapDialog(
|
||||||
|
shots: _controller.matchShots,
|
||||||
|
myTeamName: _controller.myTeam,
|
||||||
|
oppTeamName: _controller.opponentTeam,
|
||||||
|
myPlayersIds: [..._controller.myCourt, ..._controller.myBench],
|
||||||
|
oppPlayersIds: [..._controller.oppCourt, ..._controller.oppBench],
|
||||||
|
playerStats: _controller.playerStats,
|
||||||
|
playerNames: _controller.playerNames,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@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;
|
||||||
|
|
||||||
// 👇 CÁLCULO MANUAL DO SF 👇
|
|
||||||
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; // Tamanho ideal (Nem 38 nem 55)
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
if (_controller.isLoading) {
|
if (_controller.isLoading) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF16202C),
|
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("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||||
SizedBox(height: 35 * sf),
|
SizedBox(height: 35 * sf),
|
||||||
StreamBuilder(
|
const CircularProgressIndicator(color: Colors.orangeAccent),
|
||||||
stream: Stream.periodic(const Duration(seconds: 3)),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
List<String> frases = [
|
|
||||||
"O Treinador está a desenhar a tática...",
|
|
||||||
"A encher as bolas com ar de campeão...",
|
|
||||||
"O árbitro está a testar o apito...",
|
|
||||||
"A verificar se o cesto está nivelado...",
|
|
||||||
"Os jogadores estão a terminar o aquecimento..."
|
|
||||||
];
|
|
||||||
String frase = frases[DateTime.now().second % frases.length];
|
|
||||||
return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * sf, fontStyle: FontStyle.italic));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -131,16 +136,13 @@ import 'package:playmaker/utils/size_extension.dart';
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF266174),
|
backgroundColor: AppTheme.placarBackground,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false, bottom: false,
|
||||||
bottom: false,
|
|
||||||
// 👇 A MÁGICA DO IGNORE POINTER COMEÇA AQUI 👇
|
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
ignoring: _controller.isSaving, // Se estiver a gravar, ignora os toques!
|
ignoring: _controller.isSaving,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// --- O CAMPO ---
|
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
|
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
|
||||||
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
|
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
|
||||||
@@ -148,13 +150,21 @@ import 'package:playmaker/utils/size_extension.dart';
|
|||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final w = constraints.maxWidth;
|
final w = constraints.maxWidth;
|
||||||
final h = constraints.maxHeight;
|
final h = constraints.maxHeight;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTapDown: (details) {
|
onTapDown: (details) {
|
||||||
if (_controller.isSelectingShotLocation) {
|
if (_controller.isSelectingShotLocation) {
|
||||||
|
bool isMake = _controller.pendingAction?.startsWith("add_pts_") ?? false;
|
||||||
|
String? pData = _controller.pendingPlayerId;
|
||||||
|
|
||||||
_controller.registerShotLocation(context, details.localPosition, Size(w, h));
|
_controller.registerShotLocation(context, details.localPosition, Size(w, h));
|
||||||
|
|
||||||
|
if (isMake && pData != null) {
|
||||||
|
bool isOpp = pData.startsWith("player_opp_");
|
||||||
|
String pId = pData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||||
|
showAssistDialog(context, _controller, isOpp, pId, sf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -164,43 +174,25 @@ import 'package:playmaker/utils/size_extension.dart';
|
|||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Stack(
|
|
||||||
children: _controller.matchShots.map((shot) => Positioned(
|
|
||||||
// Agora usamos relativeX e relativeY multiplicados pela largura(w) e altura(h)
|
|
||||||
left: (shot.relativeX * w) - (9 * context.sf),
|
|
||||||
top: (shot.relativeY * h) - (9 * context.sf),
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 9 * context.sf,
|
|
||||||
backgroundColor: shot.isMake ? Colors.green : Colors.red,
|
|
||||||
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * context.sf, color: Colors.white)
|
|
||||||
),
|
|
||||||
)).toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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)),
|
||||||
|
|
||||||
// --- JOGADORES ---
|
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[0], isOpponent: true, sf: sf)),
|
||||||
if (!_controller.isSelectingShotLocation) ...[
|
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.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false, 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.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false, 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.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false, sf: sf)),
|
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[4], isOpponent: true, sf: sf)),
|
||||||
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false, sf: sf)),
|
|
||||||
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false, sf: sf)),
|
|
||||||
|
|
||||||
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true, sf: sf)),
|
|
||||||
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true, sf: sf)),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// --- BOTÕES DE FALTAS ---
|
|
||||||
if (!_controller.isSelectingShotLocation) ...[
|
if (!_controller.isSelectingShotLocation) ...[
|
||||||
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
|
_buildFloatingFoulBtn("FALTA +", AppTheme.actionPoints, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
|
||||||
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
|
_buildFloatingFoulBtn("FALTA -", AppTheme.actionMiss, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
|
||||||
],
|
],
|
||||||
|
|
||||||
// --- BOTÃO PLAY/PAUSE ---
|
|
||||||
if (!_controller.isSelectingShotLocation)
|
if (!_controller.isSelectingShotLocation)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: (h * 0.32) + (40 * sf),
|
top: (h * 0.32) + (40 * sf),
|
||||||
@@ -216,13 +208,9 @@ child: Stack(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// --- PLACAR NO TOPO ---
|
|
||||||
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
|
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
|
||||||
|
|
||||||
// --- BOTÕES DE AÇÃO ---
|
|
||||||
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
|
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
|
||||||
|
|
||||||
// --- OVERLAY LANÇAMENTO ---
|
|
||||||
if (_controller.isSelectingShotLocation)
|
if (_controller.isSelectingShotLocation)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: h * 0.4, left: 0, right: 0,
|
top: h * 0.4, left: 0, right: 0,
|
||||||
@@ -230,7 +218,7 @@ child: Stack(
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf),
|
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf),
|
||||||
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * 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 DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * sf, fontWeight: FontWeight.bold)),
|
child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL", style: TextStyle(color: Colors.white, fontSize: 22 * sf, fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -240,86 +228,87 @@ child: Stack(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- BOTÕES LATERAIS ---
|
|
||||||
// Topo Esquerdo: Guardar e Sair (Botão Único)
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 50 * sf, left: 12 * sf,
|
top: 50 * sf, left: 12 * sf,
|
||||||
child: _buildCornerBtn(
|
child: Column(
|
||||||
heroTag: 'btn_save_exit',
|
children: [
|
||||||
icon: Icons.save_alt,
|
_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); }),
|
||||||
color: const Color(0xFFD92C2C),
|
SizedBox(height: 10 * sf),
|
||||||
size: cornerBtnSize,
|
_buildCornerBtn(heroTag: 'btn_history', icon: Icons.history, color: Colors.blueGrey, size: cornerBtnSize, onTap: () => showDialog(context: context, builder: (ctx) => PlayByPlayDialog(controller: _controller))),
|
||||||
isLoading: _controller.isSaving,
|
],
|
||||||
onTap: () async {
|
|
||||||
// 1. Primeiro obriga a guardar os dados na BD
|
|
||||||
await _controller.saveGameStats(context);
|
|
||||||
|
|
||||||
// 2. Só depois de acabar de guardar é que volta para trás
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Base Esquerda: Banco Casa + TIMEOUT DA CASA
|
Positioned(
|
||||||
|
top: 50 * sf, right: 12 * sf,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildCornerBtn(heroTag: 'btn_heatmap', icon: Icons.local_fire_department, color: Colors.orange.shade800, size: cornerBtnSize, onTap: () => _showHeatmap(context)),
|
||||||
|
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))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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: [
|
||||||
if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false, sf: sf),
|
|
||||||
SizedBox(height: 12 * sf),
|
|
||||||
_buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }),
|
|
||||||
SizedBox(height: 12 * sf),
|
|
||||||
_buildCornerBtn(
|
_buildCornerBtn(
|
||||||
heroTag: 'btn_to_home',
|
heroTag: 'btn_sub_home',
|
||||||
icon: Icons.timer,
|
icon: Icons.swap_horiz,
|
||||||
color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2),
|
color: AppTheme.myTeamBlue,
|
||||||
size: cornerBtnSize,
|
size: cornerBtnSize,
|
||||||
onTap: _controller.myTimeoutsUsed >= 3
|
onTap: () => showDialog(
|
||||||
? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red))
|
context: context,
|
||||||
: () => _controller.useTimeout(false)
|
builder: (ctx) => SubstitutionDialog(
|
||||||
|
controller: _controller,
|
||||||
|
isOpponent: false,
|
||||||
|
sf: 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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Base Direita: Banco Visitante + TIMEOUT DO VISITANTE
|
|
||||||
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: [
|
||||||
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf),
|
|
||||||
SizedBox(height: 12 * sf),
|
|
||||||
_buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }),
|
|
||||||
SizedBox(height: 12 * sf),
|
|
||||||
_buildCornerBtn(
|
_buildCornerBtn(
|
||||||
heroTag: 'btn_to_away',
|
heroTag: 'btn_sub_away',
|
||||||
icon: Icons.timer,
|
icon: Icons.swap_horiz,
|
||||||
color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C),
|
color: AppTheme.oppTeamRed,
|
||||||
size: cornerBtnSize,
|
size: cornerBtnSize,
|
||||||
onTap: _controller.opponentTimeoutsUsed >= 3
|
onTap: () => showDialog(
|
||||||
? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red))
|
context: context,
|
||||||
: () => _controller.useTimeout(true)
|
builder: (ctx) => SubstitutionDialog(
|
||||||
|
controller: _controller,
|
||||||
|
isOpponent: true,
|
||||||
|
sf: 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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 👇 EFEITO VISUAL (Ecrã escurece para mostrar que está a carregar) 👇
|
|
||||||
if (_controller.isSaving)
|
if (_controller.isSaving)
|
||||||
Positioned.fill(
|
Positioned.fill(child: Container(color: Colors.black.withOpacity(0.4), child: const Center(child: CircularProgressIndicator(color: Colors.white)))),
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
|
||||||
import '../controllers/register_controller.dart';
|
import '../controllers/register_controller.dart';
|
||||||
import '../widgets/register_widgets.dart';
|
import '../widgets/register_widgets.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
||||||
@@ -22,11 +23,20 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
// 👇 BLINDADO: Adapta-se automaticamente ao Modo Claro/Escuro
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("Criar Conta", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
title: Text(
|
||||||
backgroundColor: Colors.white,
|
"Criar Conta",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável ao Modo Escuro
|
||||||
|
)
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -40,7 +50,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const RegisterHeader(), // 🔥 Agora sim, usa o Header bonito!
|
const RegisterHeader(),
|
||||||
SizedBox(height: 30 * context.sf),
|
SizedBox(height: 30 * context.sf),
|
||||||
|
|
||||||
RegisterFormFields(controller: _controller),
|
RegisterFormFields(controller: _controller),
|
||||||
|
|||||||
375
lib/pages/excel_export_service.dart
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:excel/excel.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart' hide Border, BorderStyle;
|
||||||
|
|
||||||
|
class ExcelExportService {
|
||||||
|
static Future<void> generateAndPrintBoxScoreExcel({
|
||||||
|
required String gameId,
|
||||||
|
required String myTeam,
|
||||||
|
required String opponentTeam,
|
||||||
|
required String myScore,
|
||||||
|
required String opponentScore,
|
||||||
|
required String season,
|
||||||
|
required String targetTeam,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// ── 1. DADOS DO JOGO ───────────────────────────────────────────────────
|
||||||
|
final gameData = await supabase.from('games').select().eq('id', gameId).maybeSingle();
|
||||||
|
String dateStr = "---";
|
||||||
|
if (gameData != null && gameData['game_date'] != null) {
|
||||||
|
String rawDate = gameData['game_date'].toString();
|
||||||
|
dateStr = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. ESTATÍSTICAS DOS JOGADORES ──────────────────────────────────────
|
||||||
|
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
|
if (statsResponse.isEmpty) return;
|
||||||
|
|
||||||
|
// ── 3. NOMES E NÚMEROS DAS EQUIPAS E JOGADORES ───────────────────────
|
||||||
|
final membersResponse = await supabase.from('members').select('id, name, number');
|
||||||
|
final Map<String, Map<String, dynamic>> memberInfo = {
|
||||||
|
for (var m in membersResponse) m['id'].toString(): m
|
||||||
|
};
|
||||||
|
|
||||||
|
final teamsResponse = await supabase.from('teams').select('id, name');
|
||||||
|
final Map<String, String> teamNames = {
|
||||||
|
for (var t in teamsResponse) t['id'].toString(): t['name'].toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 4. CONFIGURAÇÃO DO EXCEL ───────────────────────────────────────────
|
||||||
|
var excel = Excel.createExcel();
|
||||||
|
String sheetName = 'Estatísticas';
|
||||||
|
Sheet sheet = excel[sheetName];
|
||||||
|
excel.setDefaultSheet(sheetName);
|
||||||
|
if (excel.tables.keys.contains('Sheet1')) excel.delete('Sheet1');
|
||||||
|
|
||||||
|
// ── ESTILOS E CORES PREMIUM ───────────────────────────────────────────
|
||||||
|
final corPrincipal = ExcelColor.fromHexString('#A00000'); // Vermelho escuro
|
||||||
|
final corFundoCinza = ExcelColor.fromHexString('#F5F5F5');
|
||||||
|
final corFundoCinzaEscuro = ExcelColor.fromHexString('#E0E0E0');
|
||||||
|
final cor2P = ExcelColor.fromHexString('#E3F2FD'); // Azul claro
|
||||||
|
final cor3P = ExcelColor.fromHexString('#E8F5E9'); // Verde claro
|
||||||
|
final corGlobal = ExcelColor.fromHexString('#FFF9C4');// Amarelo claro
|
||||||
|
final corLL = ExcelColor.fromHexString('#FFF3E0'); // Laranja claro
|
||||||
|
final corReb = ExcelColor.fromHexString('#F3E5F5'); // Roxo claro
|
||||||
|
final borderGrey = ExcelColor.fromHexString('#BDBDBD');
|
||||||
|
|
||||||
|
CellStyle styleTituloJogo = CellStyle(bold: true, fontSize: 16);
|
||||||
|
CellStyle styleNomeEquipa = CellStyle(bold: true, fontSize: 14, fontColorHex: ExcelColor.white, backgroundColorHex: corPrincipal, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
|
||||||
|
CellStyle styleTituloSecundario = CellStyle(bold: true, fontSize: 12, fontColorHex: ExcelColor.black, backgroundColorHex: corFundoCinzaEscuro, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
|
||||||
|
|
||||||
|
CellStyle styleGrelha(ExcelColor bgCol, {bool isBold = false}) {
|
||||||
|
return CellStyle(
|
||||||
|
bold: isBold, backgroundColorHex: bgCol,
|
||||||
|
horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center,
|
||||||
|
leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final styleGeral = styleGrelha(ExcelColor.white);
|
||||||
|
final styleGeralBold = styleGrelha(ExcelColor.white, isBold: true);
|
||||||
|
final styleNome = CellStyle(horizontalAlign: HorizontalAlign.Left, verticalAlign: VerticalAlign.Center, leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey));
|
||||||
|
|
||||||
|
// ── CABEÇALHO DO JOGO ────────────────────────────────────────────────
|
||||||
|
sheet.cell(CellIndex.indexByString("A1")).value = TextCellValue("JOGO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A1")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B1")).value = TextCellValue("$myTeam vs $opponentTeam");
|
||||||
|
sheet.cell(CellIndex.indexByString("B1")).cellStyle = styleTituloJogo;
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A2")).value = TextCellValue("COMPETIÇÃO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A2")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B2")).value = TextCellValue(season);
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A3")).value = TextCellValue("DATA:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A3")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B3")).value = TextCellValue(dateStr);
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A4")).value = TextCellValue("RESULTADO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A4")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B4")).value = TextCellValue("$myScore - $opponentScore");
|
||||||
|
sheet.cell(CellIndex.indexByString("B4")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
|
||||||
|
|
||||||
|
// ── TOTAIS POR PERÍODO (NOVA SECÇÃO) ─────────────────────────────────
|
||||||
|
sheet.cell(CellIndex.indexByString("A6")).value = TextCellValue("PONTUAÇÃO POR PERÍODO");
|
||||||
|
sheet.cell(CellIndex.indexByString("A6")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
|
||||||
|
|
||||||
|
List<String> periodHeaders = ["EQUIPA", "1º Q", "2º Q", "3º Q", "4º Q", "TOTAL"];
|
||||||
|
for (int i = 0; i < periodHeaders.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 6));
|
||||||
|
cell.value = TextCellValue(periodHeaders[i]);
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linha Minha Equipa
|
||||||
|
List<dynamic> myRow = [
|
||||||
|
myTeam,
|
||||||
|
gameData?['my_q1']?.toString() ?? '-',
|
||||||
|
gameData?['my_q2']?.toString() ?? '-',
|
||||||
|
gameData?['my_q3']?.toString() ?? '-',
|
||||||
|
gameData?['my_q4']?.toString() ?? '-',
|
||||||
|
myScore
|
||||||
|
];
|
||||||
|
for (int i = 0; i < myRow.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 7));
|
||||||
|
cell.value = TextCellValue(myRow[i].toString());
|
||||||
|
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linha Adversário
|
||||||
|
List<dynamic> oppRow = [
|
||||||
|
opponentTeam,
|
||||||
|
gameData?['opp_q1']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q2']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q3']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q4']?.toString() ?? '-',
|
||||||
|
opponentScore
|
||||||
|
];
|
||||||
|
for (int i = 0; i < oppRow.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 8));
|
||||||
|
cell.value = TextCellValue(oppRow[i].toString());
|
||||||
|
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = 11; // 👈 AS TABELAS PRINCIPAIS AGORA COMEÇAM MAIS ABAIXO (Linha 12 no Excel)
|
||||||
|
|
||||||
|
// Agrupar estatísticas por equipa
|
||||||
|
Map<String, List<dynamic>> statsByTeam = {};
|
||||||
|
for(var s in statsResponse) {
|
||||||
|
String tId = s['team_id'].toString();
|
||||||
|
statsByTeam.putIfAbsent(tId, () => []).add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CONSTRUÇÃO DAS TABELAS DE CADA EQUIPA ────────────────────────────
|
||||||
|
for (var entry in statsByTeam.entries) {
|
||||||
|
String tId = entry.key;
|
||||||
|
List<dynamic> tStats = entry.value;
|
||||||
|
String tName = teamNames[tId] ?? "Equipa $tId";
|
||||||
|
|
||||||
|
if (targetTeam != 'Ambas' && tName != targetTeam) continue;
|
||||||
|
|
||||||
|
tStats.sort((a, b) {
|
||||||
|
var mInfoA = memberInfo[a['member_id'].toString()];
|
||||||
|
var mInfoB = memberInfo[b['member_id'].toString()];
|
||||||
|
int numA = int.tryParse(mInfoA?['number']?.toString() ?? '0') ?? 0;
|
||||||
|
int numB = int.tryParse(mInfoB?['number']?.toString() ?? '0') ?? 0;
|
||||||
|
return numA.compareTo(numB);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> processedPlayers = [];
|
||||||
|
int tMin=0, tPts=0, t2m=0, t2a=0, t3m=0, t3a=0, tFgm=0, tFga=0, tftm=0, tfta=0;
|
||||||
|
int torb=0, tdrb=0, tStl=0, tAst=0, tTov=0, tBlk=0, tFls=0;
|
||||||
|
int tSo=0, tIl=0, tLi=0, tPa=0, tTresS=0, tDr=0;
|
||||||
|
|
||||||
|
for(var stat in tStats) {
|
||||||
|
var mInfo = memberInfo[stat['member_id'].toString()];
|
||||||
|
String pNum = mInfo != null ? (mInfo['number']?.toString() ?? "-") : "-";
|
||||||
|
String pName = mInfo != null ? (mInfo['name']?.toString() ?? "Desconhecido") : "Desconhecido";
|
||||||
|
|
||||||
|
int minSecs = stat['minutos_jogados'] ?? 0;
|
||||||
|
int pts = stat['pts'] ?? 0;
|
||||||
|
int p2m = stat['p2m'] ?? 0; int p2a = stat['p2a'] ?? 0;
|
||||||
|
int p3m = stat['p3m'] ?? 0; int p3a = stat['p3a'] ?? 0;
|
||||||
|
int fgm = stat['fgm'] ?? 0; int fga = stat['fga'] ?? 0;
|
||||||
|
int ftm = stat['ftm'] ?? 0; int fta = stat['fta'] ?? 0;
|
||||||
|
int orb = stat['orb'] ?? 0; int drb = stat['drb'] ?? 0; int tr = orb + drb;
|
||||||
|
int stl = stat['stl'] ?? 0; int ast = stat['ast'] ?? 0;
|
||||||
|
int tov = stat['tov'] ?? 0; int blk = stat['blk'] ?? 0; int fls = stat['fls'] ?? 0;
|
||||||
|
int so = stat['so'] ?? 0; int il = stat['il'] ?? 0; int li = stat['li'] ?? 0;
|
||||||
|
int pa = stat['pa'] ?? 0; int tresS = stat['tres_seg'] ?? 0; int dr = stat['dr'] ?? 0;
|
||||||
|
|
||||||
|
tMin+=minSecs; tPts+=pts; t2m+=p2m; t2a+=p2a; t3m+=p3m; t3a+=p3a;
|
||||||
|
tFgm+=fgm; tFga+=fga; tftm+=ftm; tfta+=fta; torb+=orb; tdrb+=drb;
|
||||||
|
tStl+=stl; tAst+=ast; tTov+=tov; tBlk+=blk; tFls+=fls;
|
||||||
|
tSo+=so; tIl+=il; tLi+=li; tPa+=pa; tTresS+=tresS; tDr+=dr;
|
||||||
|
|
||||||
|
processedPlayers.add({
|
||||||
|
'num': pNum, 'name': pName, 'minSecs': minSecs, 'pts': pts,
|
||||||
|
'p2m': p2m, 'p2a': p2a, 'p3m': p3m, 'p3a': p3a, 'fgm': fgm, 'fga': fga,
|
||||||
|
'ftm': ftm, 'fta': fta, 'orb': orb, 'drb': drb, 'tr': tr,
|
||||||
|
'stl': stl, 'ast': ast, 'tov': tov, 'blk': blk, 'fls': fls,
|
||||||
|
'so': so, 'il': il, 'li': li, 'pa': pa, '3s': tresS, 'dr': dr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TABELA 1: LANÇAMENTOS E RESSALTOS
|
||||||
|
var teamStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var teamEnd = CellIndex.indexByColumnRow(columnIndex: 18, rowIndex: r);
|
||||||
|
sheet.merge(teamStart, teamEnd, customValue: TextCellValue("ESTATÍSTICAS DA EQUIPA: ${tName.toUpperCase()} (Lançamentos e Ressaltos)"));
|
||||||
|
for(int i=0; i<=18; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
void criarCategoria(int colStart, int colEnd, String texto, CellStyle estilo) {
|
||||||
|
sheet.merge(CellIndex.indexByColumnRow(columnIndex: colStart, rowIndex: r), CellIndex.indexByColumnRow(columnIndex: colEnd, rowIndex: r), customValue: TextCellValue(texto));
|
||||||
|
for(int i=colStart; i<=colEnd; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = estilo;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleGrelha(corFundoCinza);
|
||||||
|
criarCategoria(4, 6, "2 PONTOS", styleGrelha(cor2P, isBold: true));
|
||||||
|
criarCategoria(7, 9, "3 PONTOS", styleGrelha(cor3P, isBold: true));
|
||||||
|
criarCategoria(10, 12, "GLOBAL", styleGrelha(corGlobal, isBold: true));
|
||||||
|
criarCategoria(13, 15, "L. LIVRES", styleGrelha(corLL, isBold: true));
|
||||||
|
criarCategoria(16, 18, "RESSALTOS", styleGrelha(corReb, isBold: true));
|
||||||
|
r++;
|
||||||
|
|
||||||
|
List<String> colsT1 = ["Nº", "NOME", "MIN", "PTS", "C", "T", "%", "C", "T", "%", "C", "T", "%", "C", "T", "%", "RO", "RD", "TR"];
|
||||||
|
for(int i = 0; i < colsT1.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = TextCellValue(colsT1[i]);
|
||||||
|
|
||||||
|
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
|
||||||
|
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
|
||||||
|
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
|
||||||
|
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
|
||||||
|
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
|
||||||
|
else cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
|
||||||
|
for(var p in processedPlayers) {
|
||||||
|
String minStr = '${p['minSecs'] ~/ 60}:${(p['minSecs'] % 60).toString().padLeft(2, '0')}';
|
||||||
|
String p2Pct = p['p2a'] > 0 ? '${((p['p2m'] / p['p2a']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String p3Pct = p['p3a'] > 0 ? '${((p['p3m'] / p['p3a']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String fgPct = p['fga'] > 0 ? '${((p['fgm'] / p['fga']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String ftPct = p['fta'] > 0 ? '${((p['ftm'] / p['fta']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
|
||||||
|
List<CellValue> rowData = [
|
||||||
|
TextCellValue(p['num']), TextCellValue(p['name']), TextCellValue(minStr), IntCellValue(p['pts']),
|
||||||
|
IntCellValue(p['p2m']), IntCellValue(p['p2a']), TextCellValue(p2Pct),
|
||||||
|
IntCellValue(p['p3m']), IntCellValue(p['p3a']), TextCellValue(p3Pct),
|
||||||
|
IntCellValue(p['fgm']), IntCellValue(p['fga']), TextCellValue(fgPct),
|
||||||
|
IntCellValue(p['ftm']), IntCellValue(p['fta']), TextCellValue(ftPct),
|
||||||
|
IntCellValue(p['orb']), IntCellValue(p['drb']), IntCellValue(p['tr'])
|
||||||
|
];
|
||||||
|
|
||||||
|
for(int i = 0; i < rowData.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = rowData[i];
|
||||||
|
if (i == 1) cell.cellStyle = styleNome;
|
||||||
|
else if (i == 3) cell.cellStyle = styleGeralBold;
|
||||||
|
else cell.cellStyle = styleGeral;
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String t2Pct = t2a > 0 ? '${((t2m / t2a) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String t3Pct = t3a > 0 ? '${((t3m / t3a) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tFgPct = tFga > 0 ? '${((tFgm / tFga) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tftPct = tfta > 0 ? '${((tftm / tfta) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tMinStr = '${tMin ~/ 60}:${(tMin % 60).toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
List<CellValue> totalRowT1 = [
|
||||||
|
TextCellValue(""), TextCellValue("TOTAL EQUIPA"), TextCellValue(tMinStr), IntCellValue(tPts),
|
||||||
|
IntCellValue(t2m), IntCellValue(t2a), TextCellValue(t2Pct),
|
||||||
|
IntCellValue(t3m), IntCellValue(t3a), TextCellValue(t3Pct),
|
||||||
|
IntCellValue(tFgm), IntCellValue(tFga), TextCellValue(tFgPct),
|
||||||
|
IntCellValue(tftm), IntCellValue(tfta), TextCellValue(tftPct),
|
||||||
|
IntCellValue(torb), IntCellValue(tdrb), IntCellValue(torb + tdrb)
|
||||||
|
];
|
||||||
|
|
||||||
|
for(int i = 0; i < totalRowT1.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = totalRowT1[i];
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
|
||||||
|
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
|
||||||
|
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
|
||||||
|
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
|
||||||
|
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
|
||||||
|
}
|
||||||
|
r += 3;
|
||||||
|
|
||||||
|
// TABELA 2: OUTRAS ESTATÍSTICAS
|
||||||
|
var secStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var secEnd = CellIndex.indexByColumnRow(columnIndex: 12, rowIndex: r);
|
||||||
|
sheet.merge(secStart, secEnd, customValue: TextCellValue("OUTRAS ESTATÍSTICAS: ${tName.toUpperCase()}"));
|
||||||
|
for(int i=0; i<=12; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleTituloSecundario;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
List<String> colsT2 = ["Nº", "NOME", "BR", "AS", "BP", "BLK", "FLS", "SO", "IL", "LI", "PA", "3S", "DR"];
|
||||||
|
for(int i = 0; i < colsT2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = TextCellValue(colsT2[i]);
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
|
||||||
|
for(var p in processedPlayers) {
|
||||||
|
List<CellValue> rowData2 = [
|
||||||
|
TextCellValue(p['num']), TextCellValue(p['name']),
|
||||||
|
IntCellValue(p['stl']), IntCellValue(p['ast']), IntCellValue(p['tov']),
|
||||||
|
IntCellValue(p['blk']), IntCellValue(p['fls']), IntCellValue(p['so']),
|
||||||
|
IntCellValue(p['il']), IntCellValue(p['li']), IntCellValue(p['pa']),
|
||||||
|
IntCellValue(p['3s']), IntCellValue(p['dr'])
|
||||||
|
];
|
||||||
|
for(int i = 0; i < rowData2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = rowData2[i];
|
||||||
|
cell.cellStyle = (i == 1) ? styleNome : styleGeral;
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CellValue> totalRowT2 = [
|
||||||
|
TextCellValue(""), TextCellValue("TOTAL EQUIPA"),
|
||||||
|
IntCellValue(tStl), IntCellValue(tAst), IntCellValue(tTov), IntCellValue(tBlk), IntCellValue(tFls),
|
||||||
|
IntCellValue(tSo), IntCellValue(tIl), IntCellValue(tLi), IntCellValue(tPa), IntCellValue(tTresS), IntCellValue(tDr)
|
||||||
|
];
|
||||||
|
for(int i = 0; i < totalRowT2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = totalRowT2[i];
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DESTAQUES DO JOGO ───────────────────────────────────────
|
||||||
|
if (gameData != null) {
|
||||||
|
var startD = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var endD = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
|
||||||
|
sheet.merge(startD, endD, customValue: TextCellValue("DESTAQUES DO JOGO"));
|
||||||
|
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
void adicionarDestaque(String titulo, String valor) {
|
||||||
|
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).value = TextCellValue(titulo);
|
||||||
|
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).cellStyle = CellStyle(bold: true);
|
||||||
|
var sV = CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: r);
|
||||||
|
var eV = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
|
||||||
|
sheet.merge(sV, eV, customValue: TextCellValue(valor));
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
adicionarDestaque("Melhor Marcador:", gameData['top_pts_name'] ?? '---');
|
||||||
|
adicionarDestaque("Melhor Ressaltador:", gameData['top_rbs_name'] ?? '---');
|
||||||
|
adicionarDestaque("Melhor Passador:", gameData['top_ast_name'] ?? '---');
|
||||||
|
adicionarDestaque("MVP da Partida:", gameData['mvp_name'] ?? '---');
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.setColumnWidth(0, 18.0);
|
||||||
|
sheet.setColumnWidth(1, 26.0);
|
||||||
|
sheet.setColumnWidth(2, 8.0);
|
||||||
|
sheet.setColumnWidth(3, 6.0);
|
||||||
|
for(int i=4; i<=18; i++) sheet.setColumnWidth(i, 5.5);
|
||||||
|
|
||||||
|
var fileBytes = excel.save();
|
||||||
|
if (fileBytes != null) {
|
||||||
|
final directory = await getTemporaryDirectory();
|
||||||
|
String safeName = targetTeam == 'Ambas' ? '${myTeam}_vs_${opponentTeam}'.replaceAll(' ', '_') : targetTeam.replaceAll(' ', '_');
|
||||||
|
final filePath = '${directory.path}/BoxScore_$safeName.xlsx';
|
||||||
|
|
||||||
|
File(filePath)..createSync(recursive: true)..writeAsBytesSync(fileBytes);
|
||||||
|
await Share.shareXFiles([XFile(filePath)], text: 'Estatísticas do Jogo: $myTeam vs $opponentTeam');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erro ao gerar Excel: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,85 +1,277 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.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 '../controllers/game_controller.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../models/game_model.dart';
|
import '../controllers/game_controller.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM!
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
|
import 'pdf_export_service.dart';
|
||||||
|
import 'excel_export_service.dart';
|
||||||
|
|
||||||
// --- CARD DE EXIBIÇÃO DO JOGO ---
|
|
||||||
class GameResultCard extends StatelessWidget {
|
class GameResultCard extends StatelessWidget {
|
||||||
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
||||||
final String? myTeamLogo, opponentTeamLogo;
|
final String? myTeamLogo, opponentTeamLogo;
|
||||||
|
final double sf;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
const GameResultCard({
|
const GameResultCard({
|
||||||
super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
|
super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
|
||||||
required this.myScore, required this.opponentScore, required this.status, required this.season,
|
required this.myScore, required this.opponentScore, required this.status, required this.season,
|
||||||
this.myTeamLogo, this.opponentTeamLogo,
|
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _showTeamSelectionDialog(BuildContext context, String format) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
|
||||||
|
title: Text('Gerar ${format.toUpperCase()}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
content: Text('De qual equipa pretende exportar as estatísticas?', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, myTeam);
|
||||||
|
},
|
||||||
|
child: Text(myTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, opponentTeam);
|
||||||
|
},
|
||||||
|
child: Text(opponentTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, 'Ambas');
|
||||||
|
},
|
||||||
|
child: Text('Ambas', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportDocument(BuildContext context, String format, String targetTeam) async {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('A gerar ${format.toUpperCase()}...'), duration: const Duration(seconds: 1)));
|
||||||
|
|
||||||
|
if (format == 'pdf') {
|
||||||
|
await PdfExportService.generateAndPrintBoxScore(
|
||||||
|
gameId: gameId,
|
||||||
|
myTeam: myTeam,
|
||||||
|
opponentTeam: opponentTeam,
|
||||||
|
myScore: myScore,
|
||||||
|
opponentScore: opponentScore,
|
||||||
|
season: season,
|
||||||
|
targetTeam: targetTeam,
|
||||||
|
);
|
||||||
|
} else if (format == 'excel') {
|
||||||
|
await ExcelExportService.generateAndPrintBoxScoreExcel(
|
||||||
|
gameId: gameId,
|
||||||
|
myTeam: myTeam,
|
||||||
|
opponentTeam: opponentTeam,
|
||||||
|
myScore: myScore,
|
||||||
|
opponentScore: opponentScore,
|
||||||
|
season: season,
|
||||||
|
targetTeam: targetTeam,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(bottom: 16 * context.sf),
|
margin: EdgeInsets.only(bottom: 16 * sf),
|
||||||
padding: EdgeInsets.all(16 * context.sf),
|
padding: EdgeInsets.all(16 * sf),
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]),
|
decoration: BoxDecoration(
|
||||||
child: Row(
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(20 * sf),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)),
|
Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)),
|
||||||
_buildScoreCenter(context, gameId),
|
_buildScoreCenter(context, gameId, sf, textColor),
|
||||||
Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)),
|
Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// 👇 MENU DOS 3 PONTOS (MAIS NÍTIDO E MODERNO)
|
||||||
|
Positioned(
|
||||||
|
top: -12 * sf,
|
||||||
|
right: -12 * sf,
|
||||||
|
child: PopupMenuButton<String>(
|
||||||
|
icon: Icon(Icons.more_vert, color: Colors.grey.shade600, size: 26 * sf), // Ícone um pouco maior
|
||||||
|
splashRadius: 24 * sf,
|
||||||
|
elevation: 8, // Adiciona sombra para não se misturar com o fundo
|
||||||
|
shadowColor: Colors.black45,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * sf)),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
surfaceTintColor: Theme.of(context).colorScheme.surface, // Previne que o material 3 mude a cor
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'pdf' || value == 'excel') {
|
||||||
|
_showTeamSelectionDialog(context, value);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
_showDeleteConfirmation(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'pdf',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Gerar PDF', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'excel',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.green.shade600.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.table_chart, color: Colors.green.shade600, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Gerar Excel', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(height: 1),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade500.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.delete_outline, color: Colors.grey.shade700, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Eliminar Jogo', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) {
|
void _showDeleteConfirmation(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
|
||||||
|
title: Text('Eliminar Jogo', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
content: Text('Tem a certeza que deseja eliminar este jogo? Esta ação apagará todas as estatísticas associadas e não pode ser desfeita.', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text('CANCELAR', style: TextStyle(color: Colors.grey, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
onDelete();
|
||||||
|
},
|
||||||
|
child: Text('ELIMINAR', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
|
||||||
|
final double avatarSize = 48 * sf;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null),
|
ClipOval(
|
||||||
SizedBox(height: 6 * context.sf),
|
child: Container(
|
||||||
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: logoUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Center(child: Icon(Icons.shield, color: color, size: 24 * sf)),
|
||||||
|
errorWidget: (context, url, error) => Center(child: Icon(Icons.shield, color: color, size: 24 * sf)),
|
||||||
|
)
|
||||||
|
: Center(child: Icon(Icons.shield, color: color, size: 24 * sf)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6 * sf),
|
||||||
|
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScoreCenter(BuildContext context, String id) {
|
Widget _buildScoreCenter(BuildContext context, String id, double sf, Color textColor) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_scoreBox(context, myScore, Colors.green),
|
_scoreBox(myScore, AppTheme.successGreen, sf),
|
||||||
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)),
|
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)),
|
||||||
_scoreBox(context, opponentScore, Colors.grey),
|
_scoreBox(opponentScore, Colors.grey, sf),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 10 * context.sf),
|
SizedBox(height: 10 * sf),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
|
||||||
icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)),
|
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed),
|
||||||
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
|
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)),
|
||||||
style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact),
|
style: TextButton.styleFrom(backgroundColor: AppTheme.primaryRed.withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), visualDensity: VisualDensity.compact),
|
||||||
),
|
),
|
||||||
SizedBox(height: 6 * context.sf),
|
SizedBox(height: 6 * sf),
|
||||||
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)),
|
Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _scoreBox(BuildContext context, String pts, Color c) => Container(
|
Widget _scoreBox(String pts, Color c, double sf) => Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf),
|
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
|
||||||
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)),
|
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
|
||||||
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)),
|
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- POPUP DE CRIAÇÃO ---
|
|
||||||
class CreateGameDialogManual extends StatefulWidget {
|
class CreateGameDialogManual extends StatefulWidget {
|
||||||
final TeamController teamController;
|
final TeamController teamController;
|
||||||
final GameController gameController;
|
final GameController gameController;
|
||||||
|
final double sf;
|
||||||
|
|
||||||
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController});
|
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
|
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
|
||||||
@@ -105,24 +297,29 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
|
||||||
|
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))),
|
TextField(
|
||||||
SizedBox(height: 15 * context.sf),
|
controller: _seasonController,
|
||||||
|
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf))
|
||||||
|
),
|
||||||
|
SizedBox(height: 15 * widget.sf),
|
||||||
_buildSearch(context, "Minha Equipa", _myTeamController),
|
_buildSearch(context, "Minha Equipa", _myTeamController),
|
||||||
Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))),
|
Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))),
|
||||||
_buildSearch(context, "Adversário", _opponentController),
|
_buildSearch(context, "Adversário", _opponentController),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))),
|
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.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 {
|
||||||
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
|
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
@@ -134,7 +331,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: _isLoading ? SizedBox(width: 20 * context.sf, height: 20 * context.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
|
child: _isLoading ? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -156,9 +353,10 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf),
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
elevation: 4.0, borderRadius: BorderRadius.circular(8 * widget.sf),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
|
constraints: BoxConstraints(maxHeight: 250 * widget.sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
|
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
@@ -166,8 +364,23 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
final String name = option['name'].toString();
|
final String name = option['name'].toString();
|
||||||
final String? imageUrl = option['image_url'];
|
final String? imageUrl = option['image_url'];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null),
|
leading: ClipOval(
|
||||||
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
|
child: Container(
|
||||||
|
width: 40 * widget.sf,
|
||||||
|
height: 40 * widget.sf,
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
child: (imageUrl != null && imageUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
onTap: () { onSelected(option); },
|
onTap: () { onSelected(option); },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -180,8 +393,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
|
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
|
||||||
txtCtrl.addListener(() { controller.text = txtCtrl.text; });
|
txtCtrl.addListener(() { controller.text = txtCtrl.text; });
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf),
|
controller: txtCtrl, focusNode: node,
|
||||||
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()),
|
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * widget.sf), prefixIcon: Icon(Icons.search, size: 20 * widget.sf, color: AppTheme.primaryRed)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -190,7 +404,6 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PÁGINA PRINCIPAL DOS JOGOS ---
|
|
||||||
class GamePage extends StatefulWidget {
|
class GamePage extends StatefulWidget {
|
||||||
const GamePage({super.key});
|
const GamePage({super.key});
|
||||||
|
|
||||||
@@ -209,16 +422,16 @@ class _GamePageState extends State<GamePage> {
|
|||||||
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
|
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
|
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(right: 8.0 * context.sf),
|
padding: EdgeInsets.only(right: 8.0 * context.sf),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf),
|
icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface, size: 26 * context.sf),
|
||||||
onPressed: () => _showFilterPopup(context),
|
onPressed: () => _showFilterPopup(context),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -232,9 +445,9 @@ class _GamePageState extends State<GamePage> {
|
|||||||
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
|
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
|
||||||
builder: (context, gameSnapshot) {
|
builder: (context, gameSnapshot) {
|
||||||
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator());
|
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator());
|
||||||
if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf)));
|
if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
|
||||||
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
|
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
|
||||||
return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.shade300), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey.shade600))]));
|
return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.withOpacity(0.3)), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey))]));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: EdgeInsets.all(16 * context.sf),
|
padding: EdgeInsets.all(16 * context.sf),
|
||||||
@@ -247,8 +460,31 @@ class _GamePageState extends State<GamePage> {
|
|||||||
if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
|
if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
|
||||||
}
|
}
|
||||||
return GameResultCard(
|
return GameResultCard(
|
||||||
gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore,
|
gameId: game.id,
|
||||||
opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
|
myTeam: game.myTeam,
|
||||||
|
opponentTeam: game.opponentTeam,
|
||||||
|
myScore: game.myScore,
|
||||||
|
opponentScore: game.opponentScore,
|
||||||
|
status: game.status,
|
||||||
|
season: game.season,
|
||||||
|
myTeamLogo: myLogo,
|
||||||
|
opponentTeamLogo: oppLogo,
|
||||||
|
sf: context.sf,
|
||||||
|
onDelete: () async {
|
||||||
|
bool success = await gameController.deleteGame(game.id);
|
||||||
|
if (context.mounted) {
|
||||||
|
if (success) {
|
||||||
|
setState(() {});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Jogo eliminado com sucesso!'), backgroundColor: Colors.green)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Erro ao eliminar o jogo.'), backgroundColor: Colors.red)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -257,10 +493,10 @@ class _GamePageState extends State<GamePage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
heroTag: 'add_game_btn', // 👇 A MÁGICA ESTÁ AQUI TAMBÉM!
|
heroTag: 'add_game_btn',
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
backgroundColor: AppTheme.primaryRed,
|
||||||
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
|
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
|
||||||
onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)),
|
onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController, sf: context.sf)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -274,34 +510,36 @@ class _GamePageState extends State<GamePage> {
|
|||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setPopupState) {
|
builder: (context, setPopupState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
|
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
|
IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
|
Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
|
||||||
SizedBox(height: 6 * context.sf),
|
SizedBox(height: 6 * context.sf),
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
|
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
|
dropdownColor: Theme.of(context).colorScheme.surface,
|
||||||
|
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
|
||||||
items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
|
items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
|
||||||
onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
|
onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20 * context.sf),
|
SizedBox(height: 20 * context.sf),
|
||||||
Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
|
Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
|
||||||
SizedBox(height: 6 * context.sf),
|
SizedBox(height: 6 * context.sf),
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
|
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||||
child: StreamBuilder<List<Map<String, dynamic>>>(
|
child: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: teamController.teamsStream,
|
stream: teamController.teamsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@@ -310,7 +548,8 @@ class _GamePageState extends State<GamePage> {
|
|||||||
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
|
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
|
||||||
return DropdownButtonHideUnderline(
|
return DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
|
dropdownColor: Theme.of(context).colorScheme.surface,
|
||||||
|
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
|
||||||
items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(),
|
items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(),
|
||||||
onChanged: (newValue) => setPopupState(() => tempTeam = newValue!),
|
onChanged: (newValue) => setPopupState(() => tempTeam = newValue!),
|
||||||
),
|
),
|
||||||
@@ -322,7 +561,7 @@ class _GamePageState extends State<GamePage> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))),
|
TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))),
|
||||||
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))),
|
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/classe/home.config.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
||||||
import 'package:playmaker/pages/gamePage.dart';
|
import 'package:playmaker/pages/gamePage.dart';
|
||||||
import 'package:playmaker/pages/teamPage.dart';
|
import 'package:playmaker/pages/teamPage.dart';
|
||||||
import 'package:playmaker/controllers/team_controller.dart';
|
import 'package:playmaker/controllers/team_controller.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/pages/status_page.dart';
|
import 'package:playmaker/pages/status_page.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
|
import 'settings_screen.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@@ -21,6 +23,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
final TeamController _teamController = TeamController();
|
final TeamController _teamController = TeamController();
|
||||||
String? _selectedTeamId;
|
String? _selectedTeamId;
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
String? _selectedTeamLogo;
|
||||||
|
|
||||||
int _teamWins = 0;
|
int _teamWins = 0;
|
||||||
int _teamLosses = 0;
|
int _teamLosses = 0;
|
||||||
@@ -28,41 +31,171 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
String? _avatarUrl;
|
||||||
|
bool _isMemoryLoaded = false;
|
||||||
|
|
||||||
|
// A chave mágica para forçar a StatusPage a atualizar
|
||||||
|
String _statusKey = 'status_page_inicial';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadUserAvatar();
|
||||||
|
_loadSelectedTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSelectedTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
if (savedId != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = savedId;
|
||||||
|
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
|
||||||
|
_selectedTeamLogo = prefs.getString('last_team_logo');
|
||||||
|
_teamWins = prefs.getInt('last_team_wins') ?? 0;
|
||||||
|
_teamLosses = prefs.getInt('last_team_losses') ?? 0;
|
||||||
|
_teamDraws = prefs.getInt('last_team_draws') ?? 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profile = await _supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
|
||||||
|
|
||||||
|
if (profile != null && profile['selected_team_id'] != null) {
|
||||||
|
final dbTeamId = profile['selected_team_id'].toString();
|
||||||
|
final teamData = await _supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
|
||||||
|
|
||||||
|
if (teamData != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = teamData['id'].toString();
|
||||||
|
_selectedTeamName = teamData['name'] ?? 'Desconhecido';
|
||||||
|
_selectedTeamLogo = teamData['image_url'];
|
||||||
|
_teamWins = int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0;
|
||||||
|
_teamLosses = int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0;
|
||||||
|
_teamDraws = int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0;
|
||||||
|
});
|
||||||
|
await _saveToSharedPreferences();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSelectedTeam() async {
|
||||||
|
await _saveToSharedPreferences();
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null && _selectedTeamId != null) {
|
||||||
|
try {
|
||||||
|
await _supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': _selectedTeamId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveToSharedPreferences() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_selectedTeamId != null) {
|
||||||
|
await prefs.setString('last_team_id', _selectedTeamId!);
|
||||||
|
await prefs.setString('last_team_name', _selectedTeamName);
|
||||||
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', _selectedTeamLogo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', _teamWins);
|
||||||
|
await prefs.setInt('last_team_losses', _teamLosses);
|
||||||
|
await prefs.setInt('last_team_draws', _teamDraws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUserAvatar() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedUrl = prefs.getString('meu_avatar_guardado');
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
if (savedUrl != null) _avatarUrl = savedUrl;
|
||||||
|
_isMemoryLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data = await _supabase.from('profiles').select('avatar_url').eq('id', userId).maybeSingle();
|
||||||
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
|
final urlDoSupabase = data['avatar_url'];
|
||||||
|
if (urlDoSupabase != savedUrl) {
|
||||||
|
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||||
|
setState(() { _avatarUrl = urlDoSupabase; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar avatar na Home: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Já não precisamos calcular o sf aqui!
|
|
||||||
|
|
||||||
final List<Widget> pages = [
|
final List<Widget> pages = [
|
||||||
_buildHomeContent(context), // Passamos só o context
|
_buildHomeContent(context),
|
||||||
const GamePage(),
|
const GamePage(),
|
||||||
const TeamsPage(),
|
const TeamsPage(),
|
||||||
const StatusPage(),
|
StatusPage(key: ValueKey(_statusKey)), // A StatusPage recarrega sempre que a chave muda!
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
|
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold)),
|
||||||
backgroundColor: HomeConfig.primaryColor,
|
backgroundColor: AppTheme.primaryRed,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
leading: IconButton(
|
elevation: 0,
|
||||||
icon: Icon(Icons.person, size: 24 * context.sf),
|
leading: Padding(
|
||||||
onPressed: () {},
|
padding: EdgeInsets.all(10.0 * context.sf),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(100),
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
|
||||||
|
_loadUserAvatar();
|
||||||
|
},
|
||||||
|
child: !_isMemoryLoaded
|
||||||
|
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
|
||||||
|
: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: _avatarUrl!,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
imageBuilder: (context, imageProvider) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), backgroundImage: imageProvider),
|
||||||
|
placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)),
|
||||||
|
errorWidget: (context, url, error) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)),
|
||||||
|
)
|
||||||
|
: CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
body: IndexedStack(
|
|
||||||
index: _selectedIndex,
|
|
||||||
children: pages,
|
|
||||||
),
|
),
|
||||||
|
body: IndexedStack(index: _selectedIndex, children: pages),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
onDestinationSelected: (index) {
|
||||||
|
setState(() => _selectedIndex = index);
|
||||||
|
if (index == 0) {
|
||||||
|
_loadSelectedTeam();
|
||||||
|
}
|
||||||
|
},
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
// O math.min não é necessário se já tens o sf. Mas podes usar context.sf
|
|
||||||
height: 70 * (context.sf < 1.2 ? context.sf : 1.2),
|
height: 70 * (context.sf < 1.2 ? context.sf : 1.2),
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
|
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
|
||||||
@@ -77,13 +210,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
void _showTeamSelector(BuildContext context) {
|
void _showTeamSelector(BuildContext context) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))),
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _teamController.teamsStream,
|
stream: _teamController.teamsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
||||||
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: const Center(child: Text("Nenhuma equipa criada.")));
|
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
|
||||||
|
|
||||||
final teams = snapshot.data!;
|
final teams = snapshot.data!;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@@ -91,17 +225,33 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
itemCount: teams.length,
|
itemCount: teams.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = teams[index];
|
final team = teams[index];
|
||||||
|
final String? logoUrl = team['image_url'];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(team['name']),
|
leading: ClipOval(
|
||||||
onTap: () {
|
child: Container(
|
||||||
|
width: 36 * context.sf, height: 36 * context.sf, color: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(imageUrl: logoUrl, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf))
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedTeamId = team['id'];
|
_selectedTeamId = team['id'].toString();
|
||||||
_selectedTeamName = team['name'];
|
_selectedTeamName = team['name'] ?? 'Desconhecido';
|
||||||
_teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0;
|
_selectedTeamLogo = logoUrl;
|
||||||
_teamLosses = team['losses'] != null ? int.tryParse(team['losses'].toString()) ?? 0 : 0;
|
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
|
||||||
_teamDraws = team['draws'] != null ? int.tryParse(team['draws'].toString()) ?? 0 : 0;
|
_teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0;
|
||||||
|
_teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0;
|
||||||
|
|
||||||
|
// Dizemos à StatusPage que a equipa mudou alterando a chave!
|
||||||
|
_statusKey = DateTime.now().toString();
|
||||||
});
|
});
|
||||||
Navigator.pop(context);
|
|
||||||
|
await _saveSelectedTeam();
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -115,6 +265,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Widget _buildHomeContent(BuildContext context) {
|
Widget _buildHomeContent(BuildContext context) {
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
final double wScreen = MediaQuery.of(context).size.width;
|
||||||
final double cardHeight = wScreen * 0.5;
|
final double cardHeight = wScreen * 0.5;
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _selectedTeamId != null
|
stream: _selectedTeamId != null
|
||||||
@@ -133,12 +284,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
onTap: () => _showTeamSelector(context),
|
onTap: () => _showTeamSelector(context),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(12 * context.sf),
|
padding: EdgeInsets.all(12 * context.sf),
|
||||||
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300)),
|
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * context.sf), SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))]),
|
Row(children: [
|
||||||
const Icon(Icons.arrow_drop_down),
|
(_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
|
||||||
|
? ClipOval(child: CachedNetworkImage(imageUrl: _selectedTeamLogo!, width: 24 * context.sf, height: 24 * context.sf, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf)))
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
||||||
|
]),
|
||||||
|
Icon(Icons.arrow_drop_down, color: textColor),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -149,9 +306,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
|
Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statPtsBg, isHighlighted: true)),
|
||||||
SizedBox(width: 12 * context.sf),
|
SizedBox(width: 12 * context.sf),
|
||||||
Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
|
Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statAstBg)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -161,40 +318,37 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
|
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)),
|
||||||
SizedBox(width: 12 * context.sf),
|
SizedBox(width: 12 * context.sf),
|
||||||
Expanded(
|
Expanded(child: PieChartCard(victories: _teamWins, defeats: _teamLosses, draws: _teamDraws, title: 'DESEMPENHO', subtitle: 'Temporada', backgroundColor: AppTheme.statPieBg, sf: context.sf)),
|
||||||
child: PieChartCard(
|
|
||||||
victories: _teamWins,
|
|
||||||
defeats: _teamLosses,
|
|
||||||
draws: _teamDraws,
|
|
||||||
title: 'DESEMPENHO',
|
|
||||||
subtitle: 'Temporada',
|
|
||||||
backgroundColor: const Color(0xFFC62828),
|
|
||||||
sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 40 * context.sf),
|
SizedBox(height: 40 * context.sf),
|
||||||
|
|
||||||
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
|
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
SizedBox(height: 16 * context.sf),
|
SizedBox(height: 16 * context.sf),
|
||||||
|
|
||||||
_selectedTeamName == "Selecionar Equipa"
|
_selectedTeamName == "Selecionar Equipa"
|
||||||
? Container(
|
? Container(
|
||||||
padding: EdgeInsets.all(20 * context.sf),
|
width: double.infinity, padding: EdgeInsets.all(24.0 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color ?? Colors.white, borderRadius: BorderRadius.circular(16 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))]),
|
||||||
alignment: Alignment.center,
|
child: Column(
|
||||||
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)),
|
children: [
|
||||||
|
Container(padding: EdgeInsets.all(18 * context.sf), decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle), child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf)),
|
||||||
|
SizedBox(height: 20 * context.sf),
|
||||||
|
Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
|
SizedBox(height: 8 * context.sf),
|
||||||
|
Text("Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", textAlign: TextAlign.center, style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4)),
|
||||||
|
SizedBox(height: 24 * context.sf),
|
||||||
|
SizedBox(width: double.infinity, height: 48 * context.sf, child: ElevatedButton.icon(onPressed: () => _showTeamSelector(context), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), icon: Icon(Icons.touch_app, size: 20 * context.sf), label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)))),
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: StreamBuilder<List<Map<String, dynamic>>>(
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _supabase.from('games').stream(primaryKey: ['id'])
|
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
|
||||||
.order('game_date', ascending: false),
|
|
||||||
builder: (context, gameSnapshot) {
|
builder: (context, gameSnapshot) {
|
||||||
|
|
||||||
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
||||||
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||||
|
|
||||||
final todosOsJogos = gameSnapshot.data ?? [];
|
final todosOsJogos = gameSnapshot.data ?? [];
|
||||||
final gamesList = todosOsJogos.where((game) {
|
final gamesList = todosOsJogos.where((game) {
|
||||||
@@ -204,55 +358,23 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
|
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
|
||||||
}).take(3).toList();
|
}).take(3).toList();
|
||||||
|
|
||||||
if (gamesList.isEmpty) {
|
if (gamesList.isEmpty) return Container(width: double.infinity, padding: EdgeInsets.all(20 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)), alignment: Alignment.center, child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)));
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.all(20 * context.sf),
|
|
||||||
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: gamesList.map((game) {
|
children: gamesList.map((game) {
|
||||||
String dbMyTeam = game['my_team']?.toString() ?? '';
|
String dbMyTeam = game['my_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? '';
|
||||||
String dbOppTeam = game['opponent_team']?.toString() ?? '';
|
int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0;
|
||||||
int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0;
|
|
||||||
int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0;
|
|
||||||
|
|
||||||
String opponent; int myScore; int oppScore;
|
String opponent; int myScore; int oppScore;
|
||||||
|
|
||||||
if (dbMyTeam == _selectedTeamName) {
|
if (dbMyTeam == _selectedTeamName) { opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; }
|
||||||
opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore;
|
String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||||
} else {
|
String result = myScore > oppScore ? 'V' : (myScore < oppScore ? 'D' : 'E');
|
||||||
opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
String rawDate = game['game_date']?.toString() ?? '---';
|
return _buildGameHistoryCard(context: context, opponent: opponent, result: result, myScore: myScore, oppScore: oppScore, date: date, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---');
|
||||||
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
|
||||||
|
|
||||||
String result = 'E';
|
|
||||||
if (myScore > oppScore) result = 'V';
|
|
||||||
if (myScore < oppScore) result = 'D';
|
|
||||||
|
|
||||||
return _buildGameHistoryCard(
|
|
||||||
context: context, // Usamos o context para o sf
|
|
||||||
opponent: opponent,
|
|
||||||
result: result,
|
|
||||||
myScore: myScore,
|
|
||||||
oppScore: oppScore,
|
|
||||||
date: date,
|
|
||||||
topPts: game['top_pts_name'] ?? '---',
|
|
||||||
topAst: game['top_ast_name'] ?? '---',
|
|
||||||
topRbs: game['top_rbs_name'] ?? '---',
|
|
||||||
topDef: game['top_def_name'] ?? '---',
|
|
||||||
mvp: game['mvp_name'] ?? '---',
|
|
||||||
);
|
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 20 * context.sf),
|
SizedBox(height: 20 * context.sf),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -265,29 +387,26 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
||||||
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
|
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
|
||||||
for (var row in data) {
|
for (var row in data) {
|
||||||
String pid = row['member_id'].toString();
|
String pid = row['member_id']?.toString() ?? "unknown";
|
||||||
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
||||||
ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0);
|
ptsMap[pid] = (ptsMap[pid] ?? 0) + (int.tryParse(row['pts']?.toString() ?? '0') ?? 0);
|
||||||
astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0);
|
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0);
|
||||||
rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0);
|
rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0);
|
||||||
}
|
}
|
||||||
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
||||||
String getBest(Map<String, int> map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; }
|
String getBest(Map<String, int> map) { if (map.isEmpty) return '---'; return namesMap[map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? '---'; }
|
||||||
int getBestVal(Map<String, int> map) => map.values.reduce((a, b) => a > b ? a : b);
|
int getBestVal(Map<String, int> map) { if (map.isEmpty) return 0; return map.values.reduce((a, b) => a > b ? a : b); }
|
||||||
return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)};
|
return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)};
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
|
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4, margin: EdgeInsets.zero,
|
elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final double ch = constraints.maxHeight;
|
final double ch = constraints.maxHeight; final double cw = constraints.maxWidth;
|
||||||
final double cw = constraints.maxWidth;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.all(cw * 0.06),
|
padding: EdgeInsets.all(cw * 0.06),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -295,23 +414,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
SizedBox(height: ch * 0.011),
|
SizedBox(height: ch * 0.011),
|
||||||
SizedBox(
|
SizedBox(width: double.infinity, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)))),
|
||||||
width: double.infinity,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown, alignment: Alignment.centerLeft,
|
|
||||||
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))),
|
Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))),
|
||||||
SizedBox(height: ch * 0.015),
|
SizedBox(height: ch * 0.015),
|
||||||
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
|
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Container(
|
Container(width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))),
|
||||||
width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035),
|
|
||||||
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)),
|
|
||||||
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -321,31 +430,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGameHistoryCard({
|
Widget _buildGameHistoryCard({required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp}) {
|
||||||
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date,
|
bool isWin = result == 'V'; bool isDraw = result == 'E';
|
||||||
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
|
Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
|
||||||
}) {
|
final bgColor = Theme.of(context).cardTheme.color; final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
bool isWin = result == 'V';
|
|
||||||
bool isDraw = result == 'E';
|
|
||||||
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(bottom: 14 * context.sf),
|
margin: EdgeInsets.only(bottom: 14 * context.sf), decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))]),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white, borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(14 * context.sf),
|
padding: EdgeInsets.all(14 * context.sf),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(width: 36 * context.sf, height: 36 * context.sf, decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)))),
|
||||||
width: 36 * context.sf, height: 36 * context.sf,
|
|
||||||
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle),
|
|
||||||
child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))),
|
|
||||||
),
|
|
||||||
SizedBox(width: 14 * context.sf),
|
SizedBox(width: 14 * context.sf),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -356,16 +454,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
Padding(
|
Padding(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), child: Container(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)))),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
|
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
|
||||||
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)),
|
|
||||||
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -374,32 +465,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5),
|
Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
|
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
|
||||||
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(children: [Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef))]),
|
||||||
children: [
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 8 * context.sf),
|
SizedBox(height: 8 * context.sf),
|
||||||
Row(
|
Row(children: [Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs))]),
|
||||||
children: [
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 8 * context.sf),
|
SizedBox(height: 8 * context.sf),
|
||||||
Row(
|
Row(children: [Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), const Expanded(child: SizedBox())]),
|
||||||
children: [
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
|
|
||||||
const Expanded(child: SizedBox()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -411,20 +486,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
|
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14 * context.sf, color: color),
|
Icon(icon, size: 14 * context.sf, color: color), SizedBox(width: 4 * context.sf),
|
||||||
SizedBox(width: 4 * context.sf),
|
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
|
||||||
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
|
Expanded(child: Text(value, style: TextStyle(fontSize: 11 * context.sf, color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11 * context.sf,
|
|
||||||
color: isMvp ? Colors.amber.shade900 : Colors.black87,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
),
|
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/controllers/login_controller.dart';
|
import 'package:playmaker/controllers/login_controller.dart';
|
||||||
import '../widgets/login_widgets.dart';
|
import '../widgets/login_widgets.dart';
|
||||||
import 'home.dart'; // <--- IMPORTANTE: Importa a tua HomeScreen
|
import 'home.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
@@ -23,7 +23,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
// 👇 Adaptável ao Modo Claro/Escuro do Flutter
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListenableBuilder(
|
child: ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
@@ -32,7 +33,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
// Garante que o form não fica gigante num tablet
|
|
||||||
constraints: BoxConstraints(maxWidth: 450 * context.sf),
|
constraints: BoxConstraints(maxWidth: 450 * context.sf),
|
||||||
padding: EdgeInsets.all(32 * context.sf),
|
padding: EdgeInsets.all(32 * context.sf),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
731
lib/pages/pdf_export_service.dart
Normal file
@@ -0,0 +1,731 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
import 'package:printing/printing.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
class _ShotDot {
|
||||||
|
final double relX;
|
||||||
|
final double relY;
|
||||||
|
final bool isMake;
|
||||||
|
_ShotDot({required this.relX, required this.relY, required this.isMake});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PdfExportService {
|
||||||
|
static Future<void> generateAndPrintBoxScore({
|
||||||
|
required String gameId,
|
||||||
|
required String myTeam,
|
||||||
|
required String opponentTeam,
|
||||||
|
required String myScore,
|
||||||
|
required String opponentScore,
|
||||||
|
required String season,
|
||||||
|
required String targetTeam,
|
||||||
|
}) async {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// ── Jogo ────────────────────────────────────────────────────────────────
|
||||||
|
final gameData = await supabase.from('games').select().eq('id', gameId).single();
|
||||||
|
|
||||||
|
// ── Equipas ─────────────────────────────────────────────────────────────
|
||||||
|
final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
||||||
|
|
||||||
|
String? myTeamId;
|
||||||
|
for (var t in teamsData) {
|
||||||
|
if (t['name'] == myTeam) myTeamId = t['id'].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
||||||
|
List<dynamic> myPlayers = myTeamId != null
|
||||||
|
? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador')
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// ── Estatísticas ─────────────────────────────────────────────────────────
|
||||||
|
final statsData = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
|
Map<String, Map<String, dynamic>> statsMap = {};
|
||||||
|
for (var s in statsData) {
|
||||||
|
statsMap[s['member_id'].toString()] = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tiros ──────────────────────
|
||||||
|
final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId);
|
||||||
|
final Set<String> myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet();
|
||||||
|
final List<_ShotDot> myTeamShots = [];
|
||||||
|
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
||||||
|
|
||||||
|
for (var shot in shotsData) {
|
||||||
|
final memberId = shot['member_id'].toString();
|
||||||
|
if (!myPlayerIds.contains(memberId)) continue;
|
||||||
|
|
||||||
|
final dot = _ShotDot(
|
||||||
|
relX: double.tryParse(shot['relative_x'].toString()) ?? 0.5,
|
||||||
|
relY: double.tryParse(shot['relative_y'].toString()) ?? 0.5,
|
||||||
|
isMake: shot['is_make'] == true,
|
||||||
|
);
|
||||||
|
myTeamShots.add(dot);
|
||||||
|
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tabela de estatísticas ────────────────────
|
||||||
|
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONSTRUÇÃO DO PDF
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
final pdf = pw.Document();
|
||||||
|
|
||||||
|
pdf.addPage(
|
||||||
|
pw.Page(
|
||||||
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
|
margin: const pw.EdgeInsets.all(14),
|
||||||
|
build: (pw.Context context) {
|
||||||
|
return pw.FittedBox(
|
||||||
|
fit: pw.BoxFit.scaleDown,
|
||||||
|
child: pw.Container(
|
||||||
|
width: PdfPageFormat.a4.landscape.availableWidth,
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text('Relatório Estatístico', style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
|
pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)),
|
||||||
|
pw.Text('Resultado: $myScore — $opponentScore', style: const pw.TextStyle(fontSize: 13)),
|
||||||
|
pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
|
||||||
|
// 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇
|
||||||
|
pw.Table.fromTextArray(
|
||||||
|
context: context,
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||||
|
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8),
|
||||||
|
cellStyle: const pw.TextStyle(fontSize: 8),
|
||||||
|
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||||
|
cellAlignment: pw.Alignment.center,
|
||||||
|
data: <List<String>>[
|
||||||
|
['Equipa', '1ºQ', '2ºQ', '3ºQ', '4ºQ', 'F'],
|
||||||
|
[
|
||||||
|
myTeam,
|
||||||
|
gameData['my_q1']?.toString() ?? '-',
|
||||||
|
gameData['my_q2']?.toString() ?? '-',
|
||||||
|
gameData['my_q3']?.toString() ?? '-',
|
||||||
|
gameData['my_q4']?.toString() ?? '-',
|
||||||
|
myScore
|
||||||
|
],
|
||||||
|
[
|
||||||
|
opponentTeam,
|
||||||
|
gameData['opp_q1']?.toString() ?? '-',
|
||||||
|
gameData['opp_q2']?.toString() ?? '-',
|
||||||
|
gameData['opp_q3']?.toString() ?? '-',
|
||||||
|
gameData['opp_q4']?.toString() ?? '-',
|
||||||
|
opponentScore
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
|
||||||
|
pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||||
|
pw.SizedBox(height: 2),
|
||||||
|
_buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||||
|
|
||||||
|
pw.SizedBox(height: 14),
|
||||||
|
|
||||||
|
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||||
|
pw.SizedBox(height: 2),
|
||||||
|
_buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||||
|
|
||||||
|
pw.SizedBox(height: 16),
|
||||||
|
|
||||||
|
pw.Row(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'),
|
||||||
|
pw.SizedBox(width: 10),
|
||||||
|
_buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'),
|
||||||
|
pw.SizedBox(width: 10),
|
||||||
|
_buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'),
|
||||||
|
pw.SizedBox(width: 10),
|
||||||
|
_buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── PÁGINA 2: Mapa de Calor — Equipa completa ────────────────────────
|
||||||
|
if (myTeamShots.isNotEmpty) {
|
||||||
|
pdf.addPage(
|
||||||
|
pw.Page(
|
||||||
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
|
margin: const pw.EdgeInsets.all(20),
|
||||||
|
build: (pw.Context context) {
|
||||||
|
return pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)),
|
||||||
|
pw.SizedBox(height: 12),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Center(
|
||||||
|
child: pw.CustomPaint(
|
||||||
|
size: const PdfPoint(360, 360),
|
||||||
|
painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
_heatmapLegend(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
|
||||||
|
final activePlayers = myPlayers.where((p) {
|
||||||
|
final pid = p['id'].toString();
|
||||||
|
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
for (int i = 0; i < activePlayers.length; i += 4) {
|
||||||
|
final chunk = activePlayers.sublist(i, math.min(i + 4, activePlayers.length));
|
||||||
|
|
||||||
|
pdf.addPage(
|
||||||
|
pw.Page(
|
||||||
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
|
margin: const pw.EdgeInsets.all(20),
|
||||||
|
build: (pw.Context context) {
|
||||||
|
return pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_heatmapPageHeader('MAPAS DE CALOR INDIVIDUAIS', const PdfColor.fromInt(0xFFA00000)),
|
||||||
|
pw.SizedBox(height: 12),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Column(
|
||||||
|
children: [
|
||||||
|
if (chunk.isNotEmpty)
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildPlayerHeatmap(chunk[0], shotsByPlayer[chunk[0]['id'].toString()]!, statsMap[chunk[0]['id'].toString()] ?? {}),
|
||||||
|
pw.SizedBox(width: 40),
|
||||||
|
chunk.length > 1 ? _buildPlayerHeatmap(chunk[1], shotsByPlayer[chunk[1]['id'].toString()]!, statsMap[chunk[1]['id'].toString()] ?? {}) : pw.Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 12),
|
||||||
|
if (chunk.length > 2)
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildPlayerHeatmap(chunk[2], shotsByPlayer[chunk[2]['id'].toString()]!, statsMap[chunk[2]['id'].toString()] ?? {}),
|
||||||
|
pw.SizedBox(width: 40),
|
||||||
|
chunk.length > 3 ? _buildPlayerHeatmap(chunk[3], shotsByPlayer[chunk[3]['id'].toString()]!, statsMap[chunk[3]['id'].toString()] ?? {}) : pw.Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
_heatmapLegend(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Printing.layoutPdf(
|
||||||
|
onLayout: (PdfPageFormat format) async => pdf.save(),
|
||||||
|
name: 'BoxScore_$myTeam.pdf',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
|
||||||
|
final String playerName = player['name']?.toString() ?? 'Jogador';
|
||||||
|
final String playerNumber = player['number']?.toString() ?? '0';
|
||||||
|
|
||||||
|
final int pts = stats['pts'] ?? 0;
|
||||||
|
final int fgm = stats['fgm'] ?? 0;
|
||||||
|
final int fga = stats['fga'] ?? 0;
|
||||||
|
final String fgPct = fga > 0 ? '${((fgm / fga) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
|
||||||
|
return pw.Container(
|
||||||
|
width: 250,
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
color: PdfColors.white,
|
||||||
|
border: pw.Border.all(color: PdfColors.grey300),
|
||||||
|
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
|
||||||
|
),
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
pw.Text('#$playerNumber $playerName', style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 11, color: const PdfColor.fromInt(0xFFA00000))),
|
||||||
|
pw.SizedBox(height: 4),
|
||||||
|
pw.Text('PTS: $pts | FG: $fgm/$fga ($fgPct)', style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700)),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Center(
|
||||||
|
child: pw.CustomPaint(
|
||||||
|
size: const PdfPoint(180, 180),
|
||||||
|
painter: (canvas, size) => _paintCourt(canvas, size, shots),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
|
||||||
|
final double w = size.x;
|
||||||
|
final double h = size.y;
|
||||||
|
final double basketX = w / 2;
|
||||||
|
|
||||||
|
canvas
|
||||||
|
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
|
||||||
|
..drawRect(0, 0, w, h)
|
||||||
|
..fillPath();
|
||||||
|
|
||||||
|
final double margin = w * 0.10;
|
||||||
|
final double length = h * 0.35;
|
||||||
|
final double larguraDoArco = (w / 2) - margin;
|
||||||
|
final double alturaDoArco = larguraDoArco * 0.30;
|
||||||
|
final double totalArcoHeight = alturaDoArco * 4;
|
||||||
|
|
||||||
|
canvas.setStrokeColor(PdfColors.white);
|
||||||
|
canvas.setLineWidth(2.0);
|
||||||
|
|
||||||
|
_drawLine(canvas, h, margin, 0, margin, length);
|
||||||
|
_drawLine(canvas, h, w - margin, 0, w - margin, length);
|
||||||
|
_drawLine(canvas, h, 0, length, margin, length);
|
||||||
|
_drawLine(canvas, h, w - margin, length, w, length);
|
||||||
|
|
||||||
|
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
|
||||||
|
|
||||||
|
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
|
||||||
|
double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75));
|
||||||
|
double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25));
|
||||||
|
double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25));
|
||||||
|
|
||||||
|
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
|
||||||
|
_drawLine(canvas, h, sXR, sYR, w, h * 0.85);
|
||||||
|
|
||||||
|
canvas.setStrokeColor(PdfColors.black);
|
||||||
|
canvas.setLineWidth(1.5);
|
||||||
|
|
||||||
|
final double pW = w * 0.28;
|
||||||
|
final double pH = h * 0.38;
|
||||||
|
|
||||||
|
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
|
||||||
|
|
||||||
|
final double ftR = pW / 2;
|
||||||
|
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
|
||||||
|
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
|
||||||
|
|
||||||
|
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
|
||||||
|
|
||||||
|
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
|
||||||
|
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
|
||||||
|
|
||||||
|
for (final shot in shots) {
|
||||||
|
final double px = shot.relX * w;
|
||||||
|
final double py = shot.relY * h;
|
||||||
|
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
|
||||||
|
|
||||||
|
_fillCircle(canvas, h, px, py, 6, dotColor);
|
||||||
|
canvas.setStrokeColor(PdfColors.white);
|
||||||
|
canvas.setLineWidth(1.2);
|
||||||
|
if (shot.isMake) {
|
||||||
|
_drawLine(canvas, h, px - 3, py + 1.5, px - 0.5, py - 3);
|
||||||
|
_drawLine(canvas, h, px - 0.5, py - 3, px + 4.0, py + 3.5);
|
||||||
|
} else {
|
||||||
|
_drawLine(canvas, h, px - 3, py - 3, px + 3, py + 3);
|
||||||
|
_drawLine(canvas, h, px + 3, py - 3, px - 3, py + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
|
||||||
|
c.moveTo(x1, canvasH - y1);
|
||||||
|
c.lineTo(x2, canvasH - y2);
|
||||||
|
c.strokePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
|
||||||
|
c.drawRect(x, canvasH - (y + height), width, height);
|
||||||
|
c.strokePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _drawCircle(PdfGraphics c, double canvasH, double cx, double cy, double r) {
|
||||||
|
c.drawEllipse(cx, canvasH - cy, r, r);
|
||||||
|
c.strokePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _fillCircle(PdfGraphics c, double canvasH, double cx, double cy, double r, PdfColor color) {
|
||||||
|
c.setFillColor(color);
|
||||||
|
c.drawEllipse(cx, canvasH - cy, r, r);
|
||||||
|
c.fillPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _drawEllipseArc(PdfGraphics c, double canvasH, double cx, double cy, double rx, double ry, double startAngle, double sweepAngle) {
|
||||||
|
const int steps = 30;
|
||||||
|
final double step = sweepAngle / steps;
|
||||||
|
double angle = startAngle;
|
||||||
|
|
||||||
|
double fx = cx + rx * math.cos(angle);
|
||||||
|
double fy = cy + ry * math.sin(angle);
|
||||||
|
c.moveTo(fx, canvasH - fy);
|
||||||
|
|
||||||
|
for (int i = 1; i <= steps; i++) {
|
||||||
|
angle += step;
|
||||||
|
fx = cx + rx * math.cos(angle);
|
||||||
|
fy = cy + ry * math.sin(angle);
|
||||||
|
c.lineTo(fx, canvasH - fy);
|
||||||
|
}
|
||||||
|
c.strokePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<List<String>> _buildTeamTableData(List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
||||||
|
List<List<String>> tableData = [];
|
||||||
|
|
||||||
|
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
|
||||||
|
int tOrb = 0, tDrb = 0, tStl = 0, tAst = 0, tTov = 0, tBlk = 0;
|
||||||
|
int tP3m = 0, tP2m = 0, tP3a = 0, tP2a = 0;
|
||||||
|
int tSo = 0, tIl = 0, tLi = 0, tPa = 0, tTresS = 0, tDr = 0;
|
||||||
|
int tSec = 0;
|
||||||
|
|
||||||
|
players.sort((a, b) {
|
||||||
|
int numA = int.tryParse(a['number']?.toString() ?? '0') ?? 0;
|
||||||
|
int numB = int.tryParse(b['number']?.toString() ?? '0') ?? 0;
|
||||||
|
return numA.compareTo(numB);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var p in players) {
|
||||||
|
String id = p['id'].toString();
|
||||||
|
var s = statsMap[id] ?? {};
|
||||||
|
|
||||||
|
int pts = s['pts'] ?? 0;
|
||||||
|
int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0;
|
||||||
|
int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 0;
|
||||||
|
int p2m = s['p2m'] ?? 0; int p2a = s['p2a'] ?? 0;
|
||||||
|
int p3m = s['p3m'] ?? 0; int p3a = s['p3a'] ?? 0;
|
||||||
|
int fls = s['fls'] ?? 0;
|
||||||
|
int orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0;
|
||||||
|
int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0;
|
||||||
|
int tov = s['tov'] ?? 0; int blk = s['blk'] ?? 0;
|
||||||
|
int so = s['so'] ?? 0; int il = s['il'] ?? 0; int li = s['li'] ?? 0;
|
||||||
|
int pa = s['pa'] ?? 0; int tresS = s['tres_seg'] ?? 0; int dr = s['dr'] ?? 0;
|
||||||
|
int sec = s['minutos_jogados'] ?? 0;
|
||||||
|
|
||||||
|
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
||||||
|
tFls += fls; tOrb += orb; tDrb += drb; tStl += stl;
|
||||||
|
tAst += ast; tTov += tov; tBlk += blk;
|
||||||
|
tP3m += p3m; tP2m += p2m; tP3a += p3a; tP2a += p2a;
|
||||||
|
tSo += so; tIl += il; tLi += li; tPa += pa;
|
||||||
|
tTresS += tresS; tDr += dr; tSec += sec;
|
||||||
|
|
||||||
|
String p2Pct = p2a > 0 ? '${((p2m / p2a) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
String p3Pct = p3a > 0 ? '${((p3m / p3a) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
String fgPct = fga > 0 ? '${((fgm / fga) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
String ftPct = fta > 0 ? '${((ftm / fta) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
|
||||||
|
String minStr = _secToMin(sec);
|
||||||
|
|
||||||
|
tableData.add([
|
||||||
|
p['number']?.toString() ?? '-',
|
||||||
|
p['name']?.toString() ?? '?',
|
||||||
|
minStr, pts.toString(),
|
||||||
|
p2m.toString(), p2a.toString(), p2Pct,
|
||||||
|
p3m.toString(), p3a.toString(), p3Pct,
|
||||||
|
fgm.toString(), fga.toString(), fgPct,
|
||||||
|
ftm.toString(), fta.toString(), ftPct,
|
||||||
|
orb.toString(), drb.toString(), (orb + drb).toString(),
|
||||||
|
stl.toString(), ast.toString(), tov.toString(), blk.toString(), fls.toString(),
|
||||||
|
so.toString(), il.toString(), li.toString(),
|
||||||
|
pa.toString(), tresS.toString(), dr.toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableData.isEmpty) {
|
||||||
|
tableData.add(List.filled(30, '0')..[0] = '-'..[1] = 'Sem jogadores');
|
||||||
|
}
|
||||||
|
|
||||||
|
String tP2Pct = tP2a > 0 ? '${((tP2m / tP2a) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
String tP3Pct = tP3a > 0 ? '${((tP3m / tP3a) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
String tFgPct = tFga > 0 ? '${((tFgm / tFga) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
String tFtPct = tFta > 0 ? '${((tFtm / tFta) * 100).toStringAsFixed(0)}%' : '0%';
|
||||||
|
|
||||||
|
tableData.add([
|
||||||
|
'', 'TOTAIS', _secToMin(tSec), tPts.toString(),
|
||||||
|
tP2m.toString(), tP2a.toString(), tP2Pct,
|
||||||
|
tP3m.toString(), tP3a.toString(), tP3Pct,
|
||||||
|
tFgm.toString(), tFga.toString(), tFgPct,
|
||||||
|
tFtm.toString(), tFta.toString(), tFtPct,
|
||||||
|
tOrb.toString(), tDrb.toString(), (tOrb + tDrb).toString(),
|
||||||
|
tStl.toString(), tAst.toString(), tTov.toString(), tBlk.toString(), tFls.toString(),
|
||||||
|
tSo.toString(), tIl.toString(), tLi.toString(),
|
||||||
|
tPa.toString(), tTresS.toString(), tDr.toString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return tableData;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _secToMin(int sec) {
|
||||||
|
final m = sec ~/ 60;
|
||||||
|
final s = sec % 60;
|
||||||
|
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _buildPdfTablePart1(List<List<String>> data, PdfColor headerColor) {
|
||||||
|
final hBold = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 9);
|
||||||
|
final hSub = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 8);
|
||||||
|
final cell = const pw.TextStyle(fontSize: 9);
|
||||||
|
final cellBold = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
||||||
|
|
||||||
|
return pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(1.2),
|
||||||
|
1: const pw.FlexColumnWidth(4.5),
|
||||||
|
2: const pw.FlexColumnWidth(2.0),
|
||||||
|
3: const pw.FlexColumnWidth(1.5),
|
||||||
|
4: const pw.FlexColumnWidth(4.5),
|
||||||
|
5: const pw.FlexColumnWidth(4.5),
|
||||||
|
6: const pw.FlexColumnWidth(4.5),
|
||||||
|
7: const pw.FlexColumnWidth(4.5),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: headerColor),
|
||||||
|
children: [
|
||||||
|
_sh('Nº', hSub),
|
||||||
|
_sh('NOME', hSub, left: true),
|
||||||
|
_sh('MIN', hSub),
|
||||||
|
_sh('PTS', hSub),
|
||||||
|
_groupHeader('2 PONTOS', hBold, hSub),
|
||||||
|
_groupHeader('3 PONTOS', hBold, hSub),
|
||||||
|
_groupHeader('GLOBAL', hBold, hSub),
|
||||||
|
_groupHeader('L. LIVRES', hBold, hSub),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...data.map((row) {
|
||||||
|
bool isTotais = row[1] == 'TOTAIS';
|
||||||
|
var s = isTotais ? cellBold : cell;
|
||||||
|
PdfColor? bg = isTotais ? PdfColors.grey200 : null;
|
||||||
|
|
||||||
|
return pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: bg),
|
||||||
|
children: [
|
||||||
|
_sd(row[0], s),
|
||||||
|
_sd(row[1], s, left: true),
|
||||||
|
_sd(row[2], s),
|
||||||
|
_sd(row[3], s),
|
||||||
|
_groupData(row[4], row[5], row[6], s),
|
||||||
|
_groupData(row[7], row[8], row[9], s),
|
||||||
|
_groupData(row[10], row[11], row[12], s),
|
||||||
|
_groupData(row[13], row[14], row[15], s),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _buildPdfTablePart2(List<List<String>> data, PdfColor headerColor) {
|
||||||
|
final hSub = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 8);
|
||||||
|
final cell = const pw.TextStyle(fontSize: 9);
|
||||||
|
final cellBold = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
||||||
|
|
||||||
|
return pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(1.2),
|
||||||
|
1: const pw.FlexColumnWidth(4.5),
|
||||||
|
2: const pw.FlexColumnWidth(1.5),
|
||||||
|
3: const pw.FlexColumnWidth(1.5),
|
||||||
|
4: const pw.FlexColumnWidth(1.5),
|
||||||
|
5: const pw.FlexColumnWidth(1.5),
|
||||||
|
6: const pw.FlexColumnWidth(1.5),
|
||||||
|
7: const pw.FlexColumnWidth(1.5),
|
||||||
|
8: const pw.FlexColumnWidth(1.5),
|
||||||
|
9: const pw.FlexColumnWidth(1.5),
|
||||||
|
10: const pw.FlexColumnWidth(1.5),
|
||||||
|
11: const pw.FlexColumnWidth(1.5),
|
||||||
|
12: const pw.FlexColumnWidth(1.5),
|
||||||
|
13: const pw.FlexColumnWidth(1.5),
|
||||||
|
14: const pw.FlexColumnWidth(1.5),
|
||||||
|
15: const pw.FlexColumnWidth(1.5),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: headerColor),
|
||||||
|
children: [
|
||||||
|
_sh('Nº', hSub),
|
||||||
|
_sh('NOME', hSub, left: true),
|
||||||
|
_sh('RO', hSub),
|
||||||
|
_sh('RD', hSub),
|
||||||
|
_sh('TR', hSub),
|
||||||
|
_sh('BR', hSub),
|
||||||
|
_sh('AS', hSub),
|
||||||
|
_sh('BP', hSub),
|
||||||
|
_sh('BLK', hSub),
|
||||||
|
_sh('FLS', hSub),
|
||||||
|
_sh('SO', hSub),
|
||||||
|
_sh('IL', hSub),
|
||||||
|
_sh('LI', hSub),
|
||||||
|
_sh('PA', hSub),
|
||||||
|
_sh('3S', hSub),
|
||||||
|
_sh('DR', hSub),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...data.map((row) {
|
||||||
|
bool isTotais = row[1] == 'TOTAIS';
|
||||||
|
var s = isTotais ? cellBold : cell;
|
||||||
|
PdfColor? bg = isTotais ? PdfColors.grey200 : null;
|
||||||
|
|
||||||
|
return pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: bg),
|
||||||
|
children: [
|
||||||
|
_sd(row[0], s),
|
||||||
|
_sd(row[1], s, left: true),
|
||||||
|
_sd(row[16], s),
|
||||||
|
_sd(row[17], s),
|
||||||
|
_sd(row[18], s),
|
||||||
|
_sd(row[19], s),
|
||||||
|
_sd(row[20], s),
|
||||||
|
_sd(row[21], s),
|
||||||
|
_sd(row[22], s),
|
||||||
|
_sd(row[23], s),
|
||||||
|
_sd(row[24], s),
|
||||||
|
_sd(row[25], s),
|
||||||
|
_sd(row[26], s),
|
||||||
|
_sd(row[27], s),
|
||||||
|
_sd(row[28], s),
|
||||||
|
_sd(row[29], s),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _sh(String text, pw.TextStyle style, {bool left = false}) {
|
||||||
|
return pw.Container(
|
||||||
|
alignment: left ? pw.Alignment.centerLeft : pw.Alignment.center,
|
||||||
|
padding: const pw.EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||||
|
child: pw.Text(text, style: style),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _sd(String text, pw.TextStyle style, {bool left = false}) {
|
||||||
|
return pw.Container(
|
||||||
|
alignment: left ? pw.Alignment.centerLeft : pw.Alignment.center,
|
||||||
|
padding: const pw.EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||||
|
child: pw.Text(text, style: style),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
||||||
|
return pw.Column(
|
||||||
|
children: [
|
||||||
|
pw.Container(
|
||||||
|
width: double.infinity,
|
||||||
|
alignment: pw.Alignment.center,
|
||||||
|
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||||
|
decoration: const pw.BoxDecoration(
|
||||||
|
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
||||||
|
),
|
||||||
|
child: pw.Text(title, style: hStyle),
|
||||||
|
),
|
||||||
|
pw.Row(children: [
|
||||||
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('C', style: sStyle))),
|
||||||
|
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
||||||
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('T', style: sStyle))),
|
||||||
|
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
||||||
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) {
|
||||||
|
return pw.Row(children: [
|
||||||
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(c, style: style))),
|
||||||
|
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
||||||
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(t, style: style))),
|
||||||
|
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
||||||
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(pct, style: style))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _heatmapPageHeader(String title, PdfColor color) {
|
||||||
|
return pw.Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: pw.BoxDecoration(color: color, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6))),
|
||||||
|
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _heatmapLegend() {
|
||||||
|
return pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
||||||
|
pw.SizedBox(width: 4),
|
||||||
|
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
|
||||||
|
pw.SizedBox(width: 20),
|
||||||
|
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
||||||
|
pw.SizedBox(width: 4),
|
||||||
|
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _buildSummaryBox(String title, String value) {
|
||||||
|
return pw.Container(
|
||||||
|
width: 120,
|
||||||
|
decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)),
|
||||||
|
child: pw.Column(children: [
|
||||||
|
pw.Container(
|
||||||
|
width: double.infinity, padding: const pw.EdgeInsets.all(6), color: const PdfColor.fromInt(0xFFA00000),
|
||||||
|
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 9, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||||
|
),
|
||||||
|
pw.Container(
|
||||||
|
width: double.infinity, padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
379
lib/pages/settings_screen.dart
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../utils/size_extension.dart';
|
||||||
|
import 'login.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
|
||||||
|
class SettingsScreen extends StatefulWidget {
|
||||||
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
|
||||||
|
File? _localImageFile;
|
||||||
|
String? _uploadedImageUrl;
|
||||||
|
bool _isUploadingImage = false;
|
||||||
|
bool _isMemoryLoaded = false;
|
||||||
|
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadUserAvatar();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUserAvatar() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedUrl = prefs.getString('meu_avatar_guardado');
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
if (savedUrl != null) _uploadedImageUrl = savedUrl;
|
||||||
|
_isMemoryLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('avatar_url')
|
||||||
|
.eq('id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
|
final urlDoSupabase = data['avatar_url'];
|
||||||
|
|
||||||
|
if (urlDoSupabase != savedUrl) {
|
||||||
|
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||||
|
setState(() {
|
||||||
|
_uploadedImageUrl = urlDoSupabase;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar avatar: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleImageChange() async {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
|
||||||
|
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (pickedFile == null || !mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_localImageFile = File(pickedFile.path);
|
||||||
|
_isUploadingImage = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) throw Exception("Utilizador não autenticado.");
|
||||||
|
|
||||||
|
final String storagePath = '$userId/profile_picture.png';
|
||||||
|
|
||||||
|
await supabase.storage.from('avatars').upload(
|
||||||
|
storagePath,
|
||||||
|
_localImageFile!,
|
||||||
|
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 👇 TRUQUE MÁGICO PARA O AVATAR ATUALIZAR: Adicionar o timestamp ao URL!
|
||||||
|
final String baseUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
||||||
|
final String publicUrl = '$baseUrl?v=${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.upsert({
|
||||||
|
'id': userId,
|
||||||
|
'avatar_url': publicUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString('meu_avatar_guardado', publicUrl);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_uploadedImageUrl = publicUrl;
|
||||||
|
_isUploadingImage = false;
|
||||||
|
_localImageFile = null;
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("Foto atualizada!"), backgroundColor: Colors.green)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isUploadingImage = false;
|
||||||
|
_localImageFile = null;
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Erro: $e"), backgroundColor: AppTheme.primaryRed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Color primaryRed = AppTheme.primaryRed;
|
||||||
|
final Color bgColor = Theme.of(context).scaffoldBackgroundColor;
|
||||||
|
final Color cardColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||||
|
final Color textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
final Color textLightColor = textColor.withOpacity(0.6);
|
||||||
|
|
||||||
|
bool isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: primaryRed,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text(
|
||||||
|
"Perfil e Definições",
|
||||||
|
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0 * context.sf, vertical: 24.0 * context.sf),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(20 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildTappableProfileAvatar(context, primaryRed),
|
||||||
|
SizedBox(width: 16 * context.sf),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Treinador",
|
||||||
|
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4 * context.sf),
|
||||||
|
Text(
|
||||||
|
supabase.auth.currentUser?.email ?? "sem@email.com",
|
||||||
|
style: TextStyle(color: textLightColor, fontSize: 14 * context.sf),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 32 * context.sf),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
||||||
|
child: Text(
|
||||||
|
"Definições",
|
||||||
|
style: TextStyle(color: textLightColor, fontSize: 14 * context.sf, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 20 * context.sf, vertical: 8 * context.sf),
|
||||||
|
leading: Icon(isDark ? Icons.dark_mode : Icons.light_mode, color: primaryRed, size: 28 * context.sf),
|
||||||
|
title: Text(
|
||||||
|
"Modo Escuro",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, color: textColor, fontSize: 16 * context.sf),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"Altera as cores da aplicação",
|
||||||
|
style: TextStyle(color: textLightColor, fontSize: 13 * context.sf),
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: isDark,
|
||||||
|
activeColor: primaryRed,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
themeNotifier.value = value ? ThemeMode.dark : ThemeMode.light;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 32 * context.sf),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
||||||
|
child: Text(
|
||||||
|
"Conta",
|
||||||
|
style: TextStyle(color: textLightColor, fontSize: 14 * context.sf, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 20 * context.sf, vertical: 4 * context.sf),
|
||||||
|
leading: Icon(Icons.logout_outlined, color: primaryRed, size: 26 * context.sf),
|
||||||
|
title: Text(
|
||||||
|
"Terminar Sessão",
|
||||||
|
style: TextStyle(color: primaryRed, fontWeight: FontWeight.bold, fontSize: 15 * context.sf),
|
||||||
|
),
|
||||||
|
onTap: () => _confirmLogout(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 50 * context.sf),
|
||||||
|
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
"PlayMaker v1.0.0",
|
||||||
|
style: TextStyle(color: textLightColor.withOpacity(0.7), fontSize: 13 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20 * context.sf),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_handleImageChange();
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 72 * context.sf,
|
||||||
|
height: 72 * context.sf,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: primaryRed.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: ClipOval(
|
||||||
|
child: _isUploadingImage && _localImageFile != null
|
||||||
|
? Image.file(_localImageFile!, fit: BoxFit.cover)
|
||||||
|
: !_isMemoryLoaded
|
||||||
|
? const SizedBox()
|
||||||
|
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: _uploadedImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => const SizedBox(),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(6 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.edit_outlined, color: primaryRed, size: 16 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (_isUploadingImage)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(color: Colors.black.withOpacity(0.4), shape: BoxShape.circle),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmLogout(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * context.sf)),
|
||||||
|
title: Text("Terminar Sessão", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
content: Text("Tens a certeza que queres sair da conta?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
onPressed: () async {
|
||||||
|
// 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR!
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove('meu_avatar_guardado');
|
||||||
|
await prefs.remove('last_team_id');
|
||||||
|
await prefs.remove('last_team_name');
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
await prefs.remove('last_team_wins');
|
||||||
|
await prefs.remove('last_team_losses');
|
||||||
|
await prefs.remove('last_team_draws');
|
||||||
|
|
||||||
|
await Supabase.instance.client.auth.signOut();
|
||||||
|
if (ctx.mounted) {
|
||||||
|
Navigator.of(ctx).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (context) => const LoginPage()),
|
||||||
|
(Route<dynamic> route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Sair", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
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:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF!
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class StatusPage extends StatefulWidget {
|
class StatusPage extends StatefulWidget {
|
||||||
const StatusPage({super.key});
|
const StatusPage({super.key});
|
||||||
@@ -16,11 +19,60 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
|
|
||||||
String? _selectedTeamId;
|
String? _selectedTeamId;
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
String? _selectedTeamLogo;
|
||||||
|
|
||||||
String _sortColumn = 'pts';
|
String _sortColumn = 'pts';
|
||||||
bool _isAscending = false;
|
bool _isAscending = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSelectedTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSelectedTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
if (savedId != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = savedId;
|
||||||
|
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
|
||||||
|
_selectedTeamLogo = prefs.getString('last_team_logo');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSelectedTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_selectedTeamId != null) {
|
||||||
|
await prefs.setString('last_team_id', _selectedTeamId!);
|
||||||
|
await prefs.setString('last_team_name', _selectedTeamName);
|
||||||
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', _selectedTeamLogo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null && _selectedTeamId != null) {
|
||||||
|
try {
|
||||||
|
await _supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': _selectedTeamId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
@@ -30,20 +82,32 @@ 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: Colors.white,
|
color: bgColor,
|
||||||
borderRadius: BorderRadius.circular(15 * context.sf),
|
borderRadius: BorderRadius.circular(15 * context.sf),
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * context.sf),
|
(_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
|
||||||
|
? ClipOval(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: _selectedTeamLogo!,
|
||||||
|
width: 24 * context.sf,
|
||||||
|
height: 24 * context.sf,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
|
||||||
SizedBox(width: 10 * context.sf),
|
SizedBox(width: 10 * context.sf),
|
||||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))
|
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
||||||
]),
|
]),
|
||||||
const Icon(Icons.arrow_drop_down),
|
Icon(Icons.arrow_drop_down, color: textColor),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -63,7 +127,7 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
||||||
builder: (context, membersSnapshot) {
|
builder: (context, membersSnapshot) {
|
||||||
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
|
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C)));
|
return const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
|
||||||
}
|
}
|
||||||
|
|
||||||
final membersData = membersSnapshot.data ?? [];
|
final membersData = membersSnapshot.data ?? [];
|
||||||
@@ -82,7 +146,7 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
|
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
|
||||||
});
|
});
|
||||||
|
|
||||||
return _buildStatsGrid(context, playerTotals, teamTotals);
|
return _buildStatsGrid(context, playerTotals, teamTotals, bgColor, textColor);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,15 +160,14 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
|
|
||||||
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";
|
||||||
aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
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};
|
||||||
}
|
}
|
||||||
|
|
||||||
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, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'image_url': null, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
||||||
|
|
||||||
aggregated[name]!['j'] += 1;
|
aggregated[name]!['j'] += 1;
|
||||||
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
||||||
@@ -113,7 +176,6 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
aggregated[name]!['stl'] += (row['stl'] ?? 0);
|
aggregated[name]!['stl'] += (row['stl'] ?? 0);
|
||||||
aggregated[name]!['blk'] += (row['blk'] ?? 0);
|
aggregated[name]!['blk'] += (row['blk'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var game in games) {
|
for (var game in games) {
|
||||||
String? mvp = game['mvp_name'];
|
String? mvp = game['mvp_name'];
|
||||||
String? defRaw = game['top_def_name'];
|
String? defRaw = game['top_def_name'];
|
||||||
@@ -131,97 +193,158 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
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', '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) {
|
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.white,
|
color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES!
|
||||||
|
width: double.infinity,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
child: DataTable(
|
child: DataTable(
|
||||||
columnSpacing: 25 * context.sf,
|
columnSpacing: 20 * context.sf,
|
||||||
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
|
horizontalMargin: 16 * context.sf,
|
||||||
dataRowHeight: 60 * context.sf,
|
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
||||||
|
dataRowMaxHeight: 60 * context.sf,
|
||||||
|
dataRowMinHeight: 60 * context.sf,
|
||||||
columns: [
|
columns: [
|
||||||
DataColumn(label: const Text('JOGADOR')),
|
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
|
||||||
_buildSortableColumn(context, 'J', 'j'),
|
_buildSortableColumn(context, 'J', 'j', textColor),
|
||||||
_buildSortableColumn(context, 'PTS', 'pts'),
|
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
||||||
_buildSortableColumn(context, 'AST', 'ast'),
|
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
||||||
_buildSortableColumn(context, 'RBS', 'rbs'),
|
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
|
||||||
_buildSortableColumn(context, 'STL', 'stl'),
|
_buildSortableColumn(context, 'STL', 'stl', textColor),
|
||||||
_buildSortableColumn(context, 'BLK', 'blk'),
|
_buildSortableColumn(context, 'BLK', 'blk', textColor),
|
||||||
_buildSortableColumn(context, 'DEF 🛡️', 'def'),
|
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
|
||||||
_buildSortableColumn(context, 'MVP 🏆', 'mvp'),
|
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
|
||||||
],
|
],
|
||||||
rows: [
|
rows: [
|
||||||
...players.map((player) => DataRow(cells: [
|
...players.map((player) => DataRow(cells: [
|
||||||
DataCell(Row(children: [CircleAvatar(radius: 15 * context.sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * context.sf)), SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf))])),
|
DataCell(
|
||||||
DataCell(Center(child: Text(player['j'].toString()))),
|
Row(
|
||||||
_buildStatCell(context, player['pts'], isHighlight: true),
|
children: [
|
||||||
_buildStatCell(context, player['ast']),
|
ClipOval(
|
||||||
_buildStatCell(context, player['rbs']),
|
child: Container(
|
||||||
_buildStatCell(context, player['stl']),
|
width: 30 * context.sf,
|
||||||
_buildStatCell(context, player['blk']),
|
height: 30 * context.sf,
|
||||||
_buildStatCell(context, player['def'], isBlue: true),
|
color: Colors.grey.withOpacity(0.2),
|
||||||
_buildStatCell(context, player['mvp'], isGold: true),
|
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: player['image_url'],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
||||||
|
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
||||||
|
_buildStatCell(context, player['ast'], textColor),
|
||||||
|
_buildStatCell(context, player['rbs'], textColor),
|
||||||
|
_buildStatCell(context, player['stl'], textColor),
|
||||||
|
_buildStatCell(context, player['blk'], textColor),
|
||||||
|
_buildStatCell(context, player['def'], textColor, isBlue: true),
|
||||||
|
_buildStatCell(context, player['mvp'], textColor, isGold: true),
|
||||||
])),
|
])),
|
||||||
DataRow(
|
DataRow(
|
||||||
color: MaterialStateProperty.all(Colors.grey.shade50),
|
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
||||||
cells: [
|
cells: [
|
||||||
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * context.sf))),
|
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
|
||||||
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
|
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
|
||||||
_buildStatCell(context, teamTotals['pts'], isHighlight: true),
|
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
|
||||||
_buildStatCell(context, teamTotals['ast']),
|
_buildStatCell(context, teamTotals['ast'], textColor),
|
||||||
_buildStatCell(context, teamTotals['rbs']),
|
_buildStatCell(context, teamTotals['rbs'], textColor),
|
||||||
_buildStatCell(context, teamTotals['stl']),
|
_buildStatCell(context, teamTotals['stl'], textColor),
|
||||||
_buildStatCell(context, teamTotals['blk']),
|
_buildStatCell(context, teamTotals['blk'], textColor),
|
||||||
_buildStatCell(context, teamTotals['def'], isBlue: true),
|
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
|
||||||
_buildStatCell(context, teamTotals['mvp'], isGold: true),
|
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) {
|
DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey, Color textColor) {
|
||||||
return DataColumn(label: InkWell(
|
return DataColumn(label: InkWell(
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() {
|
||||||
if (_sortColumn == sortKey) _isAscending = !_isAscending;
|
if (_sortColumn == sortKey) _isAscending = !_isAscending;
|
||||||
else { _sortColumn = sortKey; _isAscending = false; }
|
else { _sortColumn = sortKey; _isAscending = false; }
|
||||||
}),
|
}),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold)),
|
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: const Color(0xFFE74C3C)),
|
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, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
|
DataCell _buildStatCell(BuildContext context, int value, Color textColor, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
|
||||||
return DataCell(Center(child: Container(
|
return DataCell(Center(child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
||||||
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)),
|
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(
|
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
|
||||||
fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600,
|
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 ? Colors.green.shade700 : Colors.black87))
|
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, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
|
showModalBottomSheet(context: context, backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _teamController.teamsStream,
|
stream: _teamController.teamsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final teams = snapshot.data ?? [];
|
final teams = snapshot.data ?? [];
|
||||||
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
|
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) {
|
||||||
title: Text(teams[i]['name']),
|
final team = teams[i];
|
||||||
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
|
final logoUrl = team['image_url'];
|
||||||
));
|
|
||||||
|
return ListTile(
|
||||||
|
leading: ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 36 * context.sf,
|
||||||
|
height: 36 * context.sf,
|
||||||
|
color: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: logoUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = team['id'].toString();
|
||||||
|
_selectedTeamName = team['name'];
|
||||||
|
_selectedTeamLogo = logoUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _saveSelectedTeam();
|
||||||
|
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE AQUI
|
||||||
import 'package:playmaker/screens/team_stats_page.dart';
|
import 'package:playmaker/screens/team_stats_page.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class TeamsPage extends StatefulWidget {
|
class TeamsPage extends StatefulWidget {
|
||||||
const TeamsPage({super.key});
|
const TeamsPage({super.key});
|
||||||
@@ -32,14 +37,14 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setModalState) {
|
builder: (context, setModalState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
backgroundColor: const Color(0xFF2C3E50),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text("Filtros de pesquisa", style: TextStyle(color: Colors.white, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
Text("Filtros de pesquisa", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.close, color: Colors.white, size: 20 * context.sf),
|
icon: Icon(Icons.close, color: Colors.grey, size: 20 * context.sf),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -47,7 +52,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Divider(color: Colors.white24),
|
Divider(color: Colors.grey.withOpacity(0.2)),
|
||||||
SizedBox(height: 16 * context.sf),
|
SizedBox(height: 16 * context.sf),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -82,7 +87,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
|
child: Text("CONCLUÍDO", style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -107,7 +112,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
opt,
|
opt,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected ? const Color(0xFFE74C3C) : Colors.white70,
|
color: isSelected ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
fontSize: 14 * context.sf,
|
fontSize: 14 * context.sf,
|
||||||
),
|
),
|
||||||
@@ -121,16 +126,15 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 🔥 OLHA QUE LIMPEZA: Já não precisamos de calcular nada aqui!
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
|
title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
|
||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf),
|
icon: Icon(Icons.filter_list, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
onPressed: () => _showFilterDialog(context),
|
onPressed: () => _showFilterDialog(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -142,8 +146,8 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI!
|
heroTag: 'add_team_btn',
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
backgroundColor: AppTheme.primaryRed,
|
||||||
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
|
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
|
||||||
onPressed: () => _showCreateDialog(context),
|
onPressed: () => _showCreateDialog(context),
|
||||||
),
|
),
|
||||||
@@ -156,13 +160,13 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
|
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
|
||||||
style: TextStyle(fontSize: 16 * context.sf),
|
style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Pesquisar equipa...',
|
hintText: 'Pesquisar equipa...',
|
||||||
hintStyle: TextStyle(fontSize: 16 * context.sf),
|
hintStyle: TextStyle(fontSize: 16 * context.sf, color: Colors.grey),
|
||||||
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf),
|
prefixIcon: Icon(Icons.search, color: AppTheme.primaryRed, size: 22 * context.sf),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: Theme.of(context).colorScheme.surface,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -170,11 +174,11 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTeamsList() {
|
Widget _buildTeamsList() {
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return FutureBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: controller.teamsStream,
|
future: controller.getTeamsWithStats(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
if (snapshot.connectionState == ConnectionState.waiting) return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
|
||||||
if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf)));
|
if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
|
||||||
|
|
||||||
var data = List<Map<String, dynamic>>.from(snapshot.data!);
|
var data = List<Map<String, dynamic>>.from(snapshot.data!);
|
||||||
|
|
||||||
@@ -190,27 +194,45 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
|
else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
return ListView.builder(
|
return RefreshIndicator(
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
onRefresh: () async => setState(() {}),
|
||||||
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
|
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = Team.fromMap(data[index]);
|
final team = Team.fromMap(data[index]);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => setState(() {})),
|
||||||
child: TeamCard(
|
child: TeamCard(
|
||||||
team: team,
|
team: team,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite),
|
onFavoriteTap: () async {
|
||||||
|
await controller.toggleFavorite(team.id, team.isFavorite);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onDelete: () => setState(() {}),
|
||||||
|
sf: context.sf,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCreateDialog(BuildContext context) {
|
void _showCreateDialog(BuildContext context) {
|
||||||
showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl)));
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateTeamDialog(
|
||||||
|
sf: context.sf,
|
||||||
|
onConfirm: (name, season, imageFile) async {
|
||||||
|
await controller.createTeam(name, season, imageFile);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,83 +241,160 @@ class TeamCard extends StatelessWidget {
|
|||||||
final Team team;
|
final Team team;
|
||||||
final TeamController controller;
|
final TeamController controller;
|
||||||
final VoidCallback onFavoriteTap;
|
final VoidCallback onFavoriteTap;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
final double sf;
|
||||||
|
|
||||||
const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap});
|
const TeamCard({
|
||||||
|
super.key,
|
||||||
|
required this.team,
|
||||||
|
required this.controller,
|
||||||
|
required this.onFavoriteTap,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.sf,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||||
color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf),
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
|
final double avatarSize = 56 * sf; // 2 * radius (28)
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 12 * sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(15 * sf),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10 * sf)]
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(15 * sf),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf),
|
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
|
||||||
leading: Stack(
|
leading: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
// 👇 AVATAR DA EQUIPA OTIMIZADO COM CACHE 👇
|
||||||
radius: 28 * context.sf, backgroundColor: Colors.grey[200],
|
ClipOval(
|
||||||
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null,
|
child: Container(
|
||||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null,
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
child: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: team.imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero, // Fica instantâneo no scroll
|
||||||
|
placeholder: (context, url) => const SizedBox(), // Fica só o fundo cinza
|
||||||
|
errorWidget: (context, url, error) => Center(
|
||||||
|
child: Text("🏀", style: TextStyle(fontSize: 24 * sf)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
||||||
|
style: TextStyle(fontSize: 24 * sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -15 * context.sf, top: -10 * context.sf,
|
left: -15 * sf,
|
||||||
|
top: -10 * sf,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(team.isFavorite ? Icons.star : Icons.star_border, color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), size: 28 * context.sf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * context.sf)]),
|
icon: Icon(
|
||||||
|
team.isFavorite ? Icons.star : Icons.star_border,
|
||||||
|
color: team.isFavorite ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
|
||||||
|
size: 28 * sf,
|
||||||
|
shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * sf)],
|
||||||
|
),
|
||||||
onPressed: onFavoriteTap,
|
onPressed: onFavoriteTap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis),
|
title: Text(
|
||||||
|
team.name,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: textColor),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: EdgeInsets.only(top: 6.0 * context.sf),
|
padding: EdgeInsets.only(top: 6.0 * sf),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey),
|
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
||||||
SizedBox(width: 4 * context.sf),
|
SizedBox(width: 4 * sf),
|
||||||
StreamBuilder<int>(
|
Text(
|
||||||
stream: controller.getPlayerCountStream(team.id),
|
"${team.playerCount} Jogs.",
|
||||||
initialData: 0,
|
style: TextStyle(
|
||||||
builder: (context, snapshot) {
|
color: team.playerCount > 0 ? AppTheme.successGreen : AppTheme.warningAmber,
|
||||||
final count = snapshot.data ?? 0;
|
fontWeight: FontWeight.bold,
|
||||||
return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf));
|
fontSize: 13 * sf,
|
||||||
},
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8 * sf),
|
||||||
|
Expanded(
|
||||||
|
child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * sf), overflow: TextOverflow.ellipsis),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8 * context.sf),
|
|
||||||
Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), overflow: TextOverflow.ellipsis)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(tooltip: 'Ver Estatísticas', icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * context.sf), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)))),
|
IconButton(
|
||||||
IconButton(tooltip: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * context.sf), onPressed: () => _confirmDelete(context)),
|
tooltip: 'Ver Estatísticas',
|
||||||
|
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
|
||||||
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => onDelete()),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Eliminar Equipa',
|
||||||
|
icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 24 * sf),
|
||||||
|
onPressed: () => _confirmDelete(context, sf, bgColor, textColor),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmDelete(BuildContext context) {
|
void _confirmDelete(BuildContext context, double sf, Color cardColor, Color textColor) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
backgroundColor: cardColor,
|
||||||
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)),
|
surfaceTintColor: Colors.transparent,
|
||||||
|
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
|
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf, color: textColor)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))),
|
TextButton(
|
||||||
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf, color: Colors.grey)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
onDelete();
|
||||||
|
controller.deleteTeam(team.id).catchError((e) {
|
||||||
|
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao eliminar: $e'), backgroundColor: Colors.red));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text('Eliminar', style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DIALOG DE CRIAÇÃO ---
|
// --- DIALOG DE CRIAÇÃO (COM CROPPER E ESCUDO) ---
|
||||||
class CreateTeamDialog extends StatefulWidget {
|
class CreateTeamDialog extends StatefulWidget {
|
||||||
final Function(String name, String season, String imageUrl) onConfirm;
|
final Function(String name, String season, File? imageFile) onConfirm;
|
||||||
const CreateTeamDialog({super.key, required this.onConfirm});
|
final double sf;
|
||||||
|
|
||||||
|
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
|
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
|
||||||
@@ -303,37 +402,112 @@ class CreateTeamDialog extends StatefulWidget {
|
|||||||
|
|
||||||
class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final TextEditingController _imageController = TextEditingController();
|
|
||||||
String _selectedSeason = '2024/25';
|
String _selectedSeason = '2024/25';
|
||||||
|
|
||||||
|
File? _selectedImage;
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _isPickerActive = false;
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
if (_isPickerActive) return;
|
||||||
|
setState(() => _isPickerActive = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
|
||||||
|
if (pickedFile != null) {
|
||||||
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
|
sourcePath: pickedFile.path,
|
||||||
|
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
|
||||||
|
uiSettings: [
|
||||||
|
AndroidUiSettings(
|
||||||
|
toolbarTitle: 'Recortar Logo',
|
||||||
|
toolbarColor: AppTheme.primaryRed,
|
||||||
|
toolbarWidgetColor: Colors.white,
|
||||||
|
initAspectRatio: CropAspectRatioPreset.square,
|
||||||
|
lockAspectRatio: true,
|
||||||
|
hideBottomControls: true,
|
||||||
|
),
|
||||||
|
IOSUiSettings(title: 'Recortar Logo', aspectRatioLockEnabled: true, resetButtonHidden: true),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (croppedFile != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedImage = File(croppedFile.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isPickerActive = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
|
||||||
|
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words),
|
GestureDetector(
|
||||||
SizedBox(height: 15 * context.sf),
|
onTap: _pickImage,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 40 * widget.sf,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05),
|
||||||
|
backgroundImage: _selectedImage != null ? FileImage(_selectedImage!) : null,
|
||||||
|
child: _selectedImage == null
|
||||||
|
? Icon(Icons.add_photo_alternate_outlined, size: 30 * widget.sf, color: Colors.grey)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (_selectedImage == null)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0, right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(4 * widget.sf),
|
||||||
|
decoration: const BoxDecoration(color: AppTheme.primaryRed, shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.add, color: Colors.white, size: 16 * widget.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10 * widget.sf),
|
||||||
|
Text("Logótipo (Opcional)", style: TextStyle(fontSize: 12 * widget.sf, color: Colors.grey)),
|
||||||
|
SizedBox(height: 20 * widget.sf),
|
||||||
|
|
||||||
|
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * widget.sf)), textCapitalization: TextCapitalization.words),
|
||||||
|
SizedBox(height: 15 * widget.sf),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)),
|
dropdownColor: Theme.of(context).colorScheme.surface,
|
||||||
style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87),
|
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf)),
|
||||||
|
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
||||||
onChanged: (val) => setState(() => _selectedSeason = val!),
|
onChanged: (val) => setState(() => _selectedSeason = val!),
|
||||||
),
|
),
|
||||||
SizedBox(height: 15 * context.sf),
|
|
||||||
TextField(controller: _imageController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * context.sf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * context.sf))),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))),
|
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)),
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
|
||||||
onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } },
|
onPressed: _isLoading ? null : () async {
|
||||||
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)),
|
if (_nameController.text.trim().isNotEmpty) {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
await widget.onConfirm(_nameController.text.trim(), _selectedSeason, _selectedImage);
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: _isLoading
|
||||||
|
? SizedBox(width: 16 * widget.sf, height: 16 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,53 +1,103 @@
|
|||||||
import 'dart:async';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart'; // 👇 MAGIA DO CACHE AQUI
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../models/person_model.dart';
|
import '../models/person_model.dart';
|
||||||
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
// --- CABEÇALHO ---
|
// ==========================================
|
||||||
|
// 1. CABEÇALHO (AGORA COM CACHE DE IMAGEM INSTANTÂNEO)
|
||||||
|
// ==========================================
|
||||||
class StatsHeader extends StatelessWidget {
|
class StatsHeader extends StatelessWidget {
|
||||||
final Team team;
|
final Team team;
|
||||||
|
final String? currentImageUrl;
|
||||||
|
final VoidCallback onEditPhoto;
|
||||||
|
final bool isUploading;
|
||||||
|
|
||||||
const StatsHeader({super.key, required this.team});
|
const StatsHeader({
|
||||||
|
super.key,
|
||||||
|
required this.team,
|
||||||
|
required this.currentImageUrl,
|
||||||
|
required this.onEditPhoto,
|
||||||
|
required this.isUploading,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20),
|
padding: EdgeInsets.only(top: 50 * context.sf, left: 20 * context.sf, right: 20 * context.sf, bottom: 20 * context.sf),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFF2C3E50),
|
color: AppTheme.primaryRed,
|
||||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)),
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(30 * context.sf),
|
||||||
|
bottomRight: Radius.circular(30 * context.sf)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context)
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
SizedBox(width: 10 * context.sf),
|
||||||
|
|
||||||
// IMAGEM OU EMOJI DA EQUIPA AQUI!
|
GestureDetector(
|
||||||
CircleAvatar(
|
onTap: onEditPhoto,
|
||||||
radius: 24,
|
child: Stack(
|
||||||
backgroundColor: Colors.white24,
|
alignment: Alignment.center,
|
||||||
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
children: [
|
||||||
? NetworkImage(team.imageUrl)
|
// 👇 AVATAR DA EQUIPA SEM LAG 👇
|
||||||
: null,
|
ClipOval(
|
||||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
child: Container(
|
||||||
? Text(
|
width: 56 * context.sf,
|
||||||
team.imageUrl.isEmpty ? "🛡️" : team.imageUrl,
|
height: 56 * context.sf,
|
||||||
style: const TextStyle(fontSize: 20),
|
color: Colors.white24,
|
||||||
|
child: (currentImageUrl != null && currentImageUrl!.isNotEmpty && currentImageUrl!.startsWith('http'))
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: currentImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero, // Corta o atraso
|
||||||
|
placeholder: (context, url) => Center(child: Text("🛡️", style: TextStyle(fontSize: 24 * context.sf))),
|
||||||
|
errorWidget: (context, url, error) => Center(child: Text("🛡️", style: TextStyle(fontSize: 24 * context.sf))),
|
||||||
)
|
)
|
||||||
: null,
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
(currentImageUrl != null && currentImageUrl!.isNotEmpty) ? currentImageUrl! : "🛡️",
|
||||||
|
style: TextStyle(fontSize: 24 * context.sf)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0, right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(4 * context.sf),
|
||||||
|
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.edit, color: AppTheme.primaryRed, size: 12 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isUploading)
|
||||||
|
Container(
|
||||||
|
width: 56 * context.sf, height: 56 * context.sf,
|
||||||
|
decoration: const BoxDecoration(color: Colors.black45, shape: BoxShape.circle),
|
||||||
|
child: const Padding(padding: EdgeInsets.all(12.0), child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(width: 15),
|
SizedBox(width: 15 * context.sf),
|
||||||
Expanded( // Expanded evita overflow se o nome for muito longo
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis),
|
Text(team.name, style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis),
|
||||||
Text(team.season, style: const TextStyle(color: Colors.white70, fontSize: 14)),
|
Text(team.season, style: TextStyle(color: Colors.white70, fontSize: 14 * context.sf)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -60,25 +110,28 @@ class StatsHeader extends StatelessWidget {
|
|||||||
// --- CARD DE RESUMO ---
|
// --- CARD DE RESUMO ---
|
||||||
class StatsSummaryCard extends StatelessWidget {
|
class StatsSummaryCard extends StatelessWidget {
|
||||||
final int total;
|
final int total;
|
||||||
|
|
||||||
const StatsSummaryCard({super.key, required this.total});
|
const StatsSummaryCard({super.key, required this.total});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final Color bgColor = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white;
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: EdgeInsets.all(20 * context.sf),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(20 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.15))),
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
gradient: LinearGradient(colors: [Colors.blue.shade700, Colors.blue.shade400]),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text("Total de Membros", style: TextStyle(color: Colors.white, fontSize: 16)),
|
Row(
|
||||||
Text("$total", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
|
children: [
|
||||||
|
Icon(Icons.groups, color: AppTheme.primaryRed, size: 28 * context.sf),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
Text("Total de Membros", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 16 * context.sf, fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text("$total", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 28 * context.sf, fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -89,7 +142,6 @@ class StatsSummaryCard extends StatelessWidget {
|
|||||||
// --- TÍTULO DE SECÇÃO ---
|
// --- TÍTULO DE SECÇÃO ---
|
||||||
class StatsSectionTitle extends StatelessWidget {
|
class StatsSectionTitle extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
const StatsSectionTitle({super.key, required this.title});
|
const StatsSectionTitle({super.key, required this.title});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -97,63 +149,119 @@ class StatsSectionTitle extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
|
Text(title, style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
const Divider(),
|
Divider(color: Colors.grey.withOpacity(0.2)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CARD DA PESSOA (JOGADOR/TREINADOR) ---
|
// --- CARD DA PESSOA (FOTO SEM LAG) ---
|
||||||
class PersonCard extends StatelessWidget {
|
class PersonCard extends StatelessWidget {
|
||||||
final Person person;
|
final Person person;
|
||||||
final bool isCoach;
|
final bool isCoach;
|
||||||
final VoidCallback onEdit;
|
final VoidCallback onEdit;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
const PersonCard({
|
const PersonCard({super.key, required this.person, required this.isCoach, required this.onEdit, required this.onDelete});
|
||||||
super.key,
|
|
||||||
required this.person,
|
|
||||||
required this.isCoach,
|
|
||||||
required this.onEdit,
|
|
||||||
required this.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
final Color defaultBg = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white;
|
||||||
margin: const EdgeInsets.only(top: 12),
|
final Color coachBg = Theme.of(context).brightness == Brightness.dark ? AppTheme.warningAmber.withOpacity(0.1) : const Color(0xFFFFF9C4);
|
||||||
elevation: 2,
|
final String? pImage = person.imageUrl;
|
||||||
color: isCoach ? const Color(0xFFFFF9C4) : Colors.white,
|
final Color iconColor = isCoach ? Colors.white : AppTheme.primaryRed;
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
||||||
child: ListTile(
|
|
||||||
leading: isCoach
|
|
||||||
? const CircleAvatar(backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white))
|
|
||||||
: Container(
|
|
||||||
width: 45,
|
|
||||||
height: 45,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
|
||||||
child: Text(person.number ?? "J", style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)),
|
|
||||||
),
|
|
||||||
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
|
|
||||||
// --- CANTO DIREITO (Trailing) ---
|
return Card(
|
||||||
trailing: Row(
|
margin: EdgeInsets.only(top: 12 * context.sf),
|
||||||
|
elevation: 2,
|
||||||
|
color: isCoach ? coachBg : defaultBg,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 👇 FOTO DO JOGADOR/TREINADOR INSTANTÂNEA 👇
|
||||||
|
ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 44 * context.sf,
|
||||||
|
height: 44 * context.sf,
|
||||||
|
color: isCoach ? AppTheme.warningAmber : AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (pImage != null && pImage.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: pImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.person, color: iconColor, size: 24 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.person, color: iconColor, size: 24 * context.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person, color: iconColor, size: 24 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12 * context.sf),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (!isCoach && person.number != null && person.number!.isNotEmpty) ...[
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
||||||
|
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.1), borderRadius: BorderRadius.circular(6 * context.sf)),
|
||||||
|
child: Text(person.number!, style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Text(person.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface), overflow: TextOverflow.ellipsis)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// IMAGEM DA EQUIPA NO CARD DO JOGADOR
|
IconButton(icon: Icon(Icons.edit_outlined, color: Colors.blue, size: 22 * context.sf), onPressed: onEdit, padding: EdgeInsets.zero, constraints: const BoxConstraints()),
|
||||||
|
SizedBox(width: 16 * context.sf),
|
||||||
const SizedBox(width: 5), // Espaço
|
IconButton(icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 22 * context.sf), onPressed: onDelete, padding: EdgeInsets.zero, constraints: const BoxConstraints()),
|
||||||
|
],
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit_outlined, color: Colors.blue),
|
|
||||||
onPressed: onEdit,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
],
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
|
||||||
onPressed: onDelete,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// WIDGET NOVO: SKELETON LOADING (SHIMMER)
|
||||||
|
// ==========================================
|
||||||
|
class SkeletonLoadingStats extends StatelessWidget {
|
||||||
|
const SkeletonLoadingStats({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final baseColor = isDark ? Colors.grey[800]! : Colors.grey[300]!;
|
||||||
|
final highlightColor = isDark ? Colors.grey[700]! : Colors.grey[100]!;
|
||||||
|
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: baseColor,
|
||||||
|
highlightColor: highlightColor,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(16.0 * context.sf),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(height: 80 * context.sf, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf))),
|
||||||
|
SizedBox(height: 30 * context.sf),
|
||||||
|
Container(height: 20 * context.sf, width: 150 * context.sf, color: Colors.white),
|
||||||
|
SizedBox(height: 10 * context.sf),
|
||||||
|
for (int i = 0; i < 3; i++) ...[
|
||||||
|
Container(
|
||||||
|
height: 60 * context.sf, width: double.infinity,
|
||||||
|
margin: EdgeInsets.only(top: 12 * context.sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15 * context.sf)),
|
||||||
|
),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -164,10 +272,8 @@ class PersonCard extends StatelessWidget {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// 2. PÁGINA PRINCIPAL
|
// 2. PÁGINA PRINCIPAL
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
class TeamStatsPage extends StatefulWidget {
|
class TeamStatsPage extends StatefulWidget {
|
||||||
final Team team;
|
final Team team;
|
||||||
|
|
||||||
const TeamStatsPage({super.key, required this.team});
|
const TeamStatsPage({super.key, required this.team});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -177,71 +283,103 @@ class TeamStatsPage extends StatefulWidget {
|
|||||||
class _TeamStatsPageState extends State<TeamStatsPage> {
|
class _TeamStatsPageState extends State<TeamStatsPage> {
|
||||||
final StatsController _controller = StatsController();
|
final StatsController _controller = StatsController();
|
||||||
|
|
||||||
|
late String _teamImageUrl;
|
||||||
|
bool _isUploadingTeamPhoto = false;
|
||||||
|
bool _isPickerActive = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_teamImageUrl = widget.team.imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateTeamPhoto() async {
|
||||||
|
if (_isPickerActive) return;
|
||||||
|
setState(() => _isPickerActive = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final File? croppedFile = await _controller.pickAndCropImage(context);
|
||||||
|
if (croppedFile == null) return;
|
||||||
|
|
||||||
|
setState(() => _isUploadingTeamPhoto = true);
|
||||||
|
|
||||||
|
final fileName = 'team_${widget.team.id}_${DateTime.now().millisecondsSinceEpoch}.png';
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
await supabase.storage.from('avatars').upload(fileName, croppedFile, fileOptions: const FileOptions(upsert: true));
|
||||||
|
final publicUrl = supabase.storage.from('avatars').getPublicUrl(fileName);
|
||||||
|
|
||||||
|
await supabase.from('teams').update({'image_url': publicUrl}).eq('id', widget.team.id);
|
||||||
|
|
||||||
|
if (_teamImageUrl.isNotEmpty && _teamImageUrl.startsWith('http')) {
|
||||||
|
final oldPath = _controller.extractPathFromUrl(_teamImageUrl, 'avatars');
|
||||||
|
if (oldPath != null) await supabase.storage.from('avatars').remove([oldPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) setState(() => _teamImageUrl = publicUrl);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erro: $e"), backgroundColor: AppTheme.primaryRed));
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isUploadingTeamPhoto = false;
|
||||||
|
_isPickerActive = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Cabeçalho
|
StatsHeader(team: widget.team, currentImageUrl: _teamImageUrl, onEditPhoto: _updateTeamPhoto, isUploading: _isUploadingTeamPhoto),
|
||||||
StatsHeader(team: widget.team),
|
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<List<Person>>(
|
child: StreamBuilder<List<Person>>(
|
||||||
stream: _controller.getMembers(widget.team.id),
|
stream: _controller.getMembers(widget.team.id),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const SkeletonLoadingStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) return Center(child: Text("Erro ao carregar: ${snapshot.error}", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)));
|
||||||
return Center(child: Text("Erro ao carregar: ${snapshot.error}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
final members = snapshot.data ?? [];
|
final members = snapshot.data ?? [];
|
||||||
|
|
||||||
final coaches = members.where((m) => m.type == 'Treinador').toList();
|
final coaches = members.where((m) => m.type == 'Treinador').toList()..sort((a, b) => a.name.compareTo(b.name));
|
||||||
final players = members.where((m) => m.type == 'Jogador').toList();
|
final players = members.where((m) => m.type == 'Jogador').toList()..sort((a, b) {
|
||||||
|
int numA = int.tryParse(a.number ?? '999') ?? 999;
|
||||||
|
int numB = int.tryParse(b.number ?? '999') ?? 999;
|
||||||
|
return numA.compareTo(numB);
|
||||||
|
});
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
onRefresh: () async => setState(() {}),
|
onRefresh: () async => setState(() {}),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0 * context.sf),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
StatsSummaryCard(total: members.length),
|
StatsSummaryCard(total: members.length),
|
||||||
const SizedBox(height: 30),
|
SizedBox(height: 30 * context.sf),
|
||||||
|
|
||||||
// TREINADORES
|
|
||||||
if (coaches.isNotEmpty) ...[
|
if (coaches.isNotEmpty) ...[
|
||||||
const StatsSectionTitle(title: "Treinadores"),
|
const StatsSectionTitle(title: "Treinadores"),
|
||||||
...coaches.map((c) => PersonCard(
|
...coaches.map((c) => PersonCard(person: c, isCoach: true, onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c), onDelete: () => _confirmDelete(context, c))),
|
||||||
person: c,
|
SizedBox(height: 30 * context.sf),
|
||||||
isCoach: true,
|
|
||||||
|
|
||||||
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c),
|
|
||||||
onDelete: () => _confirmDelete(context, c),
|
|
||||||
)),
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// JOGADORES
|
|
||||||
const StatsSectionTitle(title: "Jogadores"),
|
const StatsSectionTitle(title: "Jogadores"),
|
||||||
if (players.isEmpty)
|
if (players.isEmpty)
|
||||||
const Padding(
|
Padding(padding: EdgeInsets.only(top: 20 * context.sf), child: Text("Nenhum jogador nesta equipa.", style: TextStyle(color: Colors.grey, fontSize: 16 * context.sf)))
|
||||||
padding: EdgeInsets.only(top: 20),
|
|
||||||
child: Text("Nenhum jogador nesta equipa.", style: TextStyle(color: Colors.grey, fontSize: 16)),
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
...players.map((p) => PersonCard(
|
...players.map((p) => PersonCard(person: p, isCoach: false, onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p), onDelete: () => _confirmDelete(context, p))),
|
||||||
person: p,
|
SizedBox(height: 80 * context.sf),
|
||||||
isCoach: false,
|
|
||||||
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p),
|
|
||||||
onDelete: () => _confirmDelete(context, p),
|
|
||||||
)),
|
|
||||||
const SizedBox(height: 80),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -254,63 +392,102 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
|
|||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
heroTag: 'fab_team_${widget.team.id}',
|
heroTag: 'fab_team_${widget.team.id}',
|
||||||
onPressed: () => _controller.showAddPersonDialog(context, widget.team.id),
|
onPressed: () => _controller.showAddPersonDialog(context, widget.team.id),
|
||||||
backgroundColor: const Color(0xFF00C853),
|
backgroundColor: AppTheme.successGreen,
|
||||||
child: const Icon(Icons.add, color: Colors.white),
|
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmDelete(BuildContext context, Person person) {
|
void _confirmDelete(BuildContext context, Person person) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text("Eliminar Membro?"),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
content: Text("Tens a certeza que queres remover ${person.name}?"),
|
title: Text("Eliminar Membro?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
content: Text("Tens a certeza que queres remover ${person.name}?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar")),
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
await _controller.deletePerson(person.id);
|
Navigator.pop(ctx);
|
||||||
if (ctx.mounted) Navigator.pop(ctx);
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("A remover ${person.name}..."), duration: const Duration(seconds: 1)));
|
||||||
|
|
||||||
|
_controller.deletePerson(person).catchError((e) {
|
||||||
|
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erro: $e"), backgroundColor: AppTheme.primaryRed));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: const Text("Eliminar", style: TextStyle(color: Colors.red)),
|
child: const Text("Eliminar", style: TextStyle(color: AppTheme.primaryRed)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. CONTROLLER
|
// 3. CONTROLLER
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
class StatsController {
|
class StatsController {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
Stream<List<Person>> getMembers(String teamId) {
|
Stream<List<Person>> getMembers(String teamId) {
|
||||||
return _supabase
|
return _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', teamId).map((data) => data.map((json) => Person.fromMap(json)).toList());
|
||||||
.from('members')
|
|
||||||
.stream(primaryKey: ['id'])
|
|
||||||
.eq('team_id', teamId)
|
|
||||||
.order('name', ascending: true)
|
|
||||||
.map((data) => data.map((json) => Person.fromMap(json)).toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deletePerson(String personId) async {
|
String? extractPathFromUrl(String url, String bucket) {
|
||||||
|
if (url.isEmpty) return null;
|
||||||
|
final parts = url.split('/$bucket/');
|
||||||
|
if (parts.length > 1) return parts.last;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePerson(Person person) async {
|
||||||
try {
|
try {
|
||||||
await _supabase.from('members').delete().eq('id', personId);
|
await _supabase.from('members').delete().eq('id', person.id);
|
||||||
|
|
||||||
|
if (person.imageUrl != null && person.imageUrl!.isNotEmpty) {
|
||||||
|
final path = extractPathFromUrl(person.imageUrl!, 'avatars');
|
||||||
|
if (path != null) await _supabase.storage.from('avatars').remove([path]);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Erro ao eliminar: $e");
|
debugPrint("Erro ao eliminar: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showAddPersonDialog(BuildContext context, String teamId) {
|
void showAddPersonDialog(BuildContext context, String teamId) { _showForm(context, teamId: teamId); }
|
||||||
_showForm(context, teamId: teamId);
|
void showEditPersonDialog(BuildContext context, String teamId, Person person) { _showForm(context, teamId: teamId, person: person); }
|
||||||
}
|
|
||||||
|
|
||||||
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
|
Future<File?> pickAndCropImage(BuildContext context) async {
|
||||||
_showForm(context, teamId: teamId, person: person);
|
final picker = ImagePicker();
|
||||||
|
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
|
||||||
|
if (pickedFile == null) return null;
|
||||||
|
|
||||||
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
|
sourcePath: pickedFile.path,
|
||||||
|
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
|
||||||
|
uiSettings: [
|
||||||
|
AndroidUiSettings(
|
||||||
|
toolbarTitle: 'Recortar Foto',
|
||||||
|
toolbarColor: AppTheme.primaryRed,
|
||||||
|
toolbarWidgetColor: Colors.white,
|
||||||
|
initAspectRatio: CropAspectRatioPreset.square,
|
||||||
|
lockAspectRatio: true,
|
||||||
|
hideBottomControls: true,
|
||||||
|
),
|
||||||
|
IOSUiSettings(
|
||||||
|
title: 'Recortar Foto',
|
||||||
|
aspectRatioLockEnabled: true,
|
||||||
|
resetButtonHidden: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (croppedFile != null) {
|
||||||
|
return File(croppedFile.path);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showForm(BuildContext context, {required String teamId, Person? person}) {
|
void _showForm(BuildContext context, {required String teamId, Person? person}) {
|
||||||
@@ -319,37 +496,101 @@ class StatsController {
|
|||||||
final numCtrl = TextEditingController(text: person?.number ?? '');
|
final numCtrl = TextEditingController(text: person?.number ?? '');
|
||||||
String selectedType = person?.type ?? 'Jogador';
|
String selectedType = person?.type ?? 'Jogador';
|
||||||
|
|
||||||
|
File? selectedImage;
|
||||||
|
bool isUploading = false;
|
||||||
|
bool isPickerActive = false;
|
||||||
|
String? currentImageUrl = isEdit ? person.imageUrl : null;
|
||||||
|
|
||||||
|
String? nameError;
|
||||||
|
String? numError;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) => StatefulBuilder(
|
||||||
builder: (ctx, setState) => AlertDialog(
|
builder: (ctx, setState) => AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
title: Text(isEdit ? "Editar Membro" : "Novo Membro"),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
|
||||||
|
title: Text(isEdit ? "Editar Membro" : "Novo Membro", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (isPickerActive) return;
|
||||||
|
setState(() => isPickerActive = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final File? croppedFile = await pickAndCropImage(context);
|
||||||
|
if (croppedFile != null) {
|
||||||
|
setState(() => selectedImage = croppedFile);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setState(() => isPickerActive = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// 👇 PREVIEW DA FOTO NO POPUP SEM LAG 👇
|
||||||
|
ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 80 * context.sf,
|
||||||
|
height: 80 * context.sf,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05),
|
||||||
|
child: selectedImage != null
|
||||||
|
? Image.file(selectedImage!, fit: BoxFit.cover)
|
||||||
|
: (currentImageUrl != null && currentImageUrl!.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: currentImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey),
|
||||||
|
)
|
||||||
|
: Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0, right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(6 * context.sf),
|
||||||
|
decoration: BoxDecoration(color: AppTheme.primaryRed, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2)),
|
||||||
|
child: Icon(Icons.edit, color: Colors.white, size: 14 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20 * context.sf),
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
controller: nameCtrl,
|
controller: nameCtrl,
|
||||||
decoration: const InputDecoration(labelText: "Nome Completo"),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Nome Completo",
|
||||||
|
errorText: nameError,
|
||||||
|
),
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
SizedBox(height: 15 * context.sf),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: selectedType,
|
value: selectedType,
|
||||||
|
dropdownColor: Theme.of(context).colorScheme.surface,
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 16 * context.sf),
|
||||||
decoration: const InputDecoration(labelText: "Função"),
|
decoration: const InputDecoration(labelText: "Função"),
|
||||||
items: ["Jogador", "Treinador"]
|
items: ["Jogador", "Treinador"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
|
||||||
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
onChanged: (v) { if (v != null) setState(() => selectedType = v); },
|
||||||
.toList(),
|
|
||||||
onChanged: (v) {
|
|
||||||
if (v != null) setState(() => selectedType = v);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
if (selectedType == "Jogador") ...[
|
if (selectedType == "Jogador") ...[
|
||||||
const SizedBox(height: 15),
|
SizedBox(height: 15 * context.sf),
|
||||||
TextField(
|
TextField(
|
||||||
controller: numCtrl,
|
controller: numCtrl,
|
||||||
decoration: const InputDecoration(labelText: "Número da Camisola"),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Número da Camisola",
|
||||||
|
errorText: numError,
|
||||||
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -357,28 +598,45 @@ class StatsController {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
|
||||||
onPressed: () => Navigator.pop(ctx),
|
|
||||||
child: const Text("Cancelar")
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.successGreen, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * context.sf))),
|
||||||
backgroundColor: const Color(0xFF00C853),
|
onPressed: isUploading ? null : () async {
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
if (nameCtrl.text.trim().isEmpty) return;
|
|
||||||
|
|
||||||
String? numeroFinal = (selectedType == "Treinador")
|
setState(() {
|
||||||
? null
|
nameError = null;
|
||||||
: (numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim());
|
numError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nameCtrl.text.trim().isEmpty) {
|
||||||
|
setState(() => nameError = "O nome é obrigatório");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => isUploading = true);
|
||||||
|
|
||||||
|
String? numeroFinal = (selectedType == "Treinador") ? null : (numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String? finalImageUrl = currentImageUrl;
|
||||||
|
|
||||||
|
if (selectedImage != null) {
|
||||||
|
final fileName = 'person_${DateTime.now().millisecondsSinceEpoch}.png';
|
||||||
|
await _supabase.storage.from('avatars').upload(fileName, selectedImage!, fileOptions: const FileOptions(upsert: true));
|
||||||
|
finalImageUrl = _supabase.storage.from('avatars').getPublicUrl(fileName);
|
||||||
|
|
||||||
|
if (currentImageUrl != null && currentImageUrl!.isNotEmpty) {
|
||||||
|
final oldPath = extractPathFromUrl(currentImageUrl!, 'avatars');
|
||||||
|
if (oldPath != null) await _supabase.storage.from('avatars').remove([oldPath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await _supabase.from('members').update({
|
await _supabase.from('members').update({
|
||||||
'name': nameCtrl.text.trim(),
|
'name': nameCtrl.text.trim(),
|
||||||
'type': selectedType,
|
'type': selectedType,
|
||||||
'number': numeroFinal,
|
'number': numeroFinal,
|
||||||
|
'image_url': finalImageUrl,
|
||||||
}).eq('id', person.id);
|
}).eq('id', person.id);
|
||||||
} else {
|
} else {
|
||||||
await _supabase.from('members').insert({
|
await _supabase.from('members').insert({
|
||||||
@@ -386,23 +644,24 @@ class StatsController {
|
|||||||
'name': nameCtrl.text.trim(),
|
'name': nameCtrl.text.trim(),
|
||||||
'type': selectedType,
|
'type': selectedType,
|
||||||
'number': numeroFinal,
|
'number': numeroFinal,
|
||||||
|
'image_url': finalImageUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ctx.mounted) Navigator.pop(ctx);
|
if (ctx.mounted) Navigator.pop(ctx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Erro Supabase: $e");
|
setState(() {
|
||||||
if (ctx.mounted) {
|
isUploading = false;
|
||||||
String errorMsg = "Erro ao guardar: $e";
|
if (e is PostgrestException && e.code == '23505') {
|
||||||
if (e.toString().contains('unique')) {
|
numError = "Este número já está em uso!";
|
||||||
errorMsg = "Já existe um membro com este numero na equipa.";
|
} else if (e.toString().toLowerCase().contains('unique') || e.toString().toLowerCase().contains('duplicate')) {
|
||||||
}
|
numError = "Este número já está em uso!";
|
||||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
} else {
|
||||||
SnackBar(content: Text(errorMsg), backgroundColor: Colors.red)
|
nameError = "Erro ao guardar. Tente novamente.";
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
|
child: isUploading ? SizedBox(width: 16 * context.sf, height: 16 * context.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text("Guardar"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
// Esta extensão adiciona o superpoder "sf" ao BuildContext
|
|
||||||
extension SizeExtension on BuildContext {
|
extension SizeExtension on BuildContext {
|
||||||
|
|
||||||
double get sf {
|
double get sf {
|
||||||
final double wScreen = MediaQuery.of(this).size.width;
|
final Size size = MediaQuery.of(this).size;
|
||||||
final double hScreen = MediaQuery.of(this).size.height;
|
|
||||||
|
|
||||||
// Calcula e devolve a escala na hora!
|
// 1. Definimos os valores base do design (geralmente feitos no Figma/Adobe XD)
|
||||||
return math.min(wScreen, hScreen) / 400;
|
const double baseWidth = 375;
|
||||||
|
const double baseHeight = 812;
|
||||||
|
|
||||||
|
// 2. Calculamos o rácio de largura e altura
|
||||||
|
double scaleW = size.width / baseWidth;
|
||||||
|
double scaleH = size.height / baseHeight;
|
||||||
|
|
||||||
|
// 3. Usamos a média ou o menor valor para manter a proporção
|
||||||
|
// O 'min' evita que o texto estique demasiado se o ecrã for muito alto ou largo
|
||||||
|
double scale = math.min(scaleW, scaleH);
|
||||||
|
|
||||||
|
// 4. Segurança (Clamping): Não deixa as coisas ficarem minúsculas
|
||||||
|
// nem exageradamente grandes em tablets.
|
||||||
|
return scale.clamp(0.8, 1.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atalhos úteis para facilitar o código
|
||||||
|
double get screenWidth => MediaQuery.of(this).size.width;
|
||||||
|
double get screenHeight => MediaQuery.of(this).size.height;
|
||||||
|
|
||||||
|
// Verifica se é Tablet (opcional)
|
||||||
|
bool get isTablet => screenWidth > 600;
|
||||||
}
|
}
|
||||||
@@ -1,104 +1,83 @@
|
|||||||
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/classe/theme.dart'; // 👇 IMPORT DO TEMA!
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../controllers/game_controller.dart';
|
import '../controllers/game_controller.dart';
|
||||||
|
|
||||||
// --- CARD DE EXIBIÇÃO DO JOGO ---
|
|
||||||
class GameResultCard extends StatelessWidget {
|
class GameResultCard extends StatelessWidget {
|
||||||
final String gameId;
|
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
||||||
final String myTeam, opponentTeam, myScore, opponentScore, status, season;
|
final String? myTeamLogo, opponentTeamLogo;
|
||||||
final String? myTeamLogo;
|
final double sf;
|
||||||
final String? opponentTeamLogo;
|
|
||||||
final double sf; // NOVA VARIÁVEL DE ESCALA
|
|
||||||
|
|
||||||
const GameResultCard({
|
const GameResultCard({
|
||||||
super.key,
|
super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
|
||||||
required this.gameId,
|
required this.myScore, required this.opponentScore, required this.status, required this.season,
|
||||||
required this.myTeam,
|
this.myTeamLogo, this.opponentTeamLogo, required this.sf,
|
||||||
required this.opponentTeam,
|
|
||||||
required this.myScore,
|
|
||||||
required this.opponentScore,
|
|
||||||
required this.status,
|
|
||||||
required this.season,
|
|
||||||
this.myTeamLogo,
|
|
||||||
this.opponentTeamLogo,
|
|
||||||
required this.sf, // OBRIGATÓRIO RECEBER A ESCALA
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// 👇 Puxa as cores de fundo dependendo do Modo (Claro/Escuro)
|
||||||
|
final bgColor = Theme.of(context).colorScheme.surface;
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(bottom: 16 * sf),
|
margin: EdgeInsets.only(bottom: 16 * sf),
|
||||||
padding: EdgeInsets.all(16 * sf),
|
padding: EdgeInsets.all(16 * sf),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: bgColor, // Usa a cor do tema
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
borderRadius: BorderRadius.circular(20 * sf),
|
||||||
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
|
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)),
|
Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), // Usa o primaryRed
|
||||||
_buildScoreCenter(context, gameId, sf),
|
_buildScoreCenter(context, gameId, sf),
|
||||||
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)),
|
Expanded(child: _buildTeamInfo(opponentTeam, textColor, opponentTeamLogo, sf, textColor)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) {
|
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24 * sf, // Ajuste do tamanho do logo
|
radius: 24 * sf,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty)
|
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
|
||||||
? NetworkImage(logoUrl)
|
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
|
||||||
: null,
|
|
||||||
child: (logoUrl == null || logoUrl.isEmpty)
|
|
||||||
? Icon(Icons.shield, color: Colors.white, size: 24 * sf)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 6 * sf),
|
SizedBox(height: 6 * sf),
|
||||||
Text(name,
|
Text(name,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), // Adapta à noite/dia
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 2, // Permite 2 linhas para nomes compridos não cortarem
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScoreCenter(BuildContext context, String id, double sf) {
|
Widget _buildScoreCenter(BuildContext context, String id, double sf) {
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_scoreBox(myScore, Colors.green, sf),
|
_scoreBox(myScore, AppTheme.successGreen, sf), // Verde do tema
|
||||||
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)),
|
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)),
|
||||||
_scoreBox(opponentScore, Colors.grey, sf),
|
_scoreBox(opponentScore, Colors.grey, sf),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 10 * sf),
|
SizedBox(height: 10 * sf),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
|
||||||
Navigator.push(
|
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed),
|
||||||
context,
|
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)),
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PlacarPage(
|
|
||||||
gameId: id,
|
|
||||||
myTeam: myTeam,
|
|
||||||
opponentTeam: opponentTeam,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)),
|
|
||||||
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
|
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1),
|
backgroundColor: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
|
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
@@ -116,203 +95,3 @@ class GameResultCard extends StatelessWidget {
|
|||||||
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- POPUP DE CRIAÇÃO ---
|
|
||||||
class CreateGameDialogManual extends StatefulWidget {
|
|
||||||
final TeamController teamController;
|
|
||||||
final GameController gameController;
|
|
||||||
final double sf; // NOVA VARIÁVEL DE ESCALA
|
|
||||||
|
|
||||||
const CreateGameDialogManual({
|
|
||||||
super.key,
|
|
||||||
required this.teamController,
|
|
||||||
required this.gameController,
|
|
||||||
required this.sf,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|
||||||
late TextEditingController _seasonController;
|
|
||||||
final TextEditingController _myTeamController = TextEditingController();
|
|
||||||
final TextEditingController _opponentController = TextEditingController();
|
|
||||||
|
|
||||||
bool _isLoading = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_seasonController = TextEditingController(text: _calculateSeason());
|
|
||||||
}
|
|
||||||
|
|
||||||
String _calculateSeason() {
|
|
||||||
final now = DateTime.now();
|
|
||||||
return now.month >= 7 ? "${now.year}/${(now.year + 1).toString().substring(2)}" : "${now.year - 1}/${now.year.toString().substring(2)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
|
|
||||||
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: _seasonController,
|
|
||||||
style: TextStyle(fontSize: 14 * widget.sf),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Temporada',
|
|
||||||
labelStyle: TextStyle(fontSize: 14 * widget.sf),
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 15 * widget.sf),
|
|
||||||
|
|
||||||
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
|
|
||||||
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 10 * widget.sf),
|
|
||||||
child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))
|
|
||||||
),
|
|
||||||
|
|
||||||
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))
|
|
||||||
),
|
|
||||||
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
|
|
||||||
),
|
|
||||||
onPressed: _isLoading ? null : () async {
|
|
||||||
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
|
|
||||||
setState(() => _isLoading = true);
|
|
||||||
|
|
||||||
String? newGameId = await widget.gameController.createGame(
|
|
||||||
_myTeamController.text,
|
|
||||||
_opponentController.text,
|
|
||||||
_seasonController.text,
|
|
||||||
);
|
|
||||||
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
|
|
||||||
if (newGameId != null && context.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PlacarPage(
|
|
||||||
gameId: newGameId,
|
|
||||||
myTeam: _myTeamController.text,
|
|
||||||
opponentTeam: _opponentController.text,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: _isLoading
|
|
||||||
? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
||||||
: Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) {
|
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
|
||||||
stream: widget.teamController.teamsStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
List<Map<String, dynamic>> teamList = snapshot.hasData ? snapshot.data! : [];
|
|
||||||
|
|
||||||
return Autocomplete<Map<String, dynamic>>(
|
|
||||||
displayStringForOption: (Map<String, dynamic> option) => option['name'].toString(),
|
|
||||||
|
|
||||||
optionsBuilder: (TextEditingValue val) {
|
|
||||||
if (val.text.isEmpty) return const Iterable<Map<String, dynamic>>.empty();
|
|
||||||
return teamList.where((t) =>
|
|
||||||
t['name'].toString().toLowerCase().contains(val.text.toLowerCase()));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelected: (Map<String, dynamic> selection) {
|
|
||||||
controller.text = selection['name'].toString();
|
|
||||||
},
|
|
||||||
|
|
||||||
optionsViewBuilder: (context, onSelected, options) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Material(
|
|
||||||
elevation: 4.0,
|
|
||||||
borderRadius: BorderRadius.circular(8 * sf),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxHeight: 250 * sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: options.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final option = options.elementAt(index);
|
|
||||||
final String name = option['name'].toString();
|
|
||||||
final String? imageUrl = option['image_url'];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
radius: 20 * sf,
|
|
||||||
backgroundColor: Colors.grey.shade200,
|
|
||||||
backgroundImage: (imageUrl != null && imageUrl.isNotEmpty)
|
|
||||||
? NetworkImage(imageUrl)
|
|
||||||
: null,
|
|
||||||
child: (imageUrl == null || imageUrl.isEmpty)
|
|
||||||
? Icon(Icons.shield, color: Colors.grey, size: 20 * sf)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * sf)),
|
|
||||||
onTap: () {
|
|
||||||
onSelected(option);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
fieldViewBuilder: (ctx, txtCtrl, node, submit) {
|
|
||||||
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) {
|
|
||||||
txtCtrl.text = controller.text;
|
|
||||||
}
|
|
||||||
txtCtrl.addListener(() {
|
|
||||||
controller.text = txtCtrl.text;
|
|
||||||
});
|
|
||||||
|
|
||||||
return TextField(
|
|
||||||
controller: txtCtrl,
|
|
||||||
focusNode: node,
|
|
||||||
style: TextStyle(fontSize: 14 * sf),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
labelStyle: TextStyle(fontSize: 14 * sf),
|
|
||||||
prefixIcon: Icon(Icons.search, size: 20 * sf),
|
|
||||||
border: const OutlineInputBorder()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/home.config.dart';
|
||||||
|
|
||||||
class StatCard extends StatelessWidget {
|
class StatCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
@@ -10,11 +11,6 @@ class StatCard extends StatelessWidget {
|
|||||||
final bool isHighlighted;
|
final bool isHighlighted;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
// Variáveis novas para que o tamanho não fique preso à HomeConfig
|
|
||||||
final double sf;
|
|
||||||
final double cardWidth;
|
|
||||||
final double cardHeight;
|
|
||||||
|
|
||||||
const StatCard({
|
const StatCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
@@ -25,30 +21,27 @@ class StatCard extends StatelessWidget {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
this.isHighlighted = false,
|
this.isHighlighted = false,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.sf = 1.0, // Default 1.0 para não dar erro se não passares o valor
|
|
||||||
required this.cardWidth,
|
|
||||||
required this.cardHeight,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: cardWidth,
|
width: HomeConfig.cardwidthPadding,
|
||||||
height: cardHeight,
|
height: HomeConfig.cardheightPadding,
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
borderRadius: BorderRadius.circular(20),
|
||||||
side: isHighlighted
|
side: isHighlighted
|
||||||
? BorderSide(color: Colors.amber, width: 2 * sf)
|
? const BorderSide(color: Colors.amber, width: 2)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
borderRadius: BorderRadius.circular(20),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
@@ -59,14 +52,13 @@ class StatCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16.0 * sf),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Cabeçalho
|
// Cabeçalho
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -74,12 +66,12 @@ class StatCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title.toUpperCase(),
|
title.toUpperCase(),
|
||||||
style: TextStyle(fontSize: 11 * sf, fontWeight: FontWeight.bold, color: Colors.white70),
|
style: HomeConfig.titleStyle,
|
||||||
),
|
),
|
||||||
SizedBox(height: 2 * sf),
|
const SizedBox(height: 5),
|
||||||
Text(
|
Text(
|
||||||
playerName,
|
playerName,
|
||||||
style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: Colors.white),
|
style: HomeConfig.playerNameStyle,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -88,75 +80,50 @@ class StatCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (isHighlighted)
|
if (isHighlighted)
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(6 * sf),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.amber,
|
color: Colors.amber,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: const Icon(Icons.star, size: 20, color: Colors.white),
|
||||||
Icons.star,
|
|
||||||
size: 16 * sf,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
SizedBox(height: 8 * sf),
|
|
||||||
|
|
||||||
// Ícone
|
// Ícone
|
||||||
Container(
|
Container(
|
||||||
width: 45 * sf,
|
width: 60,
|
||||||
height: 45 * sf,
|
height: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.2),
|
color: Colors.white.withOpacity(0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(icon, size: 30, color: Colors.white),
|
||||||
icon,
|
|
||||||
size: 24 * sf,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
||||||
// Estatística
|
// Estatística
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(statValue, style: HomeConfig.statValueStyle),
|
||||||
statValue,
|
const SizedBox(height: 5),
|
||||||
style: TextStyle(fontSize: 34 * sf, fontWeight: FontWeight.bold, color: Colors.white),
|
Text(statLabel.toUpperCase(), style: HomeConfig.statLabelStyle),
|
||||||
),
|
|
||||||
SizedBox(height: 2 * sf),
|
|
||||||
Text(
|
|
||||||
statLabel.toUpperCase(),
|
|
||||||
style: TextStyle(fontSize: 12 * sf, color: Colors.white70),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
||||||
// Botão
|
// Botão
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: EdgeInsets.symmetric(vertical: 8 * sf),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.2),
|
color: Colors.white.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(10 * sf),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'VER DETALHES',
|
'VER DETALHES',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, letterSpacing: 1),
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 11 * sf,
|
|
||||||
letterSpacing: 1,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -177,7 +144,7 @@ class SportGrid extends StatelessWidget {
|
|||||||
const SportGrid({
|
const SportGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.children,
|
required this.children,
|
||||||
this.spacing = 20.0, // Valor padrão se não for passado nada
|
this.spacing = HomeConfig.cardSpacing,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -186,7 +153,6 @@ class SportGrid extends StatelessWidget {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Primeira linha
|
|
||||||
if (children.length >= 2)
|
if (children.length >= 2)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(bottom: spacing),
|
padding: EdgeInsets.only(bottom: spacing),
|
||||||
@@ -199,8 +165,6 @@ class SportGrid extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Segunda linha
|
|
||||||
if (children.length >= 4)
|
if (children.length >= 4)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/controllers/login_controller.dart';
|
import 'package:playmaker/controllers/login_controller.dart';
|
||||||
import 'package:playmaker/pages/RegisterPage.dart';
|
import 'package:playmaker/pages/RegisterPage.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
|
||||||
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class BasketTrackHeader extends StatelessWidget {
|
class BasketTrackHeader extends StatelessWidget {
|
||||||
const BasketTrackHeader({super.key});
|
const BasketTrackHeader({super.key});
|
||||||
@@ -10,33 +11,50 @@ class BasketTrackHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
// Usamos um Stack para controlar a sobreposição exata
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// 1. A Imagem (Aumentada para 320)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 200 * context.sf, // Ajusta o tamanho da imagem suavemente
|
width: 320 * context.sf,
|
||||||
height: 200 * context.sf,
|
height: 350 * context.sf,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/playmaker-logos.png',
|
'assets/playmaker-logos.png',
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 2. O Texto "subido" para dentro da área da imagem
|
||||||
|
Positioned(
|
||||||
|
bottom: 5 * context.sf, // Ajusta este valor para aproximar/afastar do centro da logo
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'BasketTrack',
|
'PlayMaker',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 36 * context.sf,
|
fontSize: 36 * context.sf,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey[900],
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 6 * context.sf),
|
SizedBox(height: 4 * context.sf),
|
||||||
Text(
|
Text(
|
||||||
'Gere as tuas equipas e estatísticas',
|
'Gere as tuas equipas e estatísticas',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16 * context.sf,
|
fontSize: 16 * context.sf,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Espaço extra para não bater nos campos de login logo a seguir
|
||||||
|
SizedBox(height: 10 * context.sf),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,13 +70,17 @@ class LoginFormFields extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller.emailController,
|
controller: controller.emailController,
|
||||||
style: TextStyle(fontSize: 15 * context.sf),
|
style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'E-mail',
|
labelText: 'E-mail',
|
||||||
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
||||||
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
|
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
|
||||||
errorText: controller.emailError,
|
errorText: controller.emailError,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * context.sf),
|
||||||
|
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2), // 👇 Cor do tema ao focar
|
||||||
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
@@ -67,16 +89,21 @@ class LoginFormFields extends StatelessWidget {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: controller.passwordController,
|
controller: controller.passwordController,
|
||||||
obscureText: controller.obscurePassword,
|
obscureText: controller.obscurePassword,
|
||||||
style: TextStyle(fontSize: 15 * context.sf),
|
style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Palavra-passe',
|
labelText: 'Palavra-passe',
|
||||||
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
||||||
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
|
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
|
||||||
errorText: controller.passwordError,
|
errorText: controller.passwordError,
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * context.sf),
|
||||||
|
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2), // 👇 Cor do tema ao focar
|
||||||
|
),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
||||||
size: 22 * context.sf
|
size: 22 * context.sf,
|
||||||
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
onPressed: controller.togglePasswordVisibility,
|
onPressed: controller.togglePasswordVisibility,
|
||||||
),
|
),
|
||||||
@@ -106,7 +133,7 @@ class LoginButton extends StatelessWidget {
|
|||||||
if (success) onLoginSuccess();
|
if (success) onLoginSuccess();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
backgroundColor: AppTheme.primaryRed, // 👇 Usando a cor do tema
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
@@ -135,8 +162,8 @@ class CreateAccountButton extends StatelessWidget {
|
|||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFFE74C3C),
|
foregroundColor: AppTheme.primaryRed, // 👇 Usando a cor do tema
|
||||||
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf),
|
side: BorderSide(color: AppTheme.primaryRed, width: 2 * context.sf), // 👇 Usando a cor do tema
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
|
||||||
),
|
),
|
||||||
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
|
||||||
import '../controllers/register_controller.dart';
|
import '../controllers/register_controller.dart';
|
||||||
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
||||||
|
|
||||||
@@ -9,16 +10,20 @@ class RegisterHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.person_add_outlined, size: 100 * context.sf, color: const Color(0xFFE74C3C)),
|
Icon(Icons.person_add_outlined, size: 100 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
|
||||||
SizedBox(height: 10 * context.sf),
|
SizedBox(height: 10 * context.sf),
|
||||||
Text(
|
Text(
|
||||||
'Nova Conta',
|
'Nova Conta',
|
||||||
style: TextStyle(fontSize: 36 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[900]),
|
style: TextStyle(
|
||||||
|
fontSize: 36 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável ao Modo Escuro
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 5 * context.sf),
|
SizedBox(height: 5 * context.sf),
|
||||||
Text(
|
Text(
|
||||||
'Cria o teu perfil no BasketTrack',
|
'Cria o teu perfil no BasketTrack',
|
||||||
style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey[600], fontWeight: FontWeight.w500),
|
style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey, fontWeight: FontWeight.w500),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -45,12 +50,16 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: widget.controller.nameController,
|
controller: widget.controller.nameController,
|
||||||
style: TextStyle(fontSize: 15 * context.sf),
|
style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Nome Completo',
|
labelText: 'Nome Completo',
|
||||||
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
||||||
prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf),
|
prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * context.sf),
|
||||||
|
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2), // 👇 Destaque ao focar
|
||||||
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -59,12 +68,16 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: widget.controller.emailController,
|
controller: widget.controller.emailController,
|
||||||
validator: widget.controller.validateEmail,
|
validator: widget.controller.validateEmail,
|
||||||
style: TextStyle(fontSize: 15 * context.sf),
|
style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'E-mail',
|
labelText: 'E-mail',
|
||||||
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
||||||
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
|
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf, color: AppTheme.primaryRed),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * context.sf),
|
||||||
|
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2),
|
||||||
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
@@ -75,13 +88,17 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
|
|||||||
controller: widget.controller.passwordController,
|
controller: widget.controller.passwordController,
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
validator: widget.controller.validatePassword,
|
validator: widget.controller.validatePassword,
|
||||||
style: TextStyle(fontSize: 15 * context.sf),
|
style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Palavra-passe',
|
labelText: 'Palavra-passe',
|
||||||
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
||||||
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
|
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf, color: AppTheme.primaryRed),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * context.sf),
|
||||||
|
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2),
|
||||||
|
),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * context.sf),
|
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * context.sf, color: Colors.grey),
|
||||||
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
||||||
@@ -94,11 +111,15 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
|
|||||||
controller: widget.controller.confirmPasswordController,
|
controller: widget.controller.confirmPasswordController,
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
validator: widget.controller.validateConfirmPassword,
|
validator: widget.controller.validateConfirmPassword,
|
||||||
style: TextStyle(fontSize: 15 * context.sf),
|
style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Confirmar Palavra-passe',
|
labelText: 'Confirmar Palavra-passe',
|
||||||
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
labelStyle: TextStyle(fontSize: 15 * context.sf),
|
||||||
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf),
|
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf, color: AppTheme.primaryRed),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12 * context.sf),
|
||||||
|
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2),
|
||||||
|
),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
|
||||||
),
|
),
|
||||||
@@ -121,7 +142,7 @@ class RegisterButton extends StatelessWidget {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: controller.isLoading ? null : () => controller.signUp(context),
|
onPressed: controller.isLoading ? null : () => controller.signUp(context),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
backgroundColor: AppTheme.primaryRed, // 👇 Cor do tema
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ class PersonCard extends StatelessWidget {
|
|||||||
height: 45,
|
height: 45,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
||||||
child: Text(person.number, style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)),
|
child: Text(person.number ?? "J", style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)), ),
|
||||||
),
|
|
||||||
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -1,249 +1,232 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/screens/team_stats_page.dart';
|
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../models/person_model.dart';
|
||||||
|
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
|
||||||
|
|
||||||
class TeamCard extends StatelessWidget {
|
// --- CABEÇALHO ---
|
||||||
|
class StatsHeader extends StatelessWidget {
|
||||||
final Team team;
|
final Team team;
|
||||||
final TeamController controller;
|
|
||||||
final VoidCallback onFavoriteTap;
|
|
||||||
final double sf; // <-- Variável de escala
|
|
||||||
|
|
||||||
const TeamCard({
|
const StatsHeader({super.key, required this.team});
|
||||||
super.key,
|
|
||||||
required this.team,
|
|
||||||
required this.controller,
|
|
||||||
required this.onFavoriteTap,
|
|
||||||
required this.sf,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Container(
|
||||||
color: Colors.white,
|
padding: EdgeInsets.only(
|
||||||
elevation: 3,
|
top: 50 * context.sf,
|
||||||
margin: EdgeInsets.only(bottom: 12 * sf),
|
left: 20 * context.sf,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
|
right: 20 * context.sf,
|
||||||
child: ListTile(
|
bottom: 20 * context.sf
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
// --- 1. IMAGEM + FAVORITO ---
|
color: AppTheme.primaryRed, // 👇 Usando a cor do teu tema!
|
||||||
leading: Stack(
|
borderRadius: BorderRadius.only(
|
||||||
clipBehavior: Clip.none,
|
bottomLeft: Radius.circular(30 * context.sf),
|
||||||
|
bottomRight: Radius.circular(30 * context.sf)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 28 * sf,
|
radius: 24 * context.sf,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.white24,
|
||||||
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
||||||
? NetworkImage(team.imageUrl)
|
? NetworkImage(team.imageUrl)
|
||||||
: null,
|
: null,
|
||||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
||||||
? Text(
|
? Text(
|
||||||
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
team.imageUrl.isEmpty ? "🛡️" : team.imageUrl,
|
||||||
style: TextStyle(fontSize: 24 * sf),
|
style: TextStyle(fontSize: 20 * context.sf),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
Positioned(
|
SizedBox(width: 15 * context.sf),
|
||||||
left: -15 * sf,
|
Expanded(
|
||||||
top: -10 * sf,
|
child: Column(
|
||||||
child: IconButton(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
icon: Icon(
|
|
||||||
team.isFavorite ? Icons.star : Icons.star_border,
|
|
||||||
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
|
|
||||||
size: 28 * sf,
|
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
|
|
||||||
blurRadius: 4 * sf,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: onFavoriteTap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// --- 2. TÍTULO ---
|
|
||||||
title: Text(
|
|
||||||
team.name,
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
|
||||||
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
|
|
||||||
),
|
|
||||||
|
|
||||||
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
|
|
||||||
subtitle: Padding(
|
|
||||||
padding: EdgeInsets.only(top: 6.0 * sf),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
Text(
|
||||||
SizedBox(width: 4 * sf),
|
team.name,
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.bold),
|
||||||
// 👇 A CORREÇÃO ESTÁ AQUI: StreamBuilder em vez de FutureBuilder 👇
|
|
||||||
StreamBuilder<int>(
|
|
||||||
stream: controller.getPlayerCountStream(team.id),
|
|
||||||
initialData: 0,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final count = snapshot.data ?? 0;
|
|
||||||
return Text(
|
|
||||||
"$count Jogs.", // Abreviado para poupar espaço
|
|
||||||
style: TextStyle(
|
|
||||||
color: count > 0 ? Colors.green[700] : Colors.orange,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 13 * sf,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(width: 8 * sf),
|
|
||||||
Expanded( // Garante que a temporada se adapta se faltar espaço
|
|
||||||
child: Text(
|
|
||||||
"| ${team.season}",
|
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 13 * sf),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
team.season,
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 14 * context.sf)
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- 4. BOTÕES (Estatísticas e Apagar) ---
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
tooltip: 'Ver Estatísticas',
|
|
||||||
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => TeamStatsPage(team: team),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: 'Eliminar Equipa',
|
|
||||||
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf),
|
|
||||||
onPressed: () => _confirmDelete(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _confirmDelete(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)),
|
|
||||||
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
controller.deleteTeam(team.id);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DIALOG DE CRIAÇÃO ---
|
// --- CARD DE RESUMO ---
|
||||||
class CreateTeamDialog extends StatefulWidget {
|
class StatsSummaryCard extends StatelessWidget {
|
||||||
final Function(String name, String season, String imageUrl) onConfirm;
|
final int total;
|
||||||
final double sf; // Recebe a escala
|
|
||||||
|
|
||||||
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
|
const StatsSummaryCard({super.key, required this.total});
|
||||||
|
|
||||||
@override
|
|
||||||
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
|
||||||
final TextEditingController _nameController = TextEditingController();
|
|
||||||
final TextEditingController _imageController = TextEditingController();
|
|
||||||
String _selectedSeason = '2024/25';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
// 👇 Adaptável ao Modo Escuro
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
|
final cardColor = Theme.of(context).brightness == Brightness.dark
|
||||||
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)),
|
? const Color(0xFF1E1E1E)
|
||||||
content: SingleChildScrollView(
|
: Colors.white;
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Card(
|
||||||
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(20 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(20 * context.sf),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
Row(
|
||||||
controller: _nameController,
|
children: [
|
||||||
style: TextStyle(fontSize: 14 * widget.sf),
|
Icon(Icons.groups, color: AppTheme.primaryRed, size: 28 * context.sf), // 👇 Cor do tema
|
||||||
decoration: InputDecoration(
|
SizedBox(width: 10 * context.sf),
|
||||||
labelText: 'Nome da Equipa',
|
Text(
|
||||||
labelStyle: TextStyle(fontSize: 14 * widget.sf)
|
"Total de Membros",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
|
||||||
|
fontSize: 16 * context.sf,
|
||||||
|
fontWeight: FontWeight.w600
|
||||||
|
)
|
||||||
),
|
),
|
||||||
textCapitalization: TextCapitalization.words,
|
],
|
||||||
),
|
|
||||||
SizedBox(height: 15 * widget.sf),
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
value: _selectedSeason,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Temporada',
|
|
||||||
labelStyle: TextStyle(fontSize: 14 * widget.sf)
|
|
||||||
),
|
|
||||||
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
|
|
||||||
items: ['2023/24', '2024/25', '2025/26']
|
|
||||||
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (val) => setState(() => _selectedSeason = val!),
|
|
||||||
),
|
|
||||||
SizedBox(height: 15 * widget.sf),
|
|
||||||
TextField(
|
|
||||||
controller: _imageController,
|
|
||||||
style: TextStyle(fontSize: 14 * widget.sf),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'URL Imagem ou Emoji',
|
|
||||||
labelStyle: TextStyle(fontSize: 14 * widget.sf),
|
|
||||||
hintText: 'Ex: 🏀 ou https://...',
|
|
||||||
hintStyle: TextStyle(fontSize: 14 * widget.sf)
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
"$total",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
|
||||||
|
fontSize: 28 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
)
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (_nameController.text.trim().isNotEmpty) {
|
|
||||||
widget.onConfirm(
|
|
||||||
_nameController.text.trim(),
|
|
||||||
_selectedSeason,
|
|
||||||
_imageController.text.trim(),
|
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
|
|
||||||
|
// --- TÍTULO DE SECÇÃO ---
|
||||||
|
class StatsSectionTitle extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const StatsSectionTitle({super.key, required this.title});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface // 👇 Adaptável
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
Divider(color: Colors.grey.withOpacity(0.3)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CARD DA PESSOA (JOGADOR/TREINADOR) ---
|
||||||
|
class PersonCard extends StatelessWidget {
|
||||||
|
final Person person;
|
||||||
|
final bool isCoach;
|
||||||
|
final VoidCallback onEdit;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
|
const PersonCard({
|
||||||
|
super.key,
|
||||||
|
required this.person,
|
||||||
|
required this.isCoach,
|
||||||
|
required this.onEdit,
|
||||||
|
required this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 👇 Cores adaptáveis para o Card
|
||||||
|
final defaultBg = Theme.of(context).brightness == Brightness.dark
|
||||||
|
? const Color(0xFF1E1E1E)
|
||||||
|
: Colors.white;
|
||||||
|
|
||||||
|
final coachBg = Theme.of(context).brightness == Brightness.dark
|
||||||
|
? AppTheme.warningAmber.withOpacity(0.1) // Amarelo escuro se for modo noturno
|
||||||
|
: const Color(0xFFFFF9C4); // Amarelo claro original
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.only(top: 12 * context.sf),
|
||||||
|
elevation: 2,
|
||||||
|
color: isCoach ? coachBg : defaultBg,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 4 * context.sf),
|
||||||
|
leading: isCoach
|
||||||
|
? CircleAvatar(
|
||||||
|
radius: 22 * context.sf,
|
||||||
|
backgroundColor: AppTheme.warningAmber, // 👇 Cor do tema
|
||||||
|
child: Icon(Icons.person, color: Colors.white, size: 24 * context.sf)
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
width: 45 * context.sf,
|
||||||
|
height: 45 * context.sf,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryRed.withOpacity(0.1), // 👇 Cor do tema
|
||||||
|
borderRadius: BorderRadius.circular(10 * context.sf)
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
person.number ?? "J",
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.primaryRed, // 👇 Cor do tema
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16 * context.sf
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
person.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16 * context.sf,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.edit_outlined, color: Colors.blue, size: 22 * context.sf),
|
||||||
|
onPressed: onEdit,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 22 * context.sf), // 👇 Cor do tema
|
||||||
|
onPressed: onDelete,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
228
lib/zone_map_dialog.dart
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class ZoneMapDialog extends StatelessWidget {
|
||||||
|
final String playerName;
|
||||||
|
final bool isMake;
|
||||||
|
final bool is3PointAction; // 👇 AGORA O POP-UP SABE O QUE ARRASTASTE!
|
||||||
|
final Function(String zone, int points, double relativeX, double relativeY) onZoneSelected;
|
||||||
|
|
||||||
|
const ZoneMapDialog({
|
||||||
|
super.key,
|
||||||
|
required this.playerName,
|
||||||
|
required this.isMake,
|
||||||
|
required this.is3PointAction,
|
||||||
|
required this.onZoneSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Color headerColor = const Color(0xFFE88F15);
|
||||||
|
final Color yellowBackground = const Color(0xFFDFAB00);
|
||||||
|
|
||||||
|
final double screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
final double dialogHeight = screenHeight * 0.95;
|
||||||
|
final double dialogWidth = dialogHeight * 1.0;
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: yellowBackground,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
insetPadding: const EdgeInsets.all(10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: dialogHeight,
|
||||||
|
width: dialogWidth,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
color: headerColor,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isMake ? "Lançamento de $playerName (Marcou)" : "Lançamento de $playerName (Falhou)",
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 8,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => Navigator.pop(context),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.close, color: headerColor, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTapUp: (details) => _calculateAndReturnZone(context, details.localPosition, constraints.biggest),
|
||||||
|
child: CustomPaint(
|
||||||
|
size: Size(constraints.maxWidth, constraints.maxHeight),
|
||||||
|
painter: DebugPainter(is3PointAction: is3PointAction), // 👇 Passa a info para o desenhador
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateAndReturnZone(BuildContext context, Offset tap, Size size) {
|
||||||
|
final double w = size.width;
|
||||||
|
final double h = size.height;
|
||||||
|
final double x = tap.dx;
|
||||||
|
final double y = tap.dy;
|
||||||
|
final double basketX = w / 2;
|
||||||
|
|
||||||
|
final double margin = w * 0.10;
|
||||||
|
final double length = h * 0.35;
|
||||||
|
final double larguraDoArco = (w / 2) - margin;
|
||||||
|
final double alturaDoArco = larguraDoArco * 0.30;
|
||||||
|
final double totalArcoHeight = alturaDoArco * 4;
|
||||||
|
|
||||||
|
String zone = "";
|
||||||
|
int pts = 2;
|
||||||
|
|
||||||
|
// 1. SABER SE CLICOU NA ZONA DE 3 OU DE 2
|
||||||
|
bool is3 = false;
|
||||||
|
if (y < length) {
|
||||||
|
if (x < margin || x > w - margin) is3 = true;
|
||||||
|
} else {
|
||||||
|
double dx = x - basketX;
|
||||||
|
double dy = y - length;
|
||||||
|
double ellipse = (dx * dx) / (larguraDoArco * larguraDoArco) + (dy * dy) / (math.pow(totalArcoHeight / 2, 2));
|
||||||
|
if (ellipse > 1.0) is3 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 MAGIA AQUI: BLOQUEIA O CLIQUE NA ZONA ESCURA! 👇
|
||||||
|
if (is3PointAction && !is3) return; // Arrastou 3pts mas clicou na de 2pts -> IGNORA
|
||||||
|
if (!is3PointAction && is3) return; // Arrastou 2pts mas clicou na de 3pts -> IGNORA
|
||||||
|
|
||||||
|
double angle = math.atan2(y - length, x - basketX);
|
||||||
|
|
||||||
|
if (is3) {
|
||||||
|
pts = 3;
|
||||||
|
if (y < length) {
|
||||||
|
zone = (x < w / 2) ? "Canto Esquerdo (3pt)" : "Canto Direito (3pt)";
|
||||||
|
} else if (angle > 2.35) {
|
||||||
|
zone = "Ala Esquerda (3pt)";
|
||||||
|
} else if (angle < 0.78) {
|
||||||
|
zone = "Ala Direita (3pt)";
|
||||||
|
} else {
|
||||||
|
zone = "Topo (3pt)";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pts = 2;
|
||||||
|
final double pW = w * 0.28;
|
||||||
|
final double pH = h * 0.38;
|
||||||
|
if (x > basketX - pW / 2 && x < basketX + pW / 2 && y < pH) {
|
||||||
|
zone = "Garrafão";
|
||||||
|
} else {
|
||||||
|
if (y < length) {
|
||||||
|
zone = (x < w / 2) ? "Meia Distância (Canto Esq)" : "Meia Distância (Canto Dir)";
|
||||||
|
} else if (angle > 2.35) {
|
||||||
|
zone = "Meia Distância (Esq)";
|
||||||
|
} else if (angle < 0.78) {
|
||||||
|
zone = "Meia Distância (Dir)";
|
||||||
|
} else {
|
||||||
|
zone = "Meia Distância (Centro)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 A MUDANÇA ESTÁ AQUI! Passamos os dados e deixamos quem chamou decidir como fechar!
|
||||||
|
onZoneSelected(zone, pts, x / w, y / h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebugPainter extends CustomPainter {
|
||||||
|
final bool is3PointAction;
|
||||||
|
|
||||||
|
DebugPainter({required this.is3PointAction});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final double w = size.width;
|
||||||
|
final double h = size.height;
|
||||||
|
final double basketX = w / 2;
|
||||||
|
|
||||||
|
final Paint whiteStroke = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 2.0;
|
||||||
|
final Paint blackStroke = Paint()..color = Colors.black87..style = PaintingStyle.stroke..strokeWidth = 2.0;
|
||||||
|
|
||||||
|
final double margin = w * 0.10;
|
||||||
|
final double length = h * 0.35;
|
||||||
|
final double larguraDoArco = (w / 2) - margin;
|
||||||
|
final double alturaDoArco = larguraDoArco * 0.30;
|
||||||
|
final double totalArcoHeight = alturaDoArco * 4;
|
||||||
|
|
||||||
|
// DESENHA O CAMPO
|
||||||
|
canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke);
|
||||||
|
canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke);
|
||||||
|
canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke);
|
||||||
|
canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke);
|
||||||
|
canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke);
|
||||||
|
|
||||||
|
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
|
||||||
|
double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75));
|
||||||
|
double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25));
|
||||||
|
double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25));
|
||||||
|
|
||||||
|
canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke);
|
||||||
|
canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke);
|
||||||
|
|
||||||
|
final double pW = w * 0.28;
|
||||||
|
final double pH = h * 0.38;
|
||||||
|
canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke);
|
||||||
|
|
||||||
|
final double ftR = pW / 2;
|
||||||
|
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), 0, math.pi, false, blackStroke);
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke);
|
||||||
|
canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke);
|
||||||
|
|
||||||
|
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke);
|
||||||
|
canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke);
|
||||||
|
canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 👇 EFEITO DE ESCURECIMENTO (SHADOW) 👇
|
||||||
|
// ==========================================
|
||||||
|
final Paint shadowPaint = Paint()..color = Colors.black.withOpacity(0.75); // 75% escuro!
|
||||||
|
|
||||||
|
// Cria o molde da área de 2 pontos
|
||||||
|
Path path2pt = Path();
|
||||||
|
path2pt.moveTo(margin, 0);
|
||||||
|
path2pt.lineTo(margin, length);
|
||||||
|
// Faz o arco curvo da linha de 3 pontos
|
||||||
|
path2pt.arcTo(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), math.pi, -math.pi, false);
|
||||||
|
path2pt.lineTo(w - margin, 0);
|
||||||
|
path2pt.close();
|
||||||
|
|
||||||
|
if (is3PointAction) {
|
||||||
|
// Arrastou 3 Pontos -> Escurece a Zona de 2!
|
||||||
|
canvas.drawPath(path2pt, shadowPaint);
|
||||||
|
} else {
|
||||||
|
// Arrastou 2 Pontos -> Escurece a Zona de 3!
|
||||||
|
Path fullScreen = Path()..addRect(Rect.fromLTWH(0, 0, w, h));
|
||||||
|
Path path3pt = Path.combine(PathOperation.difference, fullScreen, path2pt);
|
||||||
|
canvas.drawPath(path3pt, shadowPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||||
|
}
|
||||||
@@ -6,13 +6,21 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <gtk/gtk_plugin.h>
|
#include <gtk/gtk_plugin.h>
|
||||||
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||||
|
printing_plugin_register_with_registrar(printing_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
gtk
|
gtk
|
||||||
|
printing
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import app_links
|
import app_links
|
||||||
|
import file_selector_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import printing
|
||||||
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
456
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -49,6 +57,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
barcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: barcode
|
||||||
|
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.9"
|
||||||
|
bidi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bidi
|
||||||
|
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.13"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -57,14 +81,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -89,6 +145,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -121,6 +185,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.1"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
excel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: excel
|
||||||
|
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -133,10 +213,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.2.0"
|
||||||
|
ffi_leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi_leak_tracker
|
||||||
|
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -145,6 +233,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.5"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -158,6 +278,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -166,6 +294,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.33"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -184,6 +320,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0"
|
version: "2.5.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
gotrue:
|
gotrue:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -200,6 +344,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -216,6 +368,102 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
image_cropper:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_cropper
|
||||||
|
sha256: "46c8f9aae51c8350b2a2982462f85a129e77b04675d35b09db5499437d7a996b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.0"
|
||||||
|
image_cropper_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_cropper_for_web
|
||||||
|
sha256: e09749714bc24c4e3b31fbafa2e5b7229b0ff23e8b14d4ba44bd723b77611a0f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
image_cropper_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_cropper_platform_interface
|
||||||
|
sha256: "886a30ec199362cdcc2fbb053b8e53347fbfb9dbbdaa94f9ff85622609f5e7ff"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+14"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+6"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
jwt_decode:
|
jwt_decode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -268,18 +516,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -296,6 +544,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
native_toolchain_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_c
|
||||||
|
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.6"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -304,6 +560,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -312,6 +576,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -360,6 +632,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
pdf:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pdf
|
||||||
|
sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.12.0"
|
||||||
|
pdf_widget_wrapper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdf_widget_wrapper
|
||||||
|
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -392,6 +688,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.6.0"
|
||||||
|
printing:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: printing
|
||||||
|
sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.14.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -400,6 +704,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
realtime_client:
|
realtime_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -408,6 +728,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -424,8 +752,24 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
shared_preferences:
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.1.0"
|
||||||
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.0"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
@@ -480,6 +824,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.1"
|
||||||
|
shimmer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shimmer
|
||||||
|
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -493,6 +845,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -541,6 +933,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.12.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -553,10 +953,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.7"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -629,6 +1029,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.5"
|
version: "3.1.5"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.3"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -669,6 +1077,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -677,6 +1093,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
yet_another_json_isolate:
|
yet_another_json_isolate:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -687,4 +1119,4 @@ packages:
|
|||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.38.1"
|
||||||
|
|||||||
16
pubspec.yaml
@@ -36,6 +36,15 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
provider: ^6.1.5+1
|
provider: ^6.1.5+1
|
||||||
supabase_flutter: ^2.12.0
|
supabase_flutter: ^2.12.0
|
||||||
|
image_picker: ^1.2.1
|
||||||
|
image_cropper: ^11.0.0
|
||||||
|
shimmer: ^3.0.0
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
shared_preferences: ^2.5.4
|
||||||
|
printing: ^5.14.3
|
||||||
|
pdf: ^3.12.0
|
||||||
|
excel: ^4.0.6
|
||||||
|
share_plus: ^13.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -59,6 +68,13 @@ flutter:
|
|||||||
- assets/playmaker-logo.png
|
- assets/playmaker-logo.png
|
||||||
- assets/campo.png
|
- assets/campo.png
|
||||||
- assets/playmaker-logos.png
|
- assets/playmaker-logos.png
|
||||||
|
- assets/assit.png
|
||||||
|
- assets/tov.png
|
||||||
|
- assets/stl.png
|
||||||
|
fonts:
|
||||||
|
- family: playmaker
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/MyFlutterApp.ttf
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,20 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <printing/printing_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AppLinksPluginCApiRegisterWithRegistrar(
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
PrintingPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
app_links
|
||||||
|
file_selector_windows
|
||||||
|
printing
|
||||||
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||