tentar aresolver a home

This commit is contained in:
2026-06-08 14:54:04 +01:00
parent 7d2f3c4679
commit 947e119dba
5 changed files with 264 additions and 116 deletions

51
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,51 @@
## PlayMaker — guidance for AI coding assistants
This file gives focused, actionable knowledge to quickly make safe edits in the PlayMaker Flutter app.
Key facts (big picture)
- App: Flutter (mobile + desktop) using Supabase for backend and SharedPreferences for local persistence.
- Data ownership: teams are per-user (teams.user_id). Team records include fields like `id`, `name`, `image_url`, `wins`, `losses`, `draws`, `is_favorite`.
- Global selection: the app exposes a single global active team via `lib/controllers/active_team.dart` — a `ValueNotifier<ActiveTeam?>` named `globalActiveTeam`. Use `loadGlobalTeam()` and `saveGlobalTeam()` to read/update both local (SharedPreferences) and Supabase (`profiles.selected_team_id`).
Where to look (important files)
- `lib/controllers/active_team.dart` — central logic: load/save active team, prefers favorite team after our recent change.
- `lib/controllers/team_controller.dart` — creates teams, toggles favorites and exposes `teamsStream` for realtime listing.
- `lib/pages/home.dart` — main UI that shows the selected team, reads/writes last_team_* prefs and uses `TeamController` streams.
- `lib/pages/status_page.dart` — shows detailed stats and accepts initialTeam* props; will use the same persistence keys as `home.dart` as a fallback.
- `lib/widgets/*.dart` — UI building blocks (game, placar, team widgets) that assume team names/logos are strings and use `globalActiveTeam`/selectedTeam state.
Project-specific conventions & patterns
- Single active team: stored locally under prefs keys `last_team_id`, `last_team_name`, `last_team_logo`, `last_team_wins`, `last_team_losses`, `last_team_draws`.
- Realtime lists: controllers expose Streams from Supabase (e.g., `teamsStream`) and UI uses `StreamBuilder`/`snapshot.data` expecting List<Map<String,dynamic>>.
- Favoriting: `teams.is_favorite` is a boolean; code expects at most one favorite per user. When marking favorite, clear other favorites for that user and set the favorite as the global active team (we updated `team_controller.toggleFavorite` to do this).
- Global state: prefer using `globalActiveTeam` instead of ad-hoc SharedPreferences reads when changing the current team — it notifies Home and Status pages automatically.
- Naming: Portuguese UI strings are used throughout (e.g., "Selecionar Equipa"), so preserve that when editing visible text.
Examples of common edits
- Changing how the active team is chosen: edit `loadGlobalTeam()` in `lib/controllers/active_team.dart`. Example behaviour implemented: prefer `teams.is_favorite == true`, then fallback to `profiles.selected_team_id`, then local prefs.
- Adding a field to teams: update Supabase schema, then adapt `TeamController.getTeamsWithStats()` and UI widgets under `lib/widgets/` to read the new field.
- Making UI reactive to team changes: call `saveGlobalTeam(ActiveTeam(...))` to update memory, Supabase profile and notify UI.
Developer workflows (how to build/test/debug)
- Install dependencies: `flutter pub get`.
- Run on iOS simulator: `flutter run -d ios` (macOS only). Android: `flutter run -d android`.
- Quick analyzer: `flutter analyze` or rely on the project's editor diagnostics.
- Running tests: There is a `test/widget_test.dart` — run `flutter test` to execute.
- When debugging Supabase flows locally, ensure `google-services.json` (Android) and iOS config files are present under `android/app/` and `ios/` as appropriate.
Safe-edit checklist for AI agents
- Prefer small, focused patches. Keep existing structure and naming conventions (Portuguese strings, `prefs` keys and Supabase table names).
- When touching team selection: update both local prefs and Supabase `profiles.selected_team_id` (use `saveGlobalTeam`).
- When changing persistence keys, update both `home.dart` and `status_page.dart` and `active_team.dart`.
- Avoid altering app-wide themes or widget contracts unless necessary; many widgets rely on team names/logos being non-null strings.
- If adding Supabase queries, filter by `user_id` unless deliberately global.
If you can't find something
- Search for the pref keys `last_team_id` / `last_team_name` / `last_team_logo` to locate all usages.
- Look for `globalActiveTeam` to see places that react to the active team.
Contact / next steps
- After applying changes that affect team selection flow, run `flutter analyze` and (if possible) `flutter test`.
- Ask for confirmation before changing user-visible Portuguese copy or database schema.
— end of guidance —

View File

@@ -46,11 +46,28 @@ Future<void> loadGlobalTeam() async {
if (userId == null) return;
try {
// 1) Prefer an explicit team selection stored on the user's profile (if any)
Map<String, dynamic>? teamData;
final profile = await supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
if (profile != null && profile['selected_team_id'] != null) {
final dbTeamId = profile['selected_team_id'].toString();
final teamData = await supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
final dbTeam = await supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
if (dbTeam != null) teamData = Map<String, dynamic>.from(dbTeam);
}
// 2) If the user has no explicit profile selection, fall back to any team
// marked as favorite for that user (acts as a default)
if (teamData == null) {
final favTeam = await supabase
.from('teams')
.select()
.eq('user_id', userId)
.eq('is_favorite', true)
.maybeSingle();
if (favTeam != null) teamData = Map<String, dynamic>.from(favTeam);
}
// If we found a team (favorite or profile selection), set it as active and persist locally
if (teamData != null) {
final newTeam = ActiveTeam(
id: teamData['id'].toString(),
@@ -72,7 +89,6 @@ Future<void> loadGlobalTeam() async {
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");
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/controllers/active_team.dart';
class TeamController {
final _supabase = Supabase.instance.client;
@@ -65,10 +66,34 @@ class TeamController {
// 4. FAVORITAR
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
try {
await _supabase
.from('teams')
.update({'is_favorite': !currentStatus})
.eq('id', teamId);
final userId = _supabase.auth.currentUser?.id;
if (userId == null) return;
// If we're marking this team as favorite, clear other favorites for this user
if (!currentStatus) {
await _supabase.from('teams').update({'is_favorite': false}).eq('user_id', userId);
}
// Toggle the chosen team's favorite flag
await _supabase.from('teams').update({'is_favorite': !currentStatus}).eq('id', teamId);
// If it became favorite, load its data and set global active team
if (!currentStatus) {
final teamData = await _supabase.from('teams').select().eq('id', teamId).maybeSingle();
if (teamData != null) {
final newTeam = ActiveTeam(
id: teamData['id'].toString(),
name: teamData['name'] ?? 'Desconhecido',
logo: teamData['image_url'],
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
);
// Update global active team so UI reflects the favorite immediately
await saveGlobalTeam(newTeam);
}
}
} catch (e) {
print("❌ Erro ao favoritar: $e");
}

View File

@@ -220,6 +220,7 @@ class _HomeScreenState extends State<HomeScreen> {
child: InkWell(
borderRadius: BorderRadius.circular(100),
onTap: () async {
debugPrint('Home: settings button tapped');
await Navigator.push(
context,
MaterialPageRoute(
@@ -290,14 +291,10 @@ class _HomeScreenState extends State<HomeScreen> {
}
void _showTeamSelector(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(top: Radius.circular(20 * context.sf)),
),
builder: (context) {
debugPrint('showTeamSelector called');
final isWide = MediaQuery.of(context).size.width >= 900;
Widget builderContent(BuildContext ctx) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream,
builder: (context, snapshot) {
@@ -313,8 +310,7 @@ class _HomeScreenState extends State<HomeScreen> {
child: Center(
child: Text("Nenhuma equipa criada.",
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface))),
color: Theme.of(context).colorScheme.onSurface))),
);
}
@@ -380,8 +376,35 @@ class _HomeScreenState extends State<HomeScreen> {
);
},
);
}
if (isWide) {
showDialog(
context: context,
builder: (ctx) {
return Dialog(
insetPadding: EdgeInsets.symmetric(horizontal: 200 * context.sf, vertical: 80 * context.sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 600 * context.sf),
child: Padding(
padding: EdgeInsets.all(16 * context.sf),
child: builderContent(ctx),
),
),
);
},
);
} else {
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf)),
),
builder: (context) => builderContent(context),
);
}
}
Widget _buildHomeContent(BuildContext context) {

View File

@@ -4,6 +4,7 @@ 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/active_team.dart';
import '../utils/size_extension.dart';
class StatusPage extends StatefulWidget {
@@ -44,6 +45,32 @@ class _StatusPageState extends State<StatusPage> {
if (_selectedTeamId == null) {
_loadSelectedTeamFallback();
}
// Listen to global active team changes (e.g., when user marks favorite)
globalActiveTeam.addListener(_onGlobalActiveTeamChanged);
// Se já existe um globalActiveTeam no momento da abertura da página, aplica-o
final atNow = globalActiveTeam.value;
if (atNow != null) {
_selectedTeamId = atNow.id;
_selectedTeamName = atNow.name;
_selectedTeamLogo = atNow.logo;
}
}
void _onGlobalActiveTeamChanged() {
final at = globalActiveTeam.value;
if (!mounted) return;
// Atualiza sempre para a equipa ativa global (favorita). Isto força a Status
// a mostrar a equipa marcada como favorita assim que o utilizador a define.
if (at != null) {
setState(() {
_selectedTeamId = at.id;
_selectedTeamName = at.name;
_selectedTeamLogo = at.logo;
});
}
}
String _prefsKey(String key) {
@@ -238,6 +265,12 @@ class _StatusPageState extends State<StatusPage> {
);
}
@override
void dispose() {
globalActiveTeam.removeListener(_onGlobalActiveTeamChanged);
super.dispose();
}
List<Map<String, dynamic>> _aggregateStats(
List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
Map<String, Map<String, dynamic>> aggregated = {};