quiz redirecionado aos videos | redesing da notificação
This commit is contained in:
@@ -2,12 +2,15 @@ import 'dart:async';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'main.dart' show supabase;
|
import 'main.dart' show supabase;
|
||||||
import 'widgets/app_gradients.dart';
|
import 'widgets/app_gradients.dart';
|
||||||
import 'widgets/entrance.dart';
|
import 'widgets/entrance.dart';
|
||||||
|
import 'widgets/name_input_formatter.dart';
|
||||||
|
import 'widgets/pill_snackbar.dart';
|
||||||
import 'widgets/tap_bounce.dart';
|
import 'widgets/tap_bounce.dart';
|
||||||
|
|
||||||
const Color _teal = Color(0xFF2F9E94);
|
const Color _teal = Color(0xFF2F9E94);
|
||||||
@@ -117,32 +120,22 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
} on _AccountNotFoundException {
|
} on _AccountNotFoundException {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showPillSnackBar(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text(
|
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
|
||||||
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, _friendlyAuthError(e));
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text(_friendlyAuthError(e))));
|
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showPillSnackBar(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text(
|
'Tempo esgotado. Verifique sua conexão e tente novamente.',
|
||||||
'Tempo esgotado. Verifique sua conexão e tente novamente.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, 'Erro: $e');
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Erro: $e')));
|
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _loading = false);
|
if (mounted) setState(() => _loading = false);
|
||||||
}
|
}
|
||||||
@@ -403,6 +396,8 @@ class _AuthForm extends StatelessWidget {
|
|||||||
hintText: 'Digite seu nome',
|
hintText: 'Digite seu nome',
|
||||||
icon: Icons.person_outline_rounded,
|
icon: Icons.person_outline_rounded,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
inputFormatters: [CapitalizeFirstLetterFormatter()],
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
final value = (v ?? '').trim();
|
final value = (v ?? '').trim();
|
||||||
if (value.isEmpty) return 'Informe seu nome';
|
if (value.isEmpty) return 'Informe seu nome';
|
||||||
@@ -518,6 +513,8 @@ class _AuthTextField extends StatelessWidget {
|
|||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
this.textInputAction,
|
this.textInputAction,
|
||||||
|
this.textCapitalization = TextCapitalization.none,
|
||||||
|
this.inputFormatters,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -527,6 +524,8 @@ class _AuthTextField extends StatelessWidget {
|
|||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
final TextInputType? keyboardType;
|
final TextInputType? keyboardType;
|
||||||
final TextInputAction? textInputAction;
|
final TextInputAction? textInputAction;
|
||||||
|
final TextCapitalization textCapitalization;
|
||||||
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -547,6 +546,8 @@ class _AuthTextField extends StatelessWidget {
|
|||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
|
textCapitalization: textCapitalization,
|
||||||
|
inputFormatters: inputFormatters,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import 'widgets/animated_nav_icon.dart';
|
|||||||
import 'widgets/app_dialogs.dart';
|
import 'widgets/app_dialogs.dart';
|
||||||
import 'widgets/app_gradients.dart';
|
import 'widgets/app_gradients.dart';
|
||||||
import 'widgets/entrance.dart';
|
import 'widgets/entrance.dart';
|
||||||
|
import 'widgets/name_input_formatter.dart';
|
||||||
|
import 'widgets/pill_snackbar.dart';
|
||||||
import 'widgets/tap_bounce.dart';
|
import 'widgets/tap_bounce.dart';
|
||||||
|
|
||||||
/// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números.
|
/// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números.
|
||||||
@@ -806,18 +808,20 @@ class _InicioTab extends StatelessWidget {
|
|||||||
await _requireFirstChild(context, uid);
|
await _requireFirstChild(context, uid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
if (!(await BrushingPrefs.canLogMore(scope))) {
|
if (!(await BrushingPrefs.canLogMore(scope))) {
|
||||||
messenger.showSnackBar(
|
if (context.mounted) {
|
||||||
const SnackBar(
|
showPillSnackBar(
|
||||||
content: Text('Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!'),
|
context,
|
||||||
),
|
'Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await BrushingPrefs.logToday(scope);
|
await BrushingPrefs.logToday(scope);
|
||||||
await state?.refreshStats();
|
await state?.refreshStats();
|
||||||
messenger.showSnackBar(const SnackBar(content: Text('Escovagem registada!')));
|
if (context.mounted) {
|
||||||
|
showPillSnackBar(context, 'Escovagem registada!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,9 +1013,7 @@ Future<Map<String, dynamic>?> _createChildViaSheet(
|
|||||||
return inserted;
|
return inserted;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!context.mounted) return null;
|
if (!context.mounted) return null;
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, 'Erro ao adicionar criança: $e');
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Erro ao adicionar criança: $e')));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1550,9 +1552,7 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, 'Erro ao enviar foto: $e');
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Erro ao enviar foto: $e')));
|
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _updatingPhoto = false);
|
if (mounted) setState(() => _updatingPhoto = false);
|
||||||
}
|
}
|
||||||
@@ -1563,7 +1563,6 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
required String childId,
|
required String childId,
|
||||||
required String childName,
|
required String childName,
|
||||||
}) async {
|
}) async {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final confirmed = await showConfirmDialog(
|
final confirmed = await showConfirmDialog(
|
||||||
context,
|
context,
|
||||||
title: 'Remover criança',
|
title: 'Remover criança',
|
||||||
@@ -1589,9 +1588,11 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
|
|
||||||
widget.onChildSelected(0, null, null);
|
widget.onChildSelected(0, null, null);
|
||||||
await _loadPerfilData();
|
await _loadPerfilData();
|
||||||
messenger.showSnackBar(const SnackBar(content: Text('Criança removida')));
|
if (context.mounted) showPillSnackBar(context, 'Criança removida');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
messenger.showSnackBar(SnackBar(content: Text('Erro ao remover: $e')));
|
if (context.mounted) {
|
||||||
|
showPillSnackBar(context, 'Erro ao remover: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1666,7 +1667,6 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
|
|
||||||
Future<void> _addAnotherChild(BuildContext context, String uid) async {
|
Future<void> _addAnotherChild(BuildContext context, String uid) async {
|
||||||
if (_addingChild) return;
|
if (_addingChild) return;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final result = await showModalBottomSheet<Map<String, dynamic>?>(
|
final result = await showModalBottomSheet<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -1693,9 +1693,7 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await _loadPerfilData();
|
await _loadPerfilData();
|
||||||
messenger.showSnackBar(
|
if (context.mounted) showPillSnackBar(context, 'Criança adicionada');
|
||||||
const SnackBar(content: Text('Criança adicionada')),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _addingChild = false);
|
setState(() => _addingChild = false);
|
||||||
@@ -1721,15 +1719,11 @@ class _PerfilTabState extends State<_PerfilTab> {
|
|||||||
await _addAnotherChild(context, uid);
|
await _addAnotherChild(context, uid);
|
||||||
}
|
}
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
if (!mounted) return;
|
if (!mounted || !context.mounted) return;
|
||||||
messenger.showSnackBar(
|
showPillSnackBar(context, 'Tempo esgotado ao adicionar. Tente novamente.');
|
||||||
const SnackBar(
|
|
||||||
content: Text('Tempo esgotado ao adicionar. Tente novamente.'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted || !context.mounted) return;
|
||||||
messenger.showSnackBar(SnackBar(content: Text('Erro ao adicionar: $e')));
|
showPillSnackBar(context, 'Erro ao adicionar: $e');
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _addingChild = false);
|
if (mounted) setState(() => _addingChild = false);
|
||||||
}
|
}
|
||||||
@@ -2220,6 +2214,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
|
|||||||
helpText: 'Data de nascimento',
|
helpText: 'Data de nascimento',
|
||||||
cancelText: 'Cancelar',
|
cancelText: 'Cancelar',
|
||||||
confirmText: 'Confirmar',
|
confirmText: 'Confirmar',
|
||||||
|
locale: const Locale('pt', 'PT'),
|
||||||
);
|
);
|
||||||
if (picked == null) return;
|
if (picked == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -2278,6 +2273,8 @@ class _AddChildSheetState extends State<_AddChildSheet> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
inputFormatters: [CapitalizeFirstLetterFormatter()],
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Nome da criança',
|
labelText: 'Nome da criança',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@@ -42,6 +43,13 @@ class MyApp extends StatelessWidget {
|
|||||||
scaffoldBackgroundColor: const Color(0xFFFFE2EF),
|
scaffoldBackgroundColor: const Color(0xFFFFE2EF),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [Locale('pt', 'PT'), Locale('pt', 'BR')],
|
||||||
|
locale: const Locale('pt', 'PT'),
|
||||||
home: const DebugLaunchGate(),
|
home: const DebugLaunchGate(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class Quiz1Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 6,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -83,6 +84,7 @@ class Quiz2Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 10,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -129,6 +131,7 @@ class Quiz3Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 8,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -175,6 +178,7 @@ class Quiz4Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 1,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -223,6 +227,7 @@ class Quiz5Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 7,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -264,12 +269,6 @@ class Quiz6Screen extends StatelessWidget {
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao',
|
value: 'nao',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
|
||||||
title: 'Não sei',
|
|
||||||
description: 'Não tenho a certeza',
|
|
||||||
weight: 1,
|
|
||||||
value: 'nao_sei',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
@@ -315,6 +314,7 @@ class Quiz7Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -361,6 +361,7 @@ class Quiz8Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 9,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -407,6 +408,7 @@ class Quiz9Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 3,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -453,6 +455,7 @@ class Quiz10Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 4,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -500,6 +503,7 @@ class Quiz11Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 5,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -541,12 +545,6 @@ class Quiz12Screen extends StatelessWidget {
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao',
|
value: 'nao',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
|
||||||
title: 'Não sei',
|
|
||||||
description: 'Não tenho a certeza',
|
|
||||||
weight: 1,
|
|
||||||
value: 'nao_sei',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
@@ -587,12 +585,6 @@ class Quiz13Screen extends StatelessWidget {
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao',
|
value: 'nao',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
|
||||||
title: 'Não sei',
|
|
||||||
description: 'Não tenho a certeza',
|
|
||||||
weight: 1,
|
|
||||||
value: 'nao_sei',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
@@ -633,12 +625,6 @@ class Quiz14Screen extends StatelessWidget {
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao',
|
value: 'nao',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
|
||||||
title: 'Não sei',
|
|
||||||
description: 'Não tenho a certeza',
|
|
||||||
weight: 1,
|
|
||||||
value: 'nao_sei',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||||
@@ -684,6 +670,7 @@ class Quiz15Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 11,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -730,6 +717,7 @@ class Quiz16Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 12,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
@@ -776,6 +764,7 @@ class Quiz17Screen extends StatelessWidget {
|
|||||||
description: 'Não tenho a certeza',
|
description: 'Não tenho a certeza',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
value: 'nao_sei',
|
value: 'nao_sei',
|
||||||
|
helpVideoId: 13,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentScore: currentScore,
|
currentScore: currentScore,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class QuizAnswer {
|
|||||||
required this.weight,
|
required this.weight,
|
||||||
this.imagePath,
|
this.imagePath,
|
||||||
this.value,
|
this.value,
|
||||||
|
this.helpVideoId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -27,6 +28,11 @@ class QuizAnswer {
|
|||||||
final int weight;
|
final int weight;
|
||||||
final String? imagePath;
|
final String? imagePath;
|
||||||
final String? value;
|
final String? value;
|
||||||
|
|
||||||
|
/// Quando definido (normalmente só na resposta "Não sei"), identifica um
|
||||||
|
/// vídeo em [videoList] que ajuda a responder a esta pergunta — mostrado
|
||||||
|
/// como um botão que expande quando esta resposta é selecionada.
|
||||||
|
final int? helpVideoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class QuizQuestionScreen extends StatefulWidget {
|
class QuizQuestionScreen extends StatefulWidget {
|
||||||
@@ -1013,6 +1019,15 @@ class _QuizAnswerPill extends StatelessWidget {
|
|||||||
bool get _isYes => (answer.value ?? '').trim().toLowerCase() == 'sim';
|
bool get _isYes => (answer.value ?? '').trim().toLowerCase() == 'sim';
|
||||||
bool get _isNo => (answer.value ?? '').trim().toLowerCase() == 'nao';
|
bool get _isNo => (answer.value ?? '').trim().toLowerCase() == 'nao';
|
||||||
|
|
||||||
|
VideoData? get _helpVideo {
|
||||||
|
final id = answer.helpVideoId;
|
||||||
|
if (id == null) return null;
|
||||||
|
for (final v in videoList) {
|
||||||
|
if (v.id == id) return v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final accent = _isYes
|
final accent = _isYes
|
||||||
@@ -1023,6 +1038,8 @@ class _QuizAnswerPill extends StatelessWidget {
|
|||||||
final borderColor = selected
|
final borderColor = selected
|
||||||
? const Color(0xFF2F9E94)
|
? const Color(0xFF2F9E94)
|
||||||
: Colors.black.withValues(alpha: 0.10);
|
: Colors.black.withValues(alpha: 0.10);
|
||||||
|
final helpVideo = _helpVideo;
|
||||||
|
final showHelp = selected && helpVideo != null;
|
||||||
|
|
||||||
return TapBounce(
|
return TapBounce(
|
||||||
scale: 0.97,
|
scale: 0.97,
|
||||||
@@ -1033,7 +1050,7 @@ class _QuizAnswerPill extends StatelessWidget {
|
|||||||
color: selected
|
color: selected
|
||||||
? Colors.white.withValues(alpha: 0.92)
|
? Colors.white.withValues(alpha: 0.92)
|
||||||
: Colors.white.withValues(alpha: 0.70),
|
: Colors.white.withValues(alpha: 0.70),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(22),
|
||||||
border: Border.all(color: borderColor, width: selected ? 1.4 : 1.0),
|
border: Border.all(color: borderColor, width: selected ? 1.4 : 1.0),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -1045,71 +1062,138 @@ class _QuizAnswerPill extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(999),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
onTap: onTap,
|
children: [
|
||||||
splashFactory: InkSparkle.splashFactory,
|
InkWell(
|
||||||
child: Padding(
|
borderRadius: BorderRadius.circular(22),
|
||||||
padding: const EdgeInsets.symmetric(
|
onTap: onTap,
|
||||||
horizontal: 14,
|
splashFactory: InkSparkle.splashFactory,
|
||||||
vertical: 10,
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Row(
|
horizontal: 14,
|
||||||
children: [
|
vertical: 10,
|
||||||
Container(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: accent,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
_isYes
|
|
||||||
? Icons.check_rounded
|
|
||||||
: _isNo
|
|
||||||
? Icons.close_rounded
|
|
||||||
: Icons.help_outline_rounded,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 17,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Text(
|
Container(
|
||||||
answer.title,
|
width: 28,
|
||||||
style: const TextStyle(
|
height: 28,
|
||||||
fontWeight: FontWeight.w800,
|
decoration: BoxDecoration(
|
||||||
fontSize: 15,
|
color: accent,
|
||||||
color: Color(0xFF2F9E94),
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_isYes
|
||||||
|
? Icons.check_rounded
|
||||||
|
: _isNo
|
||||||
|
? Icons.close_rounded
|
||||||
|
: Icons.help_outline_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 17,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
),
|
Expanded(
|
||||||
AnimatedContainer(
|
child: Text(
|
||||||
duration: const Duration(milliseconds: 220),
|
answer.title,
|
||||||
width: 22,
|
style: const TextStyle(
|
||||||
height: 22,
|
fontWeight: FontWeight.w800,
|
||||||
decoration: BoxDecoration(
|
fontSize: 15,
|
||||||
shape: BoxShape.circle,
|
color: Color(0xFF2F9E94),
|
||||||
color: selected
|
),
|
||||||
? const Color(0xFF2F9E94)
|
),
|
||||||
: Colors.transparent,
|
|
||||||
border: Border.all(
|
|
||||||
color: selected
|
|
||||||
? const Color(0xFF2F9E94)
|
|
||||||
: Colors.black.withValues(alpha: 0.25),
|
|
||||||
width: 1.6,
|
|
||||||
),
|
),
|
||||||
),
|
AnimatedContainer(
|
||||||
child: selected
|
duration: const Duration(milliseconds: 220),
|
||||||
? const Icon(
|
width: 22,
|
||||||
Icons.check_rounded,
|
height: 22,
|
||||||
size: 14,
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
shape: BoxShape.circle,
|
||||||
)
|
color: selected
|
||||||
: null,
|
? const Color(0xFF2F9E94)
|
||||||
|
: Colors.transparent,
|
||||||
|
border: Border.all(
|
||||||
|
color: selected
|
||||||
|
? const Color(0xFF2F9E94)
|
||||||
|
: Colors.black.withValues(alpha: 0.25),
|
||||||
|
width: 1.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: selected
|
||||||
|
? const Icon(
|
||||||
|
Icons.check_rounded,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: !showHelp
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
|
||||||
|
child: _HelpVideoButton(video: helpVideo),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Botão que aparece quando a resposta "Não sei" é selecionada, sugerindo o
|
||||||
|
/// episódio que pode ajudar a esclarecer a dúvida antes de responder.
|
||||||
|
class _HelpVideoButton extends StatelessWidget {
|
||||||
|
const _HelpVideoButton({required this.video});
|
||||||
|
|
||||||
|
final VideoData video;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TapBounce(
|
||||||
|
scale: 0.97,
|
||||||
|
child: Material(
|
||||||
|
color: const Color(0xFF2F9E94).withValues(alpha: 0.10),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
onTap: () => showVideoPlayerDialog(context, video),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.play_circle_fill_rounded,
|
||||||
|
color: Color(0xFF2F9E94),
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Não tem a certeza? Veja o "${video.title}" para ajudar a responder',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 12.5,
|
||||||
|
color: Color(0xFF2F9E94),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
color: Color(0xFF2F9E94),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import '../main.dart' show supabase;
|
import '../main.dart' show supabase;
|
||||||
import '../widgets/app_dialogs.dart';
|
import '../widgets/app_dialogs.dart';
|
||||||
import '../widgets/entrance.dart';
|
import '../widgets/entrance.dart';
|
||||||
|
import '../widgets/pill_snackbar.dart';
|
||||||
import '../widgets/tap_bounce.dart';
|
import '../widgets/tap_bounce.dart';
|
||||||
import 'terms_screen.dart';
|
import 'terms_screen.dart';
|
||||||
|
|
||||||
@@ -28,7 +29,6 @@ class _SettingsBodyState extends State<SettingsBody> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _confirmDeleteAccountData() async {
|
Future<void> _confirmDeleteAccountData() async {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final confirmed = await showConfirmDialog(
|
final confirmed = await showConfirmDialog(
|
||||||
context,
|
context,
|
||||||
title: 'Apagar dados da conta',
|
title: 'Apagar dados da conta',
|
||||||
@@ -63,7 +63,7 @@ class _SettingsBodyState extends State<SettingsBody> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
messenger.showSnackBar(SnackBar(content: Text('Erro ao apagar: $e')));
|
if (mounted) showPillSnackBar(context, 'Erro ao apagar: $e');
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _deletingAccount = false);
|
if (mounted) setState(() => _deletingAccount = false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
|||||||
import '../watched_videos_prefs.dart';
|
import '../watched_videos_prefs.dart';
|
||||||
import '../widgets/app_gradients.dart';
|
import '../widgets/app_gradients.dart';
|
||||||
import '../widgets/entrance.dart';
|
import '../widgets/entrance.dart';
|
||||||
|
import '../widgets/pill_snackbar.dart';
|
||||||
import '../widgets/tap_bounce.dart';
|
import '../widgets/tap_bounce.dart';
|
||||||
|
|
||||||
// Video data structure - easily editable for future updates.
|
// Video data structure - easily editable for future updates.
|
||||||
@@ -173,9 +174,7 @@ Future<void> showVideoPlayerDialog(
|
|||||||
}) {
|
}) {
|
||||||
if (video.youtubeId != null) {
|
if (video.youtubeId != null) {
|
||||||
if (video.youtubeId!.isEmpty) {
|
if (video.youtubeId!.isEmpty) {
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, 'Vídeo ainda não disponível');
|
||||||
context,
|
|
||||||
).showSnackBar(const SnackBar(content: Text('Vídeo ainda não disponível')));
|
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
return Navigator.of(context).push<void>(
|
return Navigator.of(context).push<void>(
|
||||||
@@ -785,9 +784,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = false;
|
_isInitialized = false;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, 'Erro ao carregar vídeo: $e');
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e')));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1049,9 +1046,7 @@ class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = false;
|
_isInitialized = false;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(
|
showPillSnackBar(context, 'Erro ao carregar vídeo: $e');
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e')));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
lib/widgets/name_input_formatter.dart
Normal file
19
lib/widgets/name_input_formatter.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// Capitaliza automaticamente a primeira letra à medida que o utilizador
|
||||||
|
/// escreve — usado nos campos de nome (criança e conta) para não depender
|
||||||
|
/// só da sugestão do teclado (que o utilizador pode ignorar ou que pode
|
||||||
|
/// não existir em todos os teclados).
|
||||||
|
class CapitalizeFirstLetterFormatter extends TextInputFormatter {
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
final text = newValue.text;
|
||||||
|
if (text.isEmpty) return newValue;
|
||||||
|
final capitalized = text[0].toUpperCase() + text.substring(1);
|
||||||
|
if (capitalized == text) return newValue;
|
||||||
|
return newValue.copyWith(text: capitalized, selection: newValue.selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
lib/widgets/pill_snackbar.dart
Normal file
73
lib/widgets/pill_snackbar.dart
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Notificação em formato de pílula flutuante (fundo branco, texto rosa,
|
||||||
|
/// cantos totalmente arredondados, com um pequeno "pop" de entrada), usada
|
||||||
|
/// em toda a app em vez do SnackBar padrão do Material (barra preta a
|
||||||
|
/// ocupar a largura toda). O SnackBar continua a ser o mecanismo por trás
|
||||||
|
/// — só o visual muda: fica transparente/sem elevação e o conteúdo real é
|
||||||
|
/// este contentor central que abraça o texto em vez de esticar de ponta a
|
||||||
|
/// ponta.
|
||||||
|
void showPillSnackBar(BuildContext context, String message) {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
messenger
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
content: Center(child: _AnimatedPill(message: message)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pequeno "pop" de entrada (escala + fade, com leve ultrapassagem via
|
||||||
|
/// [Curves.easeOutBack]) para a pílula de notificação — em vez de aparecer
|
||||||
|
/// estática, dá-lhe uma sensação de vida ao surgir. Corre uma única vez,
|
||||||
|
/// automaticamente, assim que este widget é construído (cada chamada a
|
||||||
|
/// [showPillSnackBar] cria uma instância nova).
|
||||||
|
class _AnimatedPill extends StatelessWidget {
|
||||||
|
const _AnimatedPill({required this.message});
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0.0, end: 1.0),
|
||||||
|
duration: const Duration(milliseconds: 380),
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: value.clamp(0.0, 1.0),
|
||||||
|
child: Transform.scale(scale: value, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFFF55A7),
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
pubspec.lock
13
pubspec.lock
@@ -294,6 +294,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -432,6 +437,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.20.2"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Reference in New Issue
Block a user