tentar aresolver a home
This commit is contained in:
51
.github/copilot-instructions.md
vendored
Normal file
51
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
## PlayMaker — guidance for AI coding assistants
|
||||||
|
|
||||||
|
This file gives focused, actionable knowledge to quickly make safe edits in the PlayMaker Flutter app.
|
||||||
|
|
||||||
|
Key facts (big picture)
|
||||||
|
- App: Flutter (mobile + desktop) using Supabase for backend and SharedPreferences for local persistence.
|
||||||
|
- Data ownership: teams are per-user (teams.user_id). Team records include fields like `id`, `name`, `image_url`, `wins`, `losses`, `draws`, `is_favorite`.
|
||||||
|
- Global selection: the app exposes a single global active team via `lib/controllers/active_team.dart` — a `ValueNotifier<ActiveTeam?>` named `globalActiveTeam`. Use `loadGlobalTeam()` and `saveGlobalTeam()` to read/update both local (SharedPreferences) and Supabase (`profiles.selected_team_id`).
|
||||||
|
|
||||||
|
Where to look (important files)
|
||||||
|
- `lib/controllers/active_team.dart` — central logic: load/save active team, prefers favorite team after our recent change.
|
||||||
|
- `lib/controllers/team_controller.dart` — creates teams, toggles favorites and exposes `teamsStream` for realtime listing.
|
||||||
|
- `lib/pages/home.dart` — main UI that shows the selected team, reads/writes last_team_* prefs and uses `TeamController` streams.
|
||||||
|
- `lib/pages/status_page.dart` — shows detailed stats and accepts initialTeam* props; will use the same persistence keys as `home.dart` as a fallback.
|
||||||
|
- `lib/widgets/*.dart` — UI building blocks (game, placar, team widgets) that assume team names/logos are strings and use `globalActiveTeam`/selectedTeam state.
|
||||||
|
|
||||||
|
Project-specific conventions & patterns
|
||||||
|
- Single active team: stored locally under prefs keys `last_team_id`, `last_team_name`, `last_team_logo`, `last_team_wins`, `last_team_losses`, `last_team_draws`.
|
||||||
|
- Realtime lists: controllers expose Streams from Supabase (e.g., `teamsStream`) and UI uses `StreamBuilder`/`snapshot.data` expecting List<Map<String,dynamic>>.
|
||||||
|
- Favoriting: `teams.is_favorite` is a boolean; code expects at most one favorite per user. When marking favorite, clear other favorites for that user and set the favorite as the global active team (we updated `team_controller.toggleFavorite` to do this).
|
||||||
|
- Global state: prefer using `globalActiveTeam` instead of ad-hoc SharedPreferences reads when changing the current team — it notifies Home and Status pages automatically.
|
||||||
|
- Naming: Portuguese UI strings are used throughout (e.g., "Selecionar Equipa"), so preserve that when editing visible text.
|
||||||
|
|
||||||
|
Examples of common edits
|
||||||
|
- Changing how the active team is chosen: edit `loadGlobalTeam()` in `lib/controllers/active_team.dart`. Example behaviour implemented: prefer `teams.is_favorite == true`, then fallback to `profiles.selected_team_id`, then local prefs.
|
||||||
|
- Adding a field to teams: update Supabase schema, then adapt `TeamController.getTeamsWithStats()` and UI widgets under `lib/widgets/` to read the new field.
|
||||||
|
- Making UI reactive to team changes: call `saveGlobalTeam(ActiveTeam(...))` to update memory, Supabase profile and notify UI.
|
||||||
|
|
||||||
|
Developer workflows (how to build/test/debug)
|
||||||
|
- Install dependencies: `flutter pub get`.
|
||||||
|
- Run on iOS simulator: `flutter run -d ios` (macOS only). Android: `flutter run -d android`.
|
||||||
|
- Quick analyzer: `flutter analyze` or rely on the project's editor diagnostics.
|
||||||
|
- Running tests: There is a `test/widget_test.dart` — run `flutter test` to execute.
|
||||||
|
- When debugging Supabase flows locally, ensure `google-services.json` (Android) and iOS config files are present under `android/app/` and `ios/` as appropriate.
|
||||||
|
|
||||||
|
Safe-edit checklist for AI agents
|
||||||
|
- Prefer small, focused patches. Keep existing structure and naming conventions (Portuguese strings, `prefs` keys and Supabase table names).
|
||||||
|
- When touching team selection: update both local prefs and Supabase `profiles.selected_team_id` (use `saveGlobalTeam`).
|
||||||
|
- When changing persistence keys, update both `home.dart` and `status_page.dart` and `active_team.dart`.
|
||||||
|
- Avoid altering app-wide themes or widget contracts unless necessary; many widgets rely on team names/logos being non-null strings.
|
||||||
|
- If adding Supabase queries, filter by `user_id` unless deliberately global.
|
||||||
|
|
||||||
|
If you can't find something
|
||||||
|
- Search for the pref keys `last_team_id` / `last_team_name` / `last_team_logo` to locate all usages.
|
||||||
|
- Look for `globalActiveTeam` to see places that react to the active team.
|
||||||
|
|
||||||
|
Contact / next steps
|
||||||
|
- After applying changes that affect team selection flow, run `flutter analyze` and (if possible) `flutter test`.
|
||||||
|
- Ask for confirmation before changing user-visible Portuguese copy or database schema.
|
||||||
|
|
||||||
|
— end of guidance —
|
||||||
@@ -46,11 +46,28 @@ Future<void> loadGlobalTeam() async {
|
|||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
|
|
||||||
try {
|
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();
|
final profile = await supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
|
||||||
if (profile != null && profile['selected_team_id'] != null) {
|
if (profile != null && profile['selected_team_id'] != null) {
|
||||||
final dbTeamId = profile['selected_team_id'].toString();
|
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) {
|
if (teamData != null) {
|
||||||
final newTeam = ActiveTeam(
|
final newTeam = ActiveTeam(
|
||||||
id: teamData['id'].toString(),
|
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_losses', newTeam.losses);
|
||||||
await prefs.setInt('last_team_draws', newTeam.draws);
|
await prefs.setInt('last_team_draws', newTeam.draws);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:playmaker/controllers/active_team.dart';
|
||||||
|
|
||||||
class TeamController {
|
class TeamController {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
@@ -65,10 +66,34 @@ class TeamController {
|
|||||||
// 4. FAVORITAR
|
// 4. FAVORITAR
|
||||||
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
||||||
try {
|
try {
|
||||||
await _supabase
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
.from('teams')
|
if (userId == null) return;
|
||||||
.update({'is_favorite': !currentStatus})
|
|
||||||
.eq('id', teamId);
|
// If we're marking this team as favorite, clear other favorites for this user
|
||||||
|
if (!currentStatus) {
|
||||||
|
await _supabase.from('teams').update({'is_favorite': false}).eq('user_id', userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the chosen team's favorite flag
|
||||||
|
await _supabase.from('teams').update({'is_favorite': !currentStatus}).eq('id', teamId);
|
||||||
|
|
||||||
|
// If it became favorite, load its data and set global active team
|
||||||
|
if (!currentStatus) {
|
||||||
|
final teamData = await _supabase.from('teams').select().eq('id', teamId).maybeSingle();
|
||||||
|
if (teamData != null) {
|
||||||
|
final newTeam = ActiveTeam(
|
||||||
|
id: teamData['id'].toString(),
|
||||||
|
name: teamData['name'] ?? 'Desconhecido',
|
||||||
|
logo: teamData['image_url'],
|
||||||
|
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
|
||||||
|
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
|
||||||
|
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update global active team so UI reflects the favorite immediately
|
||||||
|
await saveGlobalTeam(newTeam);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("❌ Erro ao favoritar: $e");
|
print("❌ Erro ao favoritar: $e");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(100),
|
borderRadius: BorderRadius.circular(100),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
debugPrint('Home: settings button tapped');
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -290,14 +291,10 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showTeamSelector(BuildContext context) {
|
void _showTeamSelector(BuildContext context) {
|
||||||
showModalBottomSheet(
|
debugPrint('showTeamSelector called');
|
||||||
context: context,
|
final isWide = MediaQuery.of(context).size.width >= 900;
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
shape: RoundedRectangleBorder(
|
Widget builderContent(BuildContext ctx) {
|
||||||
borderRadius:
|
|
||||||
BorderRadius.vertical(top: Radius.circular(20 * context.sf)),
|
|
||||||
),
|
|
||||||
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) {
|
||||||
@@ -313,8 +310,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text("Nenhuma equipa criada.",
|
child: Text("Nenhuma equipa criada.",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color:
|
color: Theme.of(context).colorScheme.onSurface))),
|
||||||
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) {
|
Widget _buildHomeContent(BuildContext context) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:playmaker/classe/theme.dart';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
|
import '../controllers/active_team.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
class StatusPage extends StatefulWidget {
|
class StatusPage extends StatefulWidget {
|
||||||
@@ -44,6 +45,32 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
if (_selectedTeamId == null) {
|
if (_selectedTeamId == null) {
|
||||||
_loadSelectedTeamFallback();
|
_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) {
|
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<Map<String, dynamic>> _aggregateStats(
|
||||||
List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
||||||
Map<String, Map<String, dynamic>> aggregated = {};
|
Map<String, Map<String, dynamic>> aggregated = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user