Nova tela de login | Adaptaçao nova da AppBar | Animções novas
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'login_register/login_sheet.dart';
|
||||
import 'login_register/register_sheet.dart';
|
||||
import 'main.dart' show supabase;
|
||||
import 'widgets/entrance.dart';
|
||||
import 'widgets/tap_bounce.dart';
|
||||
|
||||
const Color _teal = Color(0xFF2F9E94);
|
||||
const Color _pink = Color(0xFFFF55A7);
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -14,199 +20,505 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
bool _paused = false;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
bool _isLogin = true;
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _switchTab(bool isLogin) {
|
||||
if (_isLogin == isLogin || _loading) return;
|
||||
setState(() => _isLogin = isLogin);
|
||||
_formKey.currentState?.reset();
|
||||
}
|
||||
|
||||
Future<void> _persistRegistrationData({
|
||||
required String uid,
|
||||
required String name,
|
||||
required String email,
|
||||
}) async {
|
||||
await supabase.from('profiles').upsert({
|
||||
'id': uid,
|
||||
'name': name,
|
||||
'email': email,
|
||||
}).timeout(const Duration(seconds: 20));
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text;
|
||||
|
||||
if (_isLogin) {
|
||||
await supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
} else {
|
||||
final name = _nameController.text.trim();
|
||||
final response = await supabase.auth
|
||||
.signUp(email: email, password: password, data: {'name': name})
|
||||
.timeout(const Duration(seconds: 20));
|
||||
|
||||
final user = response.user;
|
||||
if (user == null) {
|
||||
throw StateError('Usuário não encontrado após criar conta.');
|
||||
}
|
||||
|
||||
unawaited(
|
||||
_persistRegistrationData(
|
||||
uid: user.id,
|
||||
name: name,
|
||||
email: email,
|
||||
).catchError((_) {}),
|
||||
);
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(_friendlyAuthError(e))));
|
||||
} on TimeoutException {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Tempo esgotado. Verifique sua conexão e tente novamente.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Erro: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendlyAuthError(AuthException e) {
|
||||
switch (e.code) {
|
||||
case 'invalid_credentials':
|
||||
return 'Email ou senha incorretos.';
|
||||
case 'user_not_found':
|
||||
return 'Usuário não encontrado.';
|
||||
case 'email_exists':
|
||||
case 'user_already_exists':
|
||||
return 'Este email já está em uso.';
|
||||
case 'weak_password':
|
||||
return 'Senha fraca. Use pelo menos 6 caracteres.';
|
||||
default:
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Size size = MediaQuery.sizeOf(context);
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: _paused,
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.38,
|
||||
bottom: -size.width * 0.38,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.38,
|
||||
bottom: -size.width * 0.38,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 28, 24, 20),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FadeSlideIn(
|
||||
child: Text(
|
||||
'Check-Teeth Kids',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: _pink,
|
||||
height: 1.0,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 80),
|
||||
child: Text(
|
||||
'Organize a rotina de saúde oral com inteligência',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 140),
|
||||
child: _AuthTabSwitch(
|
||||
isLogin: _isLogin,
|
||||
onChanged: _switchTab,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 190),
|
||||
child: _AuthForm(
|
||||
formKey: _formKey,
|
||||
isLogin: _isLogin,
|
||||
loading: _loading,
|
||||
nameController: _nameController,
|
||||
emailController: _emailController,
|
||||
passwordController: _passwordController,
|
||||
onSubmit: _submit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthTabSwitch extends StatelessWidget {
|
||||
const _AuthTabSwitch({required this.isLogin, required this.onChanged});
|
||||
|
||||
final bool isLogin;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _AuthTab(
|
||||
label: 'Entrar',
|
||||
selected: isLogin,
|
||||
onTap: () => onChanged(true),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _AuthTab(
|
||||
label: 'Criar Conta',
|
||||
selected: !isLogin,
|
||||
onTap: () => onChanged(false),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthTab extends StatelessWidget {
|
||||
const _AuthTab({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TapBounce(
|
||||
scale: 0.97,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 42,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? _teal : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
color: selected ? Colors.white : _teal,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Check-Teeth Kids',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFF55A7),
|
||||
height: 1.0,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Cuidar do sorriso começa aqui.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF2F9E94).withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Acompanhe a saúde oral do seu filho com\ninformação segura e prevenção inteligente.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.35,
|
||||
color: Colors.black.withValues(alpha: 0.52),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: size.width * 0.78,
|
||||
child: _PrimaryButton(
|
||||
label: 'Cadastrar',
|
||||
onPressed: _openRegister,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: size.width * 0.78,
|
||||
child: _SecondaryButton(
|
||||
label: 'Entrar',
|
||||
onPressed: _openLogin,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openLogin() async {
|
||||
setState(() => _paused = true);
|
||||
try {
|
||||
await showLoginSheet(context);
|
||||
} finally {
|
||||
if (mounted) setState(() => _paused = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openRegister() async {
|
||||
setState(() => _paused = true);
|
||||
try {
|
||||
await showRegisterSheet(context);
|
||||
} finally {
|
||||
if (mounted) setState(() => _paused = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SecondaryButton extends StatelessWidget {
|
||||
const _SecondaryButton({required this.label, required this.onPressed});
|
||||
class _AuthForm extends StatelessWidget {
|
||||
const _AuthForm({
|
||||
required this.formKey,
|
||||
required this.isLogin,
|
||||
required this.loading,
|
||||
required this.nameController,
|
||||
required this.emailController,
|
||||
required this.passwordController,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final bool isLogin;
|
||||
final bool loading;
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController emailController;
|
||||
final TextEditingController passwordController;
|
||||
final VoidCallback onSubmit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color teal = Color(0xFF2F9E94);
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: teal,
|
||||
side: const BorderSide(color: teal, width: 1.6),
|
||||
shape: const StadiumBorder(),
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.5),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: Text(label),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrimaryButton extends StatelessWidget {
|
||||
const _PrimaryButton({required this.label, required this.onPressed});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color teal = const Color(0xFF2F9E94);
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: FilledButton(
|
||||
style:
|
||||
FilledButton.styleFrom(
|
||||
backgroundColor: teal,
|
||||
foregroundColor: Colors.white,
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15,
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: !isLogin
|
||||
? Column(
|
||||
children: [
|
||||
_AuthTextField(
|
||||
controller: nameController,
|
||||
hintText: 'Digite seu nome',
|
||||
icon: Icons.person_outline_rounded,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return 'Informe seu nome';
|
||||
if (value.length < 2) return 'Nome muito curto';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
_AuthTextField(
|
||||
controller: emailController,
|
||||
hintText: 'Digite seu email',
|
||||
icon: Icons.mail_outline_rounded,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return 'Informe seu email';
|
||||
if (!value.contains('@')) return 'Email inválido';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_AuthTextField(
|
||||
controller: passwordController,
|
||||
hintText: 'Digite sua senha',
|
||||
icon: Icons.lock_outline_rounded,
|
||||
obscureText: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
validator: (v) {
|
||||
final value = v ?? '';
|
||||
if (value.isEmpty) return 'Informe sua senha';
|
||||
if (value.length < 6) return 'Mínimo de 6 caracteres';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).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: onPressed,
|
||||
child: Text(label),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthTextField extends StatelessWidget {
|
||||
const _AuthTextField({
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
required this.icon,
|
||||
required this.validator,
|
||||
this.obscureText = false,
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
final IconData icon;
|
||||
final FormFieldValidator<String> validator;
|
||||
final bool obscureText;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
validator: validator,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black.withValues(alpha: 0.35),
|
||||
),
|
||||
prefixIcon: Icon(icon, color: _teal, size: 20),
|
||||
border: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user