Videos na nuvem | atualização de desing (Appbar) | mudança no quiz

This commit is contained in:
Carlos Correia
2026-07-08 22:23:38 +01:00
parent a7b6d35026
commit 67b580778a
12 changed files with 885 additions and 660 deletions

View File

@@ -6,6 +6,7 @@ import 'package:lottie/lottie.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'main.dart' show supabase;
import 'widgets/app_gradients.dart';
import 'widgets/entrance.dart';
import 'widgets/tap_bounce.dart';
@@ -15,6 +16,12 @@ const Color _pink = Color(0xFFFF55A7);
/// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números.
final RegExp _namePattern = RegExp(r"^[a-zA-ZÀ-ÖØ-öø-ÿ' -]+$");
/// A conta existe no Supabase Auth mas os dados (perfil) já não existem na
/// base de dados — tratada como conta inexistente para efeitos de login.
class _AccountNotFoundException implements Exception {
const _AccountNotFoundException();
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -50,11 +57,10 @@ class _HomeScreenState extends State<HomeScreen> {
required String name,
required String email,
}) async {
await supabase.from('profiles').upsert({
'id': uid,
'name': name,
'email': email,
}).timeout(const Duration(seconds: 20));
await supabase
.from('profiles')
.upsert({'id': uid, 'name': name, 'email': email})
.timeout(const Duration(seconds: 20));
}
Future<void> _submit() async {
@@ -66,10 +72,34 @@ class _HomeScreenState extends State<HomeScreen> {
final password = _passwordController.text;
if (_isLogin) {
await supabase.auth.signInWithPassword(
final result = await supabase.auth.signInWithPassword(
email: email,
password: password,
);
final user = result.user;
if (user != null) {
final profile = await supabase
.from('profiles')
.select('id')
.eq('id', user.id)
.maybeSingle();
if (profile == null) {
// A conta existe no Auth mas os dados foram apagados (ex.: via
// "Apagar dados da conta" ou diretamente na base de dados).
// Trata como inexistente: limpa qualquer resquício e bloqueia.
unawaited(
supabase
.from('children')
.delete()
.eq('owner_id', user.id)
.catchError((_) => <Map<String, dynamic>>[]),
);
await supabase.auth.signOut();
throw const _AccountNotFoundException();
}
}
} else {
final name = _nameController.text.trim();
final response = await supabase.auth
@@ -81,14 +111,19 @@ class _HomeScreenState extends State<HomeScreen> {
throw StateError('Usuário não encontrado após criar conta.');
}
unawaited(
_persistRegistrationData(
uid: user.id,
name: name,
email: email,
).catchError((_) {}),
);
// Precisa de terminar antes de navegar: o AuthGate só mostra a app
// depois de confirmar que existe um perfil na base de dados.
await _persistRegistrationData(uid: user.id, name: name, email: email);
}
} on _AccountNotFoundException {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
),
),
);
} on AuthException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
@@ -412,56 +447,59 @@ class _AuthForm extends StatelessWidget {
),
const SizedBox(height: 20),
TapBounce(
child: SizedBox(
height: 50,
child: FilledButton(
style:
FilledButton.styleFrom(
backgroundColor: _teal,
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
),
).copyWith(
animationDuration: const Duration(milliseconds: 180),
splashFactory: InkSparkle.splashFactory,
overlayColor: WidgetStateProperty.resolveWith<Color?>((
states,
) {
if (states.contains(WidgetState.pressed)) {
return Colors.white.withValues(alpha: 0.14);
}
if (states.contains(WidgetState.hovered) ||
states.contains(WidgetState.focused)) {
return Colors.white.withValues(alpha: 0.08);
}
return null;
}),
),
onPressed: loading ? null : onSubmit,
child: loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: Colors.white,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(isLogin ? 'Entrar' : 'Criar Conta'),
const SizedBox(width: 8),
const Icon(
Icons.arrow_forward_rounded,
size: 18,
child: ClipRRect(
borderRadius: BorderRadius.circular(999),
child: DecoratedBox(
decoration: const BoxDecoration(gradient: kGreenButtonGradient),
child: SizedBox(
height: 50,
child: FilledButton(
style:
FilledButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
),
],
),
).copyWith(
animationDuration: const Duration(milliseconds: 180),
splashFactory: InkSparkle.splashFactory,
overlayColor: WidgetStateProperty.resolveWith<Color?>(
(states) {
if (states.contains(WidgetState.pressed)) {
return Colors.white.withValues(alpha: 0.14);
}
if (states.contains(WidgetState.hovered) ||
states.contains(WidgetState.focused)) {
return Colors.white.withValues(alpha: 0.08);
}
return null;
},
),
),
onPressed: loading ? null : onSubmit,
child: loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: Colors.white,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(isLogin ? 'Entrar' : 'Criar Conta'),
const SizedBox(width: 8),
const Icon(Icons.arrow_forward_rounded, size: 18),
],
),
),
),
),
),
),