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 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
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/name_input_formatter.dart';
|
||||
import 'widgets/pill_snackbar.dart';
|
||||
import 'widgets/tap_bounce.dart';
|
||||
|
||||
const Color _teal = Color(0xFF2F9E94);
|
||||
@@ -117,32 +120,22 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
} on _AccountNotFoundException {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
showPillSnackBar(
|
||||
context,
|
||||
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(_friendlyAuthError(e))));
|
||||
showPillSnackBar(context, _friendlyAuthError(e));
|
||||
} on TimeoutException {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
showPillSnackBar(
|
||||
context,
|
||||
'Tempo esgotado. Verifique sua conexão e tente novamente.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Erro: $e')));
|
||||
showPillSnackBar(context, 'Erro: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
@@ -403,6 +396,8 @@ class _AuthForm extends StatelessWidget {
|
||||
hintText: 'Digite seu nome',
|
||||
icon: Icons.person_outline_rounded,
|
||||
textInputAction: TextInputAction.next,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
inputFormatters: [CapitalizeFirstLetterFormatter()],
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return 'Informe seu nome';
|
||||
@@ -518,6 +513,8 @@ class _AuthTextField extends StatelessWidget {
|
||||
this.obscureText = false,
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.inputFormatters,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
@@ -527,6 +524,8 @@ class _AuthTextField extends StatelessWidget {
|
||||
final bool obscureText;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final TextCapitalization textCapitalization;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -547,6 +546,8 @@ class _AuthTextField extends StatelessWidget {
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
textCapitalization: textCapitalization,
|
||||
inputFormatters: inputFormatters,
|
||||
validator: validator,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
decoration: InputDecoration(
|
||||
|
||||
@@ -19,6 +19,8 @@ import 'widgets/animated_nav_icon.dart';
|
||||
import 'widgets/app_dialogs.dart';
|
||||
import 'widgets/app_gradients.dart';
|
||||
import 'widgets/entrance.dart';
|
||||
import 'widgets/name_input_formatter.dart';
|
||||
import 'widgets/pill_snackbar.dart';
|
||||
import 'widgets/tap_bounce.dart';
|
||||
|
||||
/// 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);
|
||||
return;
|
||||
}
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (!(await BrushingPrefs.canLogMore(scope))) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!'),
|
||||
),
|
||||
if (context.mounted) {
|
||||
showPillSnackBar(
|
||||
context,
|
||||
'Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await BrushingPrefs.logToday(scope);
|
||||
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;
|
||||
} catch (e) {
|
||||
if (!context.mounted) return null;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Erro ao adicionar criança: $e')));
|
||||
showPillSnackBar(context, 'Erro ao adicionar criança: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1550,9 +1552,7 @@ class _PerfilTabState extends State<_PerfilTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Erro ao enviar foto: $e')));
|
||||
showPillSnackBar(context, 'Erro ao enviar foto: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _updatingPhoto = false);
|
||||
}
|
||||
@@ -1563,7 +1563,6 @@ class _PerfilTabState extends State<_PerfilTab> {
|
||||
required String childId,
|
||||
required String childName,
|
||||
}) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final confirmed = await showConfirmDialog(
|
||||
context,
|
||||
title: 'Remover criança',
|
||||
@@ -1589,9 +1588,11 @@ class _PerfilTabState extends State<_PerfilTab> {
|
||||
|
||||
widget.onChildSelected(0, null, null);
|
||||
await _loadPerfilData();
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Criança removida')));
|
||||
if (context.mounted) showPillSnackBar(context, 'Criança removida');
|
||||
} 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 {
|
||||
if (_addingChild) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final result = await showModalBottomSheet<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -1693,9 +1693,7 @@ class _PerfilTabState extends State<_PerfilTab> {
|
||||
|
||||
if (!mounted) return;
|
||||
await _loadPerfilData();
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('Criança adicionada')),
|
||||
);
|
||||
if (context.mounted) showPillSnackBar(context, 'Criança adicionada');
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _addingChild = false);
|
||||
@@ -1721,15 +1719,11 @@ class _PerfilTabState extends State<_PerfilTab> {
|
||||
await _addAnotherChild(context, uid);
|
||||
}
|
||||
} on TimeoutException {
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tempo esgotado ao adicionar. Tente novamente.'),
|
||||
),
|
||||
);
|
||||
if (!mounted || !context.mounted) return;
|
||||
showPillSnackBar(context, 'Tempo esgotado ao adicionar. Tente novamente.');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(SnackBar(content: Text('Erro ao adicionar: $e')));
|
||||
if (!mounted || !context.mounted) return;
|
||||
showPillSnackBar(context, 'Erro ao adicionar: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _addingChild = false);
|
||||
}
|
||||
@@ -2220,6 +2214,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
|
||||
helpText: 'Data de nascimento',
|
||||
cancelText: 'Cancelar',
|
||||
confirmText: 'Confirmar',
|
||||
locale: const Locale('pt', 'PT'),
|
||||
);
|
||||
if (picked == null) return;
|
||||
setState(() {
|
||||
@@ -2278,6 +2273,8 @@ class _AddChildSheetState extends State<_AddChildSheet> {
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
inputFormatters: [CapitalizeFirstLetterFormatter()],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nome da criança',
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'dart:async';
|
||||
@@ -42,6 +43,13 @@ class MyApp extends StatelessWidget {
|
||||
scaffoldBackgroundColor: const Color(0xFFFFE2EF),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class Quiz1Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 6,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -83,6 +84,7 @@ class Quiz2Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 10,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -129,6 +131,7 @@ class Quiz3Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 8,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -175,6 +178,7 @@ class Quiz4Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 1,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -223,6 +227,7 @@ class Quiz5Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 7,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -264,12 +269,6 @@ class Quiz6Screen extends StatelessWidget {
|
||||
weight: 1,
|
||||
value: 'nao',
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não sei',
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
@@ -315,6 +314,7 @@ class Quiz7Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 2,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -361,6 +361,7 @@ class Quiz8Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 9,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -407,6 +408,7 @@ class Quiz9Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 3,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -453,6 +455,7 @@ class Quiz10Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -500,6 +503,7 @@ class Quiz11Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 5,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -541,12 +545,6 @@ class Quiz12Screen extends StatelessWidget {
|
||||
weight: 1,
|
||||
value: 'nao',
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não sei',
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
@@ -587,12 +585,6 @@ class Quiz13Screen extends StatelessWidget {
|
||||
weight: 1,
|
||||
value: 'nao',
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não sei',
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
@@ -633,12 +625,6 @@ class Quiz14Screen extends StatelessWidget {
|
||||
weight: 1,
|
||||
value: 'nao',
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não sei',
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
@@ -684,6 +670,7 @@ class Quiz15Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 11,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -730,6 +717,7 @@ class Quiz16Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 12,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
@@ -776,6 +764,7 @@ class Quiz17Screen extends StatelessWidget {
|
||||
description: 'Não tenho a certeza',
|
||||
weight: 1,
|
||||
value: 'nao_sei',
|
||||
helpVideoId: 13,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
|
||||
@@ -20,6 +20,7 @@ class QuizAnswer {
|
||||
required this.weight,
|
||||
this.imagePath,
|
||||
this.value,
|
||||
this.helpVideoId,
|
||||
});
|
||||
|
||||
final String title;
|
||||
@@ -27,6 +28,11 @@ class QuizAnswer {
|
||||
final int weight;
|
||||
final String? imagePath;
|
||||
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 {
|
||||
@@ -1013,6 +1019,15 @@ class _QuizAnswerPill extends StatelessWidget {
|
||||
bool get _isYes => (answer.value ?? '').trim().toLowerCase() == 'sim';
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final accent = _isYes
|
||||
@@ -1023,6 +1038,8 @@ class _QuizAnswerPill extends StatelessWidget {
|
||||
final borderColor = selected
|
||||
? const Color(0xFF2F9E94)
|
||||
: Colors.black.withValues(alpha: 0.10);
|
||||
final helpVideo = _helpVideo;
|
||||
final showHelp = selected && helpVideo != null;
|
||||
|
||||
return TapBounce(
|
||||
scale: 0.97,
|
||||
@@ -1033,7 +1050,7 @@ class _QuizAnswerPill extends StatelessWidget {
|
||||
color: selected
|
||||
? Colors.white.withValues(alpha: 0.92)
|
||||
: 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),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -1045,8 +1062,11 @@ class _QuizAnswerPill extends StatelessWidget {
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: onTap,
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
child: Padding(
|
||||
@@ -1112,6 +1132,70 @@ class _QuizAnswerPill extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
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 '../widgets/app_dialogs.dart';
|
||||
import '../widgets/entrance.dart';
|
||||
import '../widgets/pill_snackbar.dart';
|
||||
import '../widgets/tap_bounce.dart';
|
||||
import 'terms_screen.dart';
|
||||
|
||||
@@ -28,7 +29,6 @@ class _SettingsBodyState extends State<SettingsBody> {
|
||||
}
|
||||
|
||||
Future<void> _confirmDeleteAccountData() async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final confirmed = await showConfirmDialog(
|
||||
context,
|
||||
title: 'Apagar dados da conta',
|
||||
@@ -63,7 +63,7 @@ class _SettingsBodyState extends State<SettingsBody> {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(SnackBar(content: Text('Erro ao apagar: $e')));
|
||||
if (mounted) showPillSnackBar(context, 'Erro ao apagar: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _deletingAccount = false);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
import '../watched_videos_prefs.dart';
|
||||
import '../widgets/app_gradients.dart';
|
||||
import '../widgets/entrance.dart';
|
||||
import '../widgets/pill_snackbar.dart';
|
||||
import '../widgets/tap_bounce.dart';
|
||||
|
||||
// Video data structure - easily editable for future updates.
|
||||
@@ -173,9 +174,7 @@ Future<void> showVideoPlayerDialog(
|
||||
}) {
|
||||
if (video.youtubeId != null) {
|
||||
if (video.youtubeId!.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Vídeo ainda não disponível')));
|
||||
showPillSnackBar(context, 'Vídeo ainda não disponível');
|
||||
return Future.value();
|
||||
}
|
||||
return Navigator.of(context).push<void>(
|
||||
@@ -785,9 +784,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
||||
setState(() {
|
||||
_isInitialized = false;
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e')));
|
||||
showPillSnackBar(context, 'Erro ao carregar vídeo: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1049,9 +1046,7 @@ class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> {
|
||||
setState(() {
|
||||
_isInitialized = false;
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e')));
|
||||
showPillSnackBar(context, '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"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -432,6 +437,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -30,6 +30,8 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
Reference in New Issue
Block a user