diff --git a/lib/Screens/add_item_screen.dart b/lib/Screens/add_item_screen.dart index 4f932ce..0940c55 100644 --- a/lib/Screens/add_item_screen.dart +++ b/lib/Screens/add_item_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../constants/item_categories.dart'; +import '../theme/app_theme.dart'; class AddItemScreen extends StatefulWidget { const AddItemScreen({super.key}); @@ -12,143 +13,94 @@ class AddItemScreen extends StatefulWidget { } class _AddItemScreenState extends State { - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _descriptionController = TextEditingController(); + final _nameController = TextEditingController(); + final _descController = TextEditingController(); + final _imagePicker = ImagePicker(); + XFile? _pickedImage; ItemCategory? _selectedCategory; Subcategory? _selectedSubcategory; final Set _selectedTags = {}; - XFile? _selectedImage; - bool _isLoading = false; - Future _pickImage(ImageSource source) async { - final picker = ImagePicker(); - final image = await picker.pickImage( - source: source, - maxWidth: 1024, - imageQuality: 80, - ); - if (image != null) { - setState(() => _selectedImage = image); - } - } - - Future _showImageSourcePicker() async { - final source = await showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (context) => SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon( - Icons.photo_camera, - color: Color(0xFF0066CC), - ), - title: const Text('Tirar foto'), - onTap: () => Navigator.pop(context, ImageSource.camera), - ), - ListTile( - leading: const Icon( - Icons.photo_library, - color: Color(0xFF0066CC), - ), - title: const Text('Escolher da galeria'), - onTap: () => Navigator.pop(context, ImageSource.gallery), - ), - ], - ), - ), - ), - ); - - if (source != null) { - await _pickImage(source); - } - } - @override void dispose() { _nameController.dispose(); - _descriptionController.dispose(); + _descController.dispose(); super.dispose(); } - void _onCategoryChanged(ItemCategory? category) { - setState(() { - _selectedCategory = category; - _selectedSubcategory = null; - _selectedTags.clear(); - - if (category != null && category.subcategories.isNotEmpty) { - _selectedSubcategory = category.subcategories.first; - _autoAssignTags(category.id, category.subcategories.first.id); + Future _pickImage() async { + try { + final image = await _imagePicker.pickImage( + source: ImageSource.gallery, + maxWidth: 1080, + maxHeight: 1080, + imageQuality: 80, + ); + if (image != null) { + setState(() => _pickedImage = image); } - }); + } catch (e) { + if (mounted) AppSnack.error(context, 'Erro ao selecionar foto: $e'); + } } - void _onSubcategoryChanged(Subcategory? subcategory) { - setState(() { - _selectedSubcategory = subcategory; - _selectedTags.clear(); - - if (_selectedCategory != null && subcategory != null) { - _autoAssignTags(_selectedCategory!.id, subcategory.id); + Future _takePhoto() async { + try { + final image = await _imagePicker.pickImage( + source: ImageSource.camera, + maxWidth: 1080, + maxHeight: 1080, + imageQuality: 80, + ); + if (image != null) { + setState(() => _pickedImage = image); } - }); + } catch (e) { + if (mounted) AppSnack.error(context, 'Erro ao abrir câmara: $e'); + } } - void _autoAssignTags(String categoryId, String subcategoryId) { - final autoTags = getAutoContextTags(categoryId, subcategoryId); - setState(() { - _selectedTags.addAll(autoTags); - }); - } - - void _toggleTag(String tagId) { - setState(() { - if (_selectedTags.contains(tagId)) { - _selectedTags.remove(tagId); - } else if (_selectedTags.length < 10) { - _selectedTags.add(tagId); + void _toggleSubcategoryTags( + Subcategory? oldSub, + Subcategory? newSub, + ) { + if (oldSub != null && _selectedCategory != null) { + for (final t in getAutoContextTags(_selectedCategory!.id, oldSub.id)) { + _selectedTags.remove(t); } - }); + } + if (newSub != null && _selectedCategory != null) { + _selectedTags.addAll( + getAutoContextTags(_selectedCategory!.id, newSub.id), + ); + } } Future _saveItem() async { if (_nameController.text.trim().isEmpty) { - _showErrorSnackBar('Por favor, insira o nome do item'); + AppSnack.error(context, 'Indique um nome'); return; } - if (_selectedCategory == null) { - _showErrorSnackBar('Por favor, selecione uma categoria'); + AppSnack.error(context, 'Selecione uma categoria'); return; } setState(() => _isLoading = true); - try { final user = Supabase.instance.client.auth.currentUser; if (user == null) { - _showErrorSnackBar('Usuário não autenticado'); + AppSnack.error(context, 'Utilizador não autenticado'); return; } - // Ensure user profile row exists in public.users (FK requirement) await Supabase.instance.client.from('users').upsert({ 'id': user.id, - 'nome': - user.userMetadata?['username'] ?? + 'nome': user.userMetadata?['username'] ?? user.email?.split('@').first ?? - 'Usuário', + 'Utilizador', }, onConflict: 'id'); final inserted = await Supabase.instance.client @@ -164,11 +116,10 @@ class _AddItemScreenState extends State { }) .select() .single(); + final itemId = inserted['id']; - // Upload image if selected - if (_selectedImage != null) { - final itemId = inserted['id']; - final file = File(_selectedImage!.path); + if (_pickedImage != null) { + final file = File(_pickedImage!.path); final fileName = 'item_${itemId}_${DateTime.now().millisecondsSinceEpoch}.jpg'; await Supabase.instance.client.storage @@ -183,333 +134,348 @@ class _AddItemScreenState extends State { }); } - _showSuccessSnackBar('Item adicionado com sucesso!'); - if (mounted) Navigator.pop(context); + if (mounted) { + AppSnack.success(context, 'Item adicionado!'); + Navigator.pop(context); + } } catch (e) { - _showErrorSnackBar('Erro ao salvar item: $e'); + if (mounted) AppSnack.error(context, 'Erro ao guardar: $e'); } finally { - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } - void _showErrorSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), - ); - } - - void _showSuccessSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.green), - ); - } - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), + backgroundColor: AppColors.background, appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), + backgroundColor: AppColors.background, elevation: 0, - title: const Text( - 'Adicionar Item', - style: TextStyle(color: Colors.white, fontSize: 20), - ), - centerTitle: true, + iconTheme: const IconThemeData(color: AppColors.textPrimary), + title: const Text('Adicionar Item', style: AppText.h3), ), body: SingleChildScrollView( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.fromLTRB(20, 8, 20, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Image picker - Center( - child: GestureDetector( - onTap: _showImageSourcePicker, - child: Container( - width: 140, - height: 140, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: _selectedImage != null - ? ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Image.file( - File(_selectedImage!.path), - fit: BoxFit.cover, - ), - ) - : const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.add_a_photo_outlined, - size: 40, - color: Color(0xFF0066CC), - ), - SizedBox(height: 8), - Text( - 'Adicionar foto', - style: TextStyle( - color: Color(0xFF666666), - fontSize: 12, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(height: 20), - - // Name field - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: TextField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Nome do item', - hintText: 'Ex: Camiseta Azul', - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - ), - ), - - const SizedBox(height: 16), - - // Description field - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: TextField( - controller: _descriptionController, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Descrição (opcional)', - hintText: 'Detalhes sobre o item', - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - ), - ), - + _buildImagePicker(), const SizedBox(height: 24), - - // Category selection - const Text( - 'Categoria', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), + const Text('Nome', style: AppText.label), const SizedBox(height: 8), - - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _selectedCategory, - hint: const Text('Selecione uma categoria'), - isExpanded: true, - items: itemCategories.map((category) { - return DropdownMenuItem( - value: category, - child: Row( - children: [ - Text( - category.icon, - style: const TextStyle(fontSize: 24), - ), - const SizedBox(width: 12), - Text(category.name), - ], - ), - ); - }).toList(), - onChanged: _onCategoryChanged, - ), - ), + _textField( + controller: _nameController, + hint: 'ex: Camisa azul', + icon: Icons.label_outline_rounded, ), - - const SizedBox(height: 16), - - // Subcategory selection + const SizedBox(height: 20), + const Text('Descrição (opcional)', style: AppText.label), + const SizedBox(height: 8), + _textField( + controller: _descController, + hint: 'detalhes do item', + icon: Icons.notes_rounded, + maxLines: 3, + ), + const SizedBox(height: 24), + const Text('Categoria', style: AppText.label), + const SizedBox(height: 12), + _buildCategoryGrid(), if (_selectedCategory != null && _selectedCategory!.subcategories.isNotEmpty) ...[ - const Text( - 'Subcategoria', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), + const SizedBox(height: 24), + const Text('Subcategoria', style: AppText.label), const SizedBox(height: 8), - - SizedBox( - height: 60, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _selectedCategory!.subcategories.length, - itemBuilder: (context, index) { - final subcategory = _selectedCategory!.subcategories[index]; - final isSelected = _selectedSubcategory == subcategory; - - return Padding( - padding: const EdgeInsets.only(right: 8), - child: GestureDetector( - onTap: () => _onSubcategoryChanged(subcategory), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF0066CC) - : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isSelected - ? const Color(0xFF0066CC) - : const Color(0xFFE0E0E0), - ), - ), - child: Center( - child: Text( - subcategory.name, - style: TextStyle( - color: isSelected - ? Colors.white - : const Color(0xFF333333), - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - ); - }, - ), + Wrap( + spacing: 8, + runSpacing: 8, + children: _selectedCategory!.subcategories.map((sub) { + final selected = _selectedSubcategory?.id == sub.id; + return AppChip( + label: sub.name, + selected: selected, + color: _selectedCategory!.color, + onTap: () { + setState(() { + final oldSub = _selectedSubcategory; + _selectedSubcategory = selected ? null : sub; + _toggleSubcategoryTags( + oldSub, + _selectedSubcategory, + ); + }); + }, + ); + }).toList(), ), - - const SizedBox(height: 8), - if (_selectedSubcategory != null) + if (_selectedSubcategory != null) ...[ + const SizedBox(height: 6), Text( - _selectedSubcategory!.examples, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF666666), + _selectedSubcategory!.description, + style: AppText.caption.copyWith( fontStyle: FontStyle.italic, ), ), + ], ], - const SizedBox(height: 24), - - // Context tags - const Text( - 'Tags de Contexto', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), + const Text('Tags de Contexto', style: AppText.label), const SizedBox(height: 8), - Wrap( spacing: 8, runSpacing: 8, children: contextTags.map((tag) { - final isSelected = _selectedTags.contains(tag.id); - return FilterChip( - label: Text(tag.name), - selected: isSelected, - onSelected: (selected) => _toggleTag(tag.id), - selectedColor: const Color(0xFF0066CC).withValues(alpha: 0.2), - checkmarkColor: const Color(0xFF0066CC), - backgroundColor: Colors.white, - labelStyle: TextStyle( - color: isSelected - ? const Color(0xFF0066CC) - : const Color(0xFF333333), - ), + final selected = _selectedTags.contains(tag.id); + return AppChip( + label: tag.name, + selected: selected, + onTap: () { + setState(() { + if (selected) { + _selectedTags.remove(tag.id); + } else { + _selectedTags.add(tag.id); + } + }); + }, ); }).toList(), ), - - const SizedBox(height: 8), - Text( - 'Tags selecionadas: ${_selectedTags.length}/10', - style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), - ), - const SizedBox(height: 32), - - // Save button - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: _isLoading ? null : _saveItem, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0066CC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - child: _isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), - ), - ) - : const Text( - 'Salvar Item', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ), + AppButton( + label: 'Guardar Item', + icon: Icons.check_rounded, + loading: _isLoading, + onPressed: _saveItem, ), ], ), ), ); } + + Widget _buildImagePicker() { + return Container( + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all(color: AppColors.border), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 11, + child: _pickedImage != null + ? Stack( + fit: StackFit.expand, + children: [ + Image.file(File(_pickedImage!.path), fit: BoxFit.cover), + Positioned( + top: 8, + right: 8, + child: Material( + color: Colors.black.withValues(alpha: 0.55), + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => setState(() => _pickedImage = null), + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon( + Icons.close_rounded, + color: Colors.white, + size: 18, + ), + ), + ), + ), + ), + ], + ) + : Container( + color: AppColors.surfaceAlt, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.image_outlined, + size: 48, + color: AppColors.textTertiary, + ), + const SizedBox(height: 8), + Text( + 'Adicione uma foto', + style: AppText.caption, + ), + ], + ), + ), + ), + Row( + children: [ + Expanded( + child: _imageBtn( + icon: Icons.photo_camera_rounded, + label: 'Câmara', + onTap: _takePhoto, + ), + ), + Container(width: 1, height: 44, color: AppColors.border), + Expanded( + child: _imageBtn( + icon: Icons.photo_library_rounded, + label: 'Galeria', + onTap: _pickImage, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _imageBtn({ + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 18, color: AppColors.primary), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _textField({ + required TextEditingController controller, + required String hint, + required IconData icon, + int maxLines = 1, + }) { + return Container( + decoration: AppDecorations.outlined(), + child: TextField( + controller: controller, + maxLines: maxLines, + style: AppText.body, + decoration: InputDecoration( + prefixIcon: maxLines == 1 + ? Icon(icon, color: AppColors.textSecondary) + : Padding( + padding: const EdgeInsets.only(left: 12, top: 14), + child: Icon(icon, color: AppColors.textSecondary), + ), + prefixIconConstraints: maxLines == 1 + ? null + : const BoxConstraints(minWidth: 40, minHeight: 40), + hintText: hint, + hintStyle: TextStyle(color: AppColors.textTertiary), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, + ), + ), + ), + ); + } + + Widget _buildCategoryGrid() { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: itemCategories.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 1, + ), + itemBuilder: (_, i) { + final c = itemCategories[i]; + final selected = _selectedCategory?.id == c.id; + return AnimatedContainer( + duration: const Duration(milliseconds: 180), + decoration: BoxDecoration( + color: selected ? c.color : AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: selected ? c.color : AppColors.border, + width: 1.5, + ), + boxShadow: selected + ? [ + BoxShadow( + color: c.color.withValues(alpha: 0.3), + blurRadius: 12, + offset: const Offset(0, 6), + ) + ] + : null, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.lg), + onTap: () { + setState(() { + if (_selectedCategory?.id != c.id) { + _selectedCategory = c; + _selectedSubcategory = null; + } + }); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + c.icon, + size: 28, + color: selected ? Colors.white : c.color, + ), + const SizedBox(height: 6), + Text( + c.name, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: selected + ? Colors.white + : AppColors.textPrimary, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } } diff --git a/lib/Screens/home_screen.dart b/lib/Screens/home_screen.dart index e34542e..bbd89fc 100644 --- a/lib/Screens/home_screen.dart +++ b/lib/Screens/home_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'perfil_screen.dart'; +import '../constants/item_categories.dart'; +import '../theme/app_theme.dart'; import 'add_item_screen.dart'; import 'item_screen.dart'; +import 'perfil_screen.dart'; import 'week_screen.dart'; -import 'ai_chat_screen.dart'; -import '../constants/item_categories.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -15,55 +15,105 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - int _selectedIndex = 0; + int _index = 0; - final List _screens = [ - const _HomeContent(), - const _ItemsScreen(), - const _WeekScreen(), - const _AiScreen(), - const _ProfileScreen(), + static const _tabs = [ + _HomeContent(), + ItemScreen(), + WeekScreen(), + PerfilScreen(), ]; @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), - body: _screens[_selectedIndex], - bottomNavigationBar: BottomNavigationBar( - currentIndex: _selectedIndex, - onTap: (index) => setState(() => _selectedIndex = index), - selectedItemColor: const Color(0xFF0066CC), - unselectedItemColor: const Color(0xFF666666), - backgroundColor: Colors.white, - type: BottomNavigationBarType.fixed, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Início', - ), - BottomNavigationBarItem( - icon: Icon(Icons.inventory_2_outlined), - label: 'Itens', - ), - BottomNavigationBarItem( - icon: Icon(Icons.calendar_today_outlined), - label: 'Semana', - ), - BottomNavigationBarItem( - icon: Icon(Icons.auto_awesome_outlined), - label: 'IA', - ), - BottomNavigationBarItem( - icon: Icon(Icons.person_outline), - label: 'Perfil', + backgroundColor: AppColors.background, + body: IndexedStack(index: _index, children: _tabs), + bottomNavigationBar: _buildBottomNav(), + ); + } + + Widget _buildBottomNav() { + return Container( + decoration: BoxDecoration( + color: AppColors.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 20, + offset: const Offset(0, -4), ), ], ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _navItem(0, Icons.home_rounded, 'Início'), + _navItem(1, Icons.inventory_2_rounded, 'Itens'), + _navItem(2, Icons.calendar_month_rounded, 'Semana'), + _navItem(3, Icons.person_rounded, 'Perfil'), + ], + ), + ), + ), + ); + } + + Widget _navItem(int idx, IconData icon, String label) { + final selected = _index == idx; + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.lg), + onTap: () => setState(() => _index = idx), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: selected + ? AppColors.primary.withValues(alpha: 0.08) + : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 24, + color: + selected ? AppColors.primary : AppColors.textSecondary, + ), + const SizedBox(height: 3), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: + selected ? FontWeight.w700 : FontWeight.w500, + color: selected + ? AppColors.primary + : AppColors.textSecondary, + ), + ), + ], + ), + ), + ), + ), ); } } +// ============================================================ +// Home tab content +// ============================================================ class _HomeContent extends StatefulWidget { const _HomeContent(); @@ -73,6 +123,7 @@ class _HomeContent extends StatefulWidget { class _HomeContentState extends State<_HomeContent> { int _itemCount = 0; + String _userName = ''; List> _todayItems = []; List> _recentItems = []; bool _isLoading = true; @@ -104,21 +155,27 @@ class _HomeContentState extends State<_HomeContent> { final user = Supabase.instance.client.auth.currentUser; if (user == null) return; - // total item count final all = await Supabase.instance.client .from('items') .select('id') .eq('user_id', user.id); - // recent 5 items final recent = await Supabase.instance.client .from('items') .select('*, item_images(image_url)') .eq('user_id', user.id) .order('id', ascending: false) - .limit(5); + .limit(8); + + Map? userRow; + try { + userRow = await Supabase.instance.client + .from('users') + .select('nome') + .eq('id', user.id) + .maybeSingle(); + } catch (_) {} - // today's plan final today = DateTime.now(); final plan = await Supabase.instance.client .from('plans') @@ -138,356 +195,450 @@ class _HomeContentState extends State<_HomeContent> { .toList(); } + if (!mounted) return; setState(() { _itemCount = all.length; + _userName = userRow?['nome'] ?? + user.email?.split('@').first ?? + 'Utilizador'; _recentItems = List>.from(recent); _todayItems = todayItems; _isLoading = false; }); } catch (e) { debugPrint('Error loading home: $e'); - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { - return SafeArea( - child: Stack( - children: [ - Column( + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + bottom: false, + child: RefreshIndicator( + onRefresh: _loadData, + color: AppColors.primary, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 20, 20, 100), children: [ - // App Bar - Container( - color: const Color(0xFF0066CC), - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'DayMaker', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - '$_itemCount itens', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ], - ), - IconButton( - icon: const Icon( - Icons.notifications_outlined, - color: Colors.white, - ), - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Notificações'), - backgroundColor: Color(0xFF0066CC), - ), - ); - }, - ), - ], - ), - ), - - Expanded( - child: RefreshIndicator( - onRefresh: _loadData, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTodaySection(), - const SizedBox(height: 24), - const Text( - 'Itens Recentes', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 12), - _buildRecentItems(), - ], - ), - ), - ), - ), + _buildGreeting(), + const SizedBox(height: 20), + _buildHeroCard(), + const SizedBox(height: 24), + _buildSectionHeader('Hoje'), + const SizedBox(height: 12), + _buildTodaySection(), + const SizedBox(height: 24), + _buildSectionHeader('Itens recentes'), + const SizedBox(height: 12), + _buildRecentItems(), + const SizedBox(height: 24), + _buildAddCta(), ], ), - - // Floating Action Button - Positioned( - bottom: 80, - right: 20, - child: FloatingActionButton( - onPressed: () { - Navigator.of(context) - .push( - MaterialPageRoute(builder: (_) => const AddItemScreen()), - ) - .then((_) => _loadData()); - }, - backgroundColor: const Color(0xFF0066CC), - child: const Icon(Icons.add, color: Colors.white), - ), - ), - ], + ), ), ); } - Widget _buildTodaySection() { + Widget _buildGreeting() { + final hour = DateTime.now().hour; + final saudacao = hour < 12 + ? 'Bom dia' + : hour < 19 + ? 'Boa tarde' + : 'Boa noite'; + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + saudacao, + style: AppText.bodySecondary, + ), + const SizedBox(height: 2), + Text( + _userName.isEmpty ? 'Olá!' : _userName, + style: AppText.h2, + ), + ], + ), + ), + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.surface, + shape: BoxShape.circle, + boxShadow: AppShadows.soft, + ), + child: const Icon( + Icons.notifications_none_rounded, + color: AppColors.textPrimary, + ), + ), + ], + ); + } + + Widget _buildHeroCard() { final today = DateTime.now(); final dayName = _weekdayLong[today.weekday - 1]; return Container( - padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), + gradient: AppColors.brandGradient, + borderRadius: BorderRadius.circular(AppRadius.xl), + boxShadow: AppShadows.brand, ), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Hoje - $dayName', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF0066CC), - ), + const Icon( + Icons.calendar_today_rounded, + color: Colors.white, + size: 18, ), + const SizedBox(width: 8), Text( - '${today.day}/${today.month}', - style: const TextStyle(fontSize: 14, color: Color(0xFF666666)), + '$dayName, ${today.day}/${today.month}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), ), ], ), - const SizedBox(height: 6), - Text( - '${_todayItems.length} ${_todayItems.length == 1 ? "item planejado" : "itens planejados"}', - style: const TextStyle(fontSize: 14, color: Color(0xFF666666)), - ), - const SizedBox(height: 12), - if (_isLoading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center(child: CircularProgressIndicator()), - ) - else if (_todayItems.isEmpty) - Container( - padding: const EdgeInsets.symmetric(vertical: 20), - alignment: Alignment.center, - child: const Text( - 'Nada planejado para hoje', - style: TextStyle(color: Color(0xFF999999)), + const SizedBox(height: 14), + Row( + children: [ + _heroStat( + value: '${_todayItems.length}', + label: 'Hoje', + icon: Icons.today_rounded, ), - ) - else - SizedBox( - height: 90, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _todayItems.length, - itemBuilder: (_, i) => _buildTodayChip(_todayItems[i]), + const SizedBox(width: 12), + _heroStat( + value: '$_itemCount', + label: 'No inventário', + icon: Icons.inventory_2_rounded, + ), + ], + ), + ], + ), + ); + } + + Widget _heroStat({ + required String value, + required String label, + required IconData icon, + }) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all(color: Colors.white.withValues(alpha: 0.25)), + ), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 22), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + height: 1, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.85), + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildSectionHeader(String title) { + return Text(title, style: AppText.h3); + } + + Widget _buildTodaySection() { + if (_isLoading) { + return _skeletonRow(); + } + if (_todayItems.isEmpty) { + return Container( + padding: const EdgeInsets.all(18), + decoration: AppDecorations.card(), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: const Icon( + Icons.event_available_rounded, + color: AppColors.primary, ), ), - ], - ), - ); - } - - Widget _buildTodayChip(Map item) { - final images = item['item_images'] as List?; - final imageUrl = (images != null && images.isNotEmpty) - ? images.first['image_url'] - : null; - final category = itemCategories.firstWhere( - (c) => c.id == item['categoria'], - orElse: () => itemCategories.last, - ); - return Container( - width: 80, - margin: const EdgeInsets.only(right: 10), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: imageUrl != null - ? Image.network( - imageUrl, - width: 60, - height: 60, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _placeholder(category.icon), - ) - : _placeholder(category.icon), - ), - const SizedBox(height: 4), - Text( - item['nome'] ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 11, color: Color(0xFF333333)), - ), - ], - ), - ); - } - - Widget _placeholder(String icon) { - return Container( - width: 60, - height: 60, - color: const Color(0xFF0066CC).withValues(alpha: 0.1), - alignment: Alignment.center, - child: Text(icon, style: const TextStyle(fontSize: 26)), - ); - } - - Widget _buildRecentItems() { - if (_isLoading) { - return const SizedBox( - height: 100, - child: Center(child: CircularProgressIndicator()), - ); - } - if (_recentItems.isEmpty) { - return Container( - height: 100, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Text( - 'Sem itens ainda', - style: TextStyle(color: Color(0xFF999999)), - ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Nada planeado para hoje', + style: AppText.body, + ), + SizedBox(height: 2), + Text( + 'Vá à aba Semana para organizar', + style: AppText.caption, + ), + ], + ), + ), + ], ), ); } return SizedBox( height: 130, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _todayItems.length, + itemBuilder: (_, i) => _itemCard(_todayItems[i], compact: true), + ), + ); + } + + Widget _buildRecentItems() { + if (_isLoading) return _skeletonRow(); + if (_recentItems.isEmpty) { + return Container( + padding: const EdgeInsets.all(18), + decoration: AppDecorations.card(), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.accent.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: const Icon( + Icons.add_box_rounded, + color: AppColors.accent, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Adicione o seu primeiro item', + style: AppText.body, + ), + ), + ], + ), + ); + } + return SizedBox( + height: 160, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _recentItems.length, - itemBuilder: (_, i) { - final item = _recentItems[i]; - final images = item['item_images'] as List?; - final imageUrl = (images != null && images.isNotEmpty) - ? images.first['image_url'] - : null; - final category = itemCategories.firstWhere( - (c) => c.id == item['categoria'], - orElse: () => itemCategories.last, - ); - return Container( - width: 110, - margin: const EdgeInsets.only(right: 10), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: imageUrl != null - ? Image.network( - imageUrl, - width: double.infinity, - height: 70, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _placeholder(category.icon), - ) - : Container( - width: double.infinity, - height: 70, - color: const Color(0xFF0066CC).withValues(alpha: 0.1), - alignment: Alignment.center, - child: Text( - category.icon, - style: const TextStyle(fontSize: 30), + itemBuilder: (_, i) => _itemCard(_recentItems[i]), + ), + ); + } + + Widget _itemCard(Map item, {bool compact = false}) { + final images = item['item_images'] as List?; + final imageUrl = (images != null && images.isNotEmpty) + ? images.first['image_url'] as String? + : null; + final cat = categoryById(item['categoria'] as String?); + final size = compact ? 110.0 : 130.0; + return Container( + width: size, + margin: const EdgeInsets.only(right: 12), + decoration: AppDecorations.card(), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + width: double.infinity, + height: compact ? 72 : 90, + color: cat.color.withValues(alpha: 0.15), + child: imageUrl != null + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Center( + child: Icon( + cat.icon, + color: cat.color, + size: 32, + ), + ), + ) + : Center( + child: Icon( + cat.icon, + color: cat.color, + size: 32, + ), ), - ), - ), - const SizedBox(height: 6), - Text( - item['nome'] ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, ), + ], + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['nome'] ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + cat.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: cat.color, + fontWeight: FontWeight.w600, + ), + ), + ], ), - Text( - category.name, - style: const TextStyle( - fontSize: 10, - color: Color(0xFF666666), - ), - ), - ], - ), - ); + ), + ], + ), + ), + ), + ); + } + + Widget _skeletonRow() { + return SizedBox( + height: 130, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 3, + itemBuilder: (_, __) => Container( + width: 110, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + ), + ), + ); + } + + Widget _buildAddCta() { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.lg), + onTap: () { + Navigator.of(context) + .push( + MaterialPageRoute(builder: (_) => const AddItemScreen()), + ) + .then((_) => _loadData()); }, + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.3), + style: BorderStyle.solid, + width: 1.5, + ), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: AppColors.brandGradient, + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: + const Icon(Icons.add_rounded, color: Colors.white, size: 24), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Adicionar item', style: AppText.h3), + SizedBox(height: 2), + Text( + 'Foto, categoria e tags em segundos', + style: AppText.caption, + ), + ], + ), + ), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: AppColors.textTertiary, + ), + ], + ), + ), ), ); } } - -class _ItemsScreen extends StatelessWidget { - const _ItemsScreen(); - - @override - Widget build(BuildContext context) => const ItemScreen(); -} - -class _WeekScreen extends StatelessWidget { - const _WeekScreen(); - - @override - Widget build(BuildContext context) => const WeekScreen(); -} - -class _AiScreen extends StatelessWidget { - const _AiScreen(); - - @override - Widget build(BuildContext context) => const AiChatScreen(); -} - -class _ProfileScreen extends StatelessWidget { - const _ProfileScreen(); - - @override - Widget build(BuildContext context) => const PerfilScreen(); -} diff --git a/lib/Screens/item_screen.dart b/lib/Screens/item_screen.dart index 3fccd76..bb19ea6 100644 --- a/lib/Screens/item_screen.dart +++ b/lib/Screens/item_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../constants/item_categories.dart'; +import '../theme/app_theme.dart'; import 'add_item_screen.dart'; class ItemScreen extends StatefulWidget { @@ -15,19 +16,17 @@ class _ItemScreenState extends State { bool _isLoading = true; String _searchQuery = ''; String? _selectedCategoryFilter; + bool _gridView = true; List> get _filteredItems { return _items.where((item) { final name = (item['nome'] ?? '').toString().toLowerCase(); - final tags = List.from( - item['tags'] ?? [], - ).join(' ').toLowerCase(); - final matchesSearch = - _searchQuery.isEmpty || + final tags = + List.from(item['tags'] ?? []).join(' ').toLowerCase(); + final matchesSearch = _searchQuery.isEmpty || name.contains(_searchQuery.toLowerCase()) || tags.contains(_searchQuery.toLowerCase()); - final matchesCategory = - _selectedCategoryFilter == null || + final matchesCategory = _selectedCategoryFilter == null || item['categoria'] == _selectedCategoryFilter; return matchesSearch && matchesCategory; }).toList(); @@ -44,20 +43,19 @@ class _ItemScreenState extends State { try { final user = Supabase.instance.client.auth.currentUser; if (user == null) return; - final response = await Supabase.instance.client .from('items') .select('*, item_images(image_url)') .eq('user_id', user.id) .order('id', ascending: false); - + if (!mounted) return; setState(() { _items = List>.from(response); _isLoading = false; }); } catch (e) { debugPrint('Error loading items: $e'); - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } @@ -69,26 +67,15 @@ class _ItemScreenState extends State { return null; } - String _categoryName(String? id) { - if (id == null) return 'Outros'; - return itemCategories - .firstWhere((c) => c.id == id, orElse: () => itemCategories.last) - .name; - } - - String _categoryIcon(String? id) { - if (id == null) return '📦'; - return itemCategories - .firstWhere((c) => c.id == id, orElse: () => itemCategories.last) - .icon; - } - Future _deleteItem(Map item) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Apagar item'), - content: Text('Tem certeza que deseja apagar "${item['nome']}"?'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + title: const Text('Apagar item?'), + content: Text('"${item['nome']}" será removido permanentemente.'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), @@ -96,14 +83,13 @@ class _ItemScreenState extends State { ), TextButton( onPressed: () => Navigator.pop(ctx, true), - style: TextButton.styleFrom(foregroundColor: Colors.red), + style: TextButton.styleFrom(foregroundColor: AppColors.error), child: const Text('Apagar'), ), ], ), ); if (confirmed != true) return; - try { await Supabase.instance.client .from('item_images') @@ -114,28 +100,18 @@ class _ItemScreenState extends State { .delete() .eq('id', item['id']); _loadItems(); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Item apagado'), - backgroundColor: Colors.green, - ), - ); - } + if (mounted) AppSnack.success(context, 'Item apagado'); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red), - ); - } + if (mounted) AppSnack.error(context, 'Erro: $e'); } } - void _viewItem(Map item) { + void _openItem(Map item) { Navigator.push( context, MaterialPageRoute( - builder: (_) => ItemDetailScreen(item: item, imageUrl: _imageUrl(item)), + builder: (_) => + ItemDetailScreen(item: item, imageUrl: _imageUrl(item)), ), ).then((_) => _loadItems()); } @@ -150,277 +126,309 @@ class _ItemScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), - appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), - elevation: 0, - title: const Text( - 'Meus Itens', - style: TextStyle(color: Colors.white, fontSize: 20), - ), - centerTitle: true, - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - _buildSearchAndFilters(), - Expanded( - child: _items.isEmpty + backgroundColor: AppColors.background, + body: SafeArea( + bottom: false, + child: Column( + children: [ + _buildHeader(), + _buildSearchBar(), + const SizedBox(height: 12), + _buildCategoryChips(), + const SizedBox(height: 16), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _items.isEmpty ? _buildEmpty() : _filteredItems.isEmpty - ? const Center( - child: Text( - 'Nenhum item encontrado', - style: TextStyle(color: Color(0xFF666666)), - ), - ) - : RefreshIndicator( - onRefresh: _loadItems, - child: ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _filteredItems.length, - itemBuilder: (context, i) => - _buildItemCard(_filteredItems[i]), - ), - ), - ), - ], + ? _buildNoResults() + : RefreshIndicator( + onRefresh: _loadItems, + color: AppColors.primary, + child: _gridView + ? _buildGrid() + : _buildList(), + ), ), - floatingActionButton: FloatingActionButton( - backgroundColor: const Color(0xFF0066CC), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const AddItemScreen()), - ).then((_) => _loadItems()); - }, - child: const Icon(Icons.add, color: Colors.white), + ], + ), ), + floatingActionButton: _buildFab(), ); } - Widget _buildSearchAndFilters() { - return Container( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - color: const Color(0xFFFFE5CC), - child: Column( + Widget _buildHeader() { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), + child: Row( children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - ), - child: TextField( - onChanged: (v) => setState(() => _searchQuery = v), - decoration: const InputDecoration( - prefixIcon: Icon(Icons.search, color: Color(0xFF666666)), - hintText: 'Pesquisar por nome ou tag...', - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 12, - vertical: 14, - ), - ), - ), - ), - const SizedBox(height: 8), - SizedBox( - height: 38, - child: ListView( - scrollDirection: Axis.horizontal, + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _categoryChip(null, 'Todos', '🗂'), - ...itemCategories.map( - (c) => _categoryChip(c.id, c.name, c.icon), - ), + Text('Inventário', style: AppText.h2), + SizedBox(height: 2), + Text('Os seus itens guardados', style: AppText.caption), ], ), ), + _toggleViewButton(), ], ), ); } - Widget _categoryChip(String? id, String name, String icon) { - final selected = _selectedCategoryFilter == id; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: GestureDetector( - onTap: () => setState(() => _selectedCategoryFilter = id), + Widget _toggleViewButton() { + return Container( + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(AppRadius.pill), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + _viewIcon(Icons.grid_view_rounded, _gridView), + _viewIcon(Icons.view_list_rounded, !_gridView), + ], + ), + ); + } + + Widget _viewIcon(IconData icon, bool selected) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.pill), + onTap: () => setState(() => _gridView = !_gridView), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: selected ? const Color(0xFF0066CC) : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: selected - ? const Color(0xFF0066CC) - : const Color(0xFFE0E0E0), - ), + color: selected ? AppColors.surface : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadius.pill), + boxShadow: selected ? AppShadows.soft : null, ), - child: Row( - children: [ - Text(icon, style: const TextStyle(fontSize: 14)), - const SizedBox(width: 6), - Text( - name, - style: TextStyle( - color: selected ? Colors.white : const Color(0xFF333333), - fontWeight: FontWeight.w500, - ), - ), - ], + child: Icon( + icon, + size: 18, + color: + selected ? AppColors.primary : AppColors.textSecondary, ), ), ), ); } + Widget _buildSearchBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + decoration: AppDecorations.outlined(radius: AppRadius.pill), + child: TextField( + onChanged: (v) => setState(() => _searchQuery = v), + style: AppText.body, + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.search_rounded, + color: AppColors.textSecondary, + ), + hintText: 'Pesquisar por nome ou tag...', + hintStyle: TextStyle(color: AppColors.textTertiary), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 14), + ), + ), + ), + ); + } + + Widget _buildCategoryChips() { + return SizedBox( + height: 38, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: AppChip( + label: 'Todos', + icon: Icons.apps_rounded, + selected: _selectedCategoryFilter == null, + onTap: () => setState(() => _selectedCategoryFilter = null), + ), + ), + ...itemCategories.map( + (c) => Padding( + padding: const EdgeInsets.only(right: 8), + child: AppChip( + label: c.name, + icon: c.icon, + color: c.color, + selected: _selectedCategoryFilter == c.id, + onTap: () => + setState(() => _selectedCategoryFilter = c.id), + ), + ), + ), + ], + ), + ); + } + Widget _buildEmpty() { + return _emptyState( + icon: Icons.inventory_2_outlined, + title: 'Nenhum item ainda', + subtitle: 'Toque no + para adicionar o primeiro', + ); + } + + Widget _buildNoResults() { + return _emptyState( + icon: Icons.search_off_rounded, + title: 'Sem resultados', + subtitle: 'Tente outra pesquisa ou categoria', + ); + } + + Widget _emptyState({ + required IconData icon, + required String title, + required String subtitle, + }) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey[400]), + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 36, color: AppColors.textTertiary), + ), const SizedBox(height: 16), - const Text( - 'Nenhum item ainda', - style: TextStyle(fontSize: 18, color: Color(0xFF666666)), - ), - const SizedBox(height: 8), - const Text( - 'Toque no + para adicionar', - style: TextStyle(fontSize: 14, color: Color(0xFF999999)), - ), + Text(title, style: AppText.h3), + const SizedBox(height: 4), + Text(subtitle, style: AppText.caption), ], ), ); } - Widget _buildItemCard(Map item) { - final categoryName = _categoryName(item['categoria']); - final categoryIcon = _categoryIcon(item['categoria']); + Widget _buildGrid() { + return GridView.builder( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 100), + physics: const AlwaysScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 0.78, + ), + itemCount: _filteredItems.length, + itemBuilder: (_, i) => _buildGridCard(_filteredItems[i]), + ); + } + + Widget _buildList() { + return ListView.builder( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 100), + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _filteredItems.length, + itemBuilder: (_, i) => _buildListCard(_filteredItems[i]), + ); + } + + Widget _buildGridCard(Map item) { + final cat = categoryById(item['categoria'] as String?); final tags = List.from(item['tags'] ?? []); final imageUrl = _imageUrl(item); - - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => _viewItem(item), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( + return Container( + decoration: AppDecorations.card(), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _openItem(item), + onLongPress: () => _showItemActions(item), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: imageUrl != null - ? Image.network( + AspectRatio( + aspectRatio: 1, + child: Stack( + fit: StackFit.expand, + children: [ + Container(color: cat.color.withValues(alpha: 0.15)), + if (imageUrl != null) + Image.network( imageUrl, - width: 72, - height: 72, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _iconPlaceholder(categoryIcon), + errorBuilder: (_, __, ___) => + Center(child: Icon(cat.icon, color: cat.color, size: 40)), ) - : _iconPlaceholder(categoryIcon), + else + Center( + child: Icon(cat.icon, color: cat.color, size: 40), + ), + Positioned( + top: 8, + right: 8, + child: _moreButton(item), + ), + ], + ), ), - const SizedBox(width: 12), - Expanded( + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item['nome'] ?? 'Sem nome', + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), + fontWeight: FontWeight.w600, + fontSize: 14, + color: AppColors.textPrimary, ), ), const SizedBox(height: 2), - Text( - categoryName, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - ), + Row( + children: [ + Icon(cat.icon, size: 12, color: cat.color), + const SizedBox(width: 4), + Expanded( + child: Text( + cat.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: cat.color, + fontWeight: FontWeight.w600, + ), + ), + ), + ], ), if (tags.isNotEmpty) ...[ const SizedBox(height: 6), - Wrap( - spacing: 4, - runSpacing: 4, - children: tags.take(3).map((tag) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: const Color( - 0xFF0066CC, - ).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - tag, - style: const TextStyle( - fontSize: 10, - color: Color(0xFF0066CC), - ), - ), - ); - }).toList(), + Text( + tags.take(2).join(' • '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppText.caption, ), ], ], ), ), - PopupMenuButton( - icon: const Icon(Icons.more_vert, color: Color(0xFF666666)), - onSelected: (value) { - if (value == 'view') _viewItem(item); - if (value == 'edit') _editItem(item); - if (value == 'delete') _deleteItem(item); - }, - itemBuilder: (_) => [ - const PopupMenuItem( - value: 'view', - child: Row( - children: [ - Icon(Icons.visibility_outlined, size: 18), - SizedBox(width: 8), - Text('Ver'), - ], - ), - ), - const PopupMenuItem( - value: 'edit', - child: Row( - children: [ - Icon(Icons.edit_outlined, size: 18), - SizedBox(width: 8), - Text('Editar'), - ], - ), - ), - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete_outline, size: 18, color: Colors.red), - SizedBox(width: 8), - Text('Apagar', style: TextStyle(color: Colors.red)), - ], - ), - ), - ], - ), ], ), ), @@ -428,122 +436,385 @@ class _ItemScreenState extends State { ); } - Widget _iconPlaceholder(String icon) { + Widget _buildListCard(Map item) { + final cat = categoryById(item['categoria'] as String?); + final tags = List.from(item['tags'] ?? []); + final imageUrl = _imageUrl(item); return Container( - width: 72, - height: 72, - color: const Color(0xFF0066CC).withValues(alpha: 0.1), - alignment: Alignment.center, - child: Text(icon, style: const TextStyle(fontSize: 32)), + margin: const EdgeInsets.only(bottom: 10), + decoration: AppDecorations.card(), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _openItem(item), + onLongPress: () => _showItemActions(item), + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(AppRadius.md), + child: Container( + width: 64, + height: 64, + color: cat.color.withValues(alpha: 0.15), + child: imageUrl != null + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Icon( + cat.icon, + color: cat.color, + size: 28, + ), + ) + : Icon(cat.icon, color: cat.color, size: 28), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['nome'] ?? 'Sem nome', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + color: AppColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(cat.icon, size: 13, color: cat.color), + const SizedBox(width: 4), + Text( + cat.name, + style: TextStyle( + fontSize: 12, + color: cat.color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + if (tags.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + tags.take(3).join(' • '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppText.caption, + ), + ], + ], + ), + ), + _moreButton(item), + ], + ), + ), + ), + ), + ); + } + + Widget _moreButton(Map item) { + return Material( + color: Colors.white.withValues(alpha: 0.85), + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => _showItemActions(item), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.more_horiz_rounded, + size: 18, + color: AppColors.textPrimary, + ), + ), + ), + ); + } + + void _showItemActions(Map item) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + margin: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.border, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 8), + _actionTile( + Icons.visibility_outlined, + 'Ver detalhes', + AppColors.primary, + () { + Navigator.pop(ctx); + _openItem(item); + }, + ), + _actionTile( + Icons.edit_outlined, + 'Editar', + AppColors.primary, + () { + Navigator.pop(ctx); + _editItem(item); + }, + ), + _actionTile( + Icons.delete_outline_rounded, + 'Apagar', + AppColors.error, + () { + Navigator.pop(ctx); + _deleteItem(item); + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ); + } + + Widget _actionTile( + IconData icon, + String label, + Color color, + VoidCallback onTap, + ) { + return ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon(icon, color: color), + ), + title: Text( + label, + style: TextStyle( + color: color, + fontWeight: FontWeight.w600, + ), + ), + onTap: onTap, + ); + } + + Widget _buildFab() { + return Container( + decoration: BoxDecoration( + gradient: AppColors.brandGradient, + borderRadius: BorderRadius.circular(AppRadius.lg), + boxShadow: AppShadows.brand, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.lg), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const AddItemScreen()), + ).then((_) => _loadItems()); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 18, vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add_rounded, color: Colors.white), + SizedBox(width: 6), + Text( + 'Adicionar', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), ); } } -// ============================================= +// ============================================================ // Detalhe do item -// ============================================= +// ============================================================ class ItemDetailScreen extends StatelessWidget { final Map item; final String? imageUrl; - const ItemDetailScreen({super.key, required this.item, this.imageUrl}); @override Widget build(BuildContext context) { - final categoryId = item['categoria'] as String?; - final categoryName = categoryId == null - ? 'Outros' - : itemCategories - .firstWhere( - (c) => c.id == categoryId, - orElse: () => itemCategories.last, - ) - .name; + final cat = categoryById(item['categoria'] as String?); final tags = List.from(item['tags'] ?? []); return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), - appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), - title: Text( - item['nome'] ?? 'Item', - style: const TextStyle(color: Colors.white), - ), - iconTheme: const IconThemeData(color: Colors.white), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (imageUrl != null) - ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Image.network( - imageUrl!, - width: double.infinity, - height: 250, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - const SizedBox.shrink(), - ), - ), - const SizedBox(height: 20), - _label('Nome'), - _value(item['nome'] ?? ''), - const SizedBox(height: 16), - _label('Categoria'), - _value(categoryName), - if (tags.isNotEmpty) ...[ - const SizedBox(height: 16), - _label('Tags'), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: tags - .map( - (t) => Chip( - label: Text(t), - backgroundColor: const Color( - 0xFF0066CC, - ).withValues(alpha: 0.1), - labelStyle: const TextStyle(color: Color(0xFF0066CC)), - ), - ) - .toList(), + backgroundColor: AppColors.background, + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 320, + pinned: true, + backgroundColor: cat.color, + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + IconButton( + icon: const Icon(Icons.edit_rounded, color: Colors.white), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => EditItemScreen(item: item), + ), + ).then((_) { + if (context.mounted) Navigator.pop(context); + }); + }, ), ], - ], - ), + flexibleSpace: FlexibleSpaceBar( + background: Stack( + fit: StackFit.expand, + children: [ + Container(color: cat.color.withValues(alpha: 0.25)), + if (imageUrl != null) + Image.network( + imageUrl!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Center(child: Icon(cat.icon, color: cat.color, size: 80)), + ) + else + Center( + child: Icon(cat.icon, color: cat.color, size: 80), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.4), + Colors.transparent, + Colors.black.withValues(alpha: 0.2), + ], + ), + ), + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item['nome'] ?? '', style: AppText.h1), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: cat.color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(AppRadius.pill), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(cat.icon, size: 16, color: cat.color), + const SizedBox(width: 6), + Text( + cat.name, + style: TextStyle( + color: cat.color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + if (tags.isNotEmpty) ...[ + const Text('Tags', style: AppText.label), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: tags + .map( + (t) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: + BorderRadius.circular(AppRadius.pill), + border: Border.all(color: AppColors.border), + ), + child: Text( + t, + style: const TextStyle( + color: AppColors.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ) + .toList(), + ), + ], + ], + ), + ), + ), + ], ), ); } - - Widget _label(String t) => Text( - t, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color(0xFF666666), - ), - ); - - Widget _value(String t) => Container( - width: double.infinity, - margin: const EdgeInsets.only(top: 6), - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Text(t, style: const TextStyle(fontSize: 16)), - ); } -// ============================================= +// ============================================================ // Editar item -// ============================================= +// ============================================================ class EditItemScreen extends StatefulWidget { final Map item; const EditItemScreen({super.key, required this.item}); @@ -564,13 +835,9 @@ class _EditItemScreenState extends State { _nameController = TextEditingController(text: widget.item['nome'] ?? ''); final catId = widget.item['categoria'] as String?; if (catId != null) { - _selectedCategory = itemCategories.firstWhere( - (c) => c.id == catId, - orElse: () => itemCategories.last, - ); + _selectedCategory = categoryById(catId); } - final tags = List.from(widget.item['tags'] ?? []); - _selectedTags.addAll(tags); + _selectedTags.addAll(List.from(widget.item['tags'] ?? [])); } @override @@ -581,108 +848,89 @@ class _EditItemScreenState extends State { Future _save() async { if (_nameController.text.trim().isEmpty) { - _snack('Nome não pode ser vazio', Colors.red); + AppSnack.error(context, 'Nome não pode ser vazio'); return; } setState(() => _isLoading = true); try { - await Supabase.instance.client - .from('items') - .update({ - 'nome': _nameController.text.trim(), - 'categoria': _selectedCategory?.id, - 'tags': _selectedTags.toList(), - }) - .eq('id', widget.item['id']); - _snack('Item atualizado!', Colors.green); - if (mounted) Navigator.pop(context); + await Supabase.instance.client.from('items').update({ + 'nome': _nameController.text.trim(), + 'categoria': _selectedCategory?.id, + 'tags': _selectedTags.toList(), + }).eq('id', widget.item['id']); + if (mounted) { + AppSnack.success(context, 'Item atualizado!'); + Navigator.pop(context); + } } catch (e) { - _snack('Erro: $e', Colors.red); + if (mounted) AppSnack.error(context, 'Erro: $e'); } finally { if (mounted) setState(() => _isLoading = false); } } - void _snack(String msg, Color color) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(msg), backgroundColor: color)); - } - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), + backgroundColor: AppColors.background, appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), - title: const Text('Editar Item', style: TextStyle(color: Colors.white)), - iconTheme: const IconThemeData(color: Colors.white), + backgroundColor: AppColors.background, + elevation: 0, + title: const Text('Editar Item', style: AppText.h3), + iconTheme: const IconThemeData(color: AppColors.textPrimary), ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Text('Nome', style: AppText.label), + const SizedBox(height: 8), Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), + decoration: AppDecorations.outlined(), child: TextField( controller: _nameController, + style: AppText.body, decoration: const InputDecoration( - labelText: 'Nome do item', border: InputBorder.none, contentPadding: EdgeInsets.symmetric( horizontal: 16, - vertical: 16, + vertical: 14, ), + hintText: 'Nome do item', ), ), ), - const SizedBox(height: 16), - const Text( - 'Categoria', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), + const SizedBox(height: 20), + const Text('Categoria', style: AppText.label), const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _selectedCategory, - isExpanded: true, - items: itemCategories - .map( - (c) => DropdownMenuItem( - value: c, - child: Text('${c.icon} ${c.name}'), - ), - ) - .toList(), - onChanged: (v) => setState(() => _selectedCategory = v), - ), - ), - ), - const SizedBox(height: 16), - const Text( - 'Tags', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: itemCategories + .map( + (c) => AppChip( + label: c.name, + icon: c.icon, + color: c.color, + selected: _selectedCategory?.id == c.id, + onTap: () => setState(() => _selectedCategory = c), + ), + ) + .toList(), ), + const SizedBox(height: 20), + const Text('Tags de contexto', style: AppText.label), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: contextTags.map((tag) { final selected = _selectedTags.contains(tag.id); - return FilterChip( - label: Text(tag.name), + return AppChip( + label: tag.name, selected: selected, - onSelected: (_) { + onTap: () { setState(() { if (selected) { _selectedTags.remove(tag.id); @@ -691,31 +939,15 @@ class _EditItemScreenState extends State { } }); }, - selectedColor: const Color(0xFF0066CC).withValues(alpha: 0.2), - checkmarkColor: const Color(0xFF0066CC), - backgroundColor: Colors.white, ); }).toList(), ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - height: 52, - child: ElevatedButton( - onPressed: _isLoading ? null : _save, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0066CC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) - : const Text( - 'Guardar alterações', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ), + const SizedBox(height: 32), + AppButton( + label: 'Guardar alterações', + icon: Icons.check_rounded, + loading: _isLoading, + onPressed: _save, ), ], ), diff --git a/lib/Screens/perfil_screen.dart b/lib/Screens/perfil_screen.dart index 727a0ea..0624abe 100644 --- a/lib/Screens/perfil_screen.dart +++ b/lib/Screens/perfil_screen.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:image_picker/image_picker.dart'; import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import '../login/login_screen.dart'; +import '../theme/app_theme.dart'; class PerfilScreen extends StatefulWidget { const PerfilScreen({super.key}); @@ -17,6 +18,8 @@ class _PerfilScreenState extends State { String _nome = ''; String _email = ''; bool _isLoading = true; + int _itemCount = 0; + int _plansCount = 0; @override void initState() { @@ -27,299 +30,357 @@ class _PerfilScreenState extends State { Future _loadUserData() async { try { final user = Supabase.instance.client.auth.currentUser; - if (user != null) { - setState(() { - _email = user.email ?? ''; - }); + if (user == null) return; + setState(() => _email = user.email ?? ''); - // Fetch user data from users table - final response = await Supabase.instance.client - .from('users') - .select() - .eq('id', user.id) - .single(); + final response = await Supabase.instance.client + .from('users') + .select() + .eq('id', user.id) + .maybeSingle(); - setState(() { - _nome = response['nome'] ?? ''; - _avatarUrl = response['avatar_url']; - }); - } + final items = await Supabase.instance.client + .from('items') + .select('id') + .eq('user_id', user.id); + + final plans = await Supabase.instance.client + .from('plans') + .select('id') + .eq('user_id', user.id); + + if (!mounted) return; + setState(() { + _nome = response?['nome'] ?? ''; + _avatarUrl = response?['avatar_url']; + _itemCount = (items as List).length; + _plansCount = (plans as List).length; + }); } catch (e) { debugPrint('Error loading user data: $e'); } finally { - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } Future _pickImage() async { try { - final XFile? image = await _imagePicker.pickImage( + final image = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 512, maxHeight: 512, imageQuality: 75, ); - - if (image != null) { - await _uploadImage(image); - } + if (image != null) await _uploadImage(image); } catch (e) { - _showErrorSnackBar('Erro ao selecionar imagem: $e'); + if (mounted) AppSnack.error(context, 'Erro: $e'); } } Future _uploadImage(XFile image) async { final user = Supabase.instance.client.auth.currentUser; if (user == null) { - _showErrorSnackBar('Usuário não autenticado'); + AppSnack.error(context, 'Utilizador não autenticado'); return; } - setState(() => _isLoading = true); - try { final file = File(image.path); final fileName = '${user.id}_${DateTime.now().millisecondsSinceEpoch}.jpg'; - - // Upload to Supabase Storage await Supabase.instance.client.storage .from('avatars') .upload(fileName, file); - - // Get public URL final imageUrl = Supabase.instance.client.storage .from('avatars') .getPublicUrl(fileName); - - // Update user's avatar_url in database await Supabase.instance.client .from('users') .update({'avatar_url': imageUrl}) .eq('id', user.id); - - setState(() { - _avatarUrl = imageUrl; - }); - - _showSuccessSnackBar('Foto atualizada com sucesso!'); + if (!mounted) return; + setState(() => _avatarUrl = imageUrl); + AppSnack.success(context, 'Foto atualizada!'); } catch (e) { - _showErrorSnackBar('Erro ao fazer upload: $e'); + if (mounted) AppSnack.error(context, 'Erro: $e'); } finally { - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } + Future _confirmLogout() async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + title: const Text('Sair da conta?'), + content: const Text('Vai ter de fazer login novamente.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancelar'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + style: TextButton.styleFrom(foregroundColor: AppColors.error), + child: const Text('Sair'), + ), + ], + ), + ); + if (ok == true) _logout(); + } + Future _logout() async { try { await Supabase.instance.client.auth.signOut(); if (mounted) { Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const LoginScreen()), - (route) => false, + (_) => false, ); } } catch (e) { - _showErrorSnackBar('Erro ao sair: $e'); + if (mounted) AppSnack.error(context, 'Erro ao sair: $e'); } } - void _showErrorSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), - ); - } - - void _showSuccessSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.green), - ); - } - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), - appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), - elevation: 0, - title: const Text( - 'Perfil', - style: TextStyle(color: Colors.white, fontSize: 20), - ), - centerTitle: true, + backgroundColor: AppColors.background, + body: SafeArea( + bottom: false, + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 100), + child: Column( + children: [ + _buildHeader(), + const SizedBox(height: 24), + _buildAvatar(), + const SizedBox(height: 16), + Text( + _nome.isNotEmpty ? _nome : 'Utilizador', + style: AppText.h2, + ), + const SizedBox(height: 4), + Text(_email, style: AppText.bodySecondary), + const SizedBox(height: 24), + _buildStatsRow(), + const SizedBox(height: 24), + _buildInfoCard(), + const SizedBox(height: 24), + AppButton( + label: 'Sair da Conta', + icon: Icons.logout_rounded, + danger: true, + onPressed: _confirmLogout, + ), + ], + ), + ), ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - const SizedBox(height: 20), + ); + } - // Avatar section - GestureDetector( - onTap: _pickImage, - child: Stack( - children: [ - Container( - width: 120, - height: 120, - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - border: Border.all( - color: const Color(0xFF0066CC), - width: 3, - ), - ), - child: _avatarUrl != null - ? ClipOval( - child: Image.network( - _avatarUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.person, - size: 60, - color: Color(0xFFCCCCCC), - ); - }, - ), - ) - : const Icon( - Icons.person, - size: 60, - color: Color(0xFFCCCCCC), - ), - ), - Positioned( - bottom: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration( - color: Color(0xFF0066CC), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.camera_alt, - color: Colors.white, - size: 20, - ), - ), - ), - ], - ), - ), + Widget _buildHeader() { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Align( + alignment: Alignment.centerLeft, + child: Text('Perfil', style: AppText.h2), + ), + ); + } - const SizedBox(height: 20), - - // Name section - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Nome', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Text( - _nome.isNotEmpty ? _nome : 'Não definido', - style: const TextStyle( - fontSize: 18, - color: Color(0xFF333333), - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Email section - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Email', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Text( - _email, - style: const TextStyle( - fontSize: 18, - color: Color(0xFF333333), - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - - const SizedBox(height: 32), - - // Logout button - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: _logout, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.logout, color: Colors.white), - SizedBox(width: 8), - Text( - 'Sair da Conta', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], + Widget _buildAvatar() { + return Stack( + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + gradient: AppColors.brandGradient, + shape: BoxShape.circle, + boxShadow: AppShadows.brand, + ), + padding: const EdgeInsets.all(4), + child: ClipOval( + child: _avatarUrl != null + ? Image.network( + _avatarUrl!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: AppColors.surface, + child: const Icon( + Icons.person_rounded, + size: 60, + color: AppColors.textTertiary, ), ), + ) + : Container( + color: AppColors.surface, + child: const Icon( + Icons.person_rounded, + size: 60, + color: AppColors.textTertiary, + ), ), - - const SizedBox(height: 20), - ], + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Material( + color: AppColors.surface, + shape: const CircleBorder(), + elevation: 2, + child: InkWell( + customBorder: const CircleBorder(), + onTap: _pickImage, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppColors.brandGradient, + shape: BoxShape.circle, + border: Border.all(color: AppColors.surface, width: 3), + ), + child: const Icon( + Icons.camera_alt_rounded, + color: Colors.white, + size: 16, + ), ), ), + ), + ), + ], + ); + } + + Widget _buildStatsRow() { + return Row( + children: [ + _statCard( + icon: Icons.inventory_2_rounded, + color: AppColors.primary, + value: '$_itemCount', + label: 'Itens', + ), + const SizedBox(width: 12), + _statCard( + icon: Icons.calendar_month_rounded, + color: AppColors.accent, + value: '$_plansCount', + label: 'Dias planeados', + ), + ], + ); + } + + Widget _statCard({ + required IconData icon, + required Color color, + required String value, + required String label, + }) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: AppDecorations.card(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(height: 10), + Text( + value, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + Text(label, style: AppText.caption), + ], + ), + ), + ); + } + + Widget _buildInfoCard() { + return Container( + decoration: AppDecorations.card(), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + _infoRow( + icon: Icons.person_outline_rounded, + label: 'Nome', + value: _nome.isNotEmpty ? _nome : 'Não definido', + ), + const Divider(height: 1, thickness: 1, color: AppColors.divider), + _infoRow( + icon: Icons.alternate_email_rounded, + label: 'Email', + value: _email, + ), + ], + ), + ); + } + + Widget _infoRow({ + required IconData icon, + required String label, + required String value, + }) { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon(icon, color: AppColors.primary, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppText.caption), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + fontSize: 15, + ), + ), + ], + ), + ), + ], + ), ); } } diff --git a/lib/Screens/week_screen.dart b/lib/Screens/week_screen.dart index 633aaf1..60e46d8 100644 --- a/lib/Screens/week_screen.dart +++ b/lib/Screens/week_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../constants/item_categories.dart'; +import '../theme/app_theme.dart'; class WeekScreen extends StatefulWidget { const WeekScreen({super.key}); @@ -14,17 +15,8 @@ class _WeekScreenState extends State { List> _dayItems = []; bool _isLoading = false; - static const _weekdayNames = [ - 'Seg', - 'Ter', - 'Qua', - 'Qui', - 'Sex', - 'Sáb', - 'Dom', - ]; - - static const _weekdayNamesLong = [ + static const _weekdayShort = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom']; + static const _weekdayLong = [ 'Segunda', 'Terça', 'Quarta', @@ -42,11 +34,8 @@ class _WeekScreenState extends State { DateTime get _startOfWeek { final now = DateTime.now(); - return DateTime( - now.year, - now.month, - now.day, - ).subtract(Duration(days: now.weekday - 1)); + return DateTime(now.year, now.month, now.day) + .subtract(Duration(days: now.weekday - 1)); } String _dateKey(DateTime d) => @@ -57,16 +46,13 @@ class _WeekScreenState extends State { Future _getOrCreatePlanId(DateTime day) async { final user = Supabase.instance.client.auth.currentUser!; final dateStr = _dateKey(day); - final existing = await Supabase.instance.client .from('plans') .select('id') .eq('user_id', user.id) .eq('data', dateStr) .maybeSingle(); - if (existing != null) return existing['id'] as int; - final created = await Supabase.instance.client .from('plans') .insert({'user_id': user.id, 'data': dateStr}) @@ -80,7 +66,6 @@ class _WeekScreenState extends State { try { final user = Supabase.instance.client.auth.currentUser; if (user == null) return; - final dateStr = _dateKey(_selectedDay); final plan = await Supabase.instance.client .from('plans') @@ -88,15 +73,14 @@ class _WeekScreenState extends State { .eq('user_id', user.id) .eq('data', dateStr) .maybeSingle(); - if (plan == null) { + if (!mounted) return; setState(() { _dayItems = []; _isLoading = false; }); return; } - final planItems = plan['plan_items'] as List? ?? []; final items = planItems .where((pi) => pi['items'] != null) @@ -104,55 +88,46 @@ class _WeekScreenState extends State { (pi) => Map.from(pi['items']), ) .toList(); - + if (!mounted) return; setState(() { _dayItems = items; _isLoading = false; }); } catch (e) { - debugPrint('Error loading day items: $e'); - setState(() => _isLoading = false); + debugPrint('Error loading day: $e'); + if (mounted) setState(() => _isLoading = false); } } Future _addItemsToDay() async { final user = Supabase.instance.client.auth.currentUser; if (user == null) return; - final allItems = await Supabase.instance.client .from('items') .select('*, item_images(image_url)') .eq('user_id', user.id); - final available = List>.from(allItems); final existingIds = _dayItems.map((i) => i['id']).toSet(); - final toShow = available - .where((i) => !existingIds.contains(i['id'])) - .toList(); + final toShow = + available.where((i) => !existingIds.contains(i['id'])).toList(); if (!mounted) return; final selected = await showModalBottomSheet>( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (ctx) => _ItemPickerSheet(items: toShow), + builder: (_) => _ItemPickerSheet(items: toShow), ); - if (selected == null || selected.isEmpty) return; try { final planId = await _getOrCreatePlanId(_selectedDay); - final rows = selected - .map((id) => {'plan_id': planId, 'item_id': id}) - .toList(); + final rows = + selected.map((id) => {'plan_id': planId, 'item_id': id}).toList(); await Supabase.instance.client.from('plan_items').insert(rows); _loadDayItems(); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red), - ); - } + if (mounted) AppSnack.error(context, 'Erro: $e'); } } @@ -166,166 +141,192 @@ class _WeekScreenState extends State { .eq('item_id', item['id']); _loadDayItems(); } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red), - ); - } + if (mounted) AppSnack.error(context, 'Erro: $e'); } } @override Widget build(BuildContext context) { - final start = _startOfWeek; - final days = List.generate(7, (i) => start.add(Duration(days: i))); - + final days = List.generate(7, (i) => _startOfWeek.add(Duration(days: i))); return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), - appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), - elevation: 0, - title: const Text( - 'Minha Semana', - style: TextStyle(color: Colors.white, fontSize: 20), + backgroundColor: AppColors.background, + body: SafeArea( + bottom: false, + child: Column( + children: [ + _buildHeader(), + _buildDaysRow(days), + const SizedBox(height: 12), + _buildDayTitle(), + const SizedBox(height: 8), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _dayItems.isEmpty + ? _buildEmpty() + : ListView.builder( + padding: const EdgeInsets.fromLTRB( + 20, 0, 20, 120, + ), + itemCount: _dayItems.length, + itemBuilder: (_, i) => + _buildItemTile(_dayItems[i]), + ), + ), + ], ), - centerTitle: true, ), - body: Column( + floatingActionButton: _buildFab(), + ); + } + + Widget _buildHeader() { + return const Padding( + padding: EdgeInsets.fromLTRB(20, 12, 20, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Week selector - Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - child: Row( - children: days.map((day) { - final isSelected = - day.year == _selectedDay.year && - day.month == _selectedDay.month && - day.day == _selectedDay.day; - final isToday = - day.year == DateTime.now().year && - day.month == DateTime.now().month && - day.day == DateTime.now().day; - return Expanded( - child: GestureDetector( - onTap: () { - setState(() => _selectedDay = day); - _loadDayItems(); - }, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 3), - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF0066CC) - : Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isToday - ? const Color(0xFF0066CC) - : Colors.transparent, - width: 2, - ), - ), - child: Column( - children: [ - Text( - _weekdayNames[day.weekday - 1], - style: TextStyle( - fontSize: 12, - color: isSelected - ? Colors.white - : const Color(0xFF666666), - ), - ), - const SizedBox(height: 4), - Text( - '${day.day}', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isSelected - ? Colors.white - : const Color(0xFF333333), - ), - ), - ], + Text('Minha Semana', style: AppText.h2), + SizedBox(height: 2), + Text( + 'Planeie o que precisa para cada dia', + style: AppText.caption, + ), + ], + ), + ); + } + + Widget _buildDaysRow(List days) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: days.map((day) { + final isSelected = day.year == _selectedDay.year && + day.month == _selectedDay.month && + day.day == _selectedDay.day; + final today = DateTime.now(); + final isToday = day.year == today.year && + day.month == today.month && + day.day == today.day; + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.md), + onTap: () { + setState(() => _selectedDay = day); + _loadDayItems(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + gradient: isSelected ? AppColors.brandGradient : null, + color: isSelected ? null : AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all( + color: isToday && !isSelected + ? AppColors.primary + : AppColors.border, + width: isToday ? 1.5 : 1, ), + boxShadow: isSelected ? AppShadows.brand : null, ), - ), - ); - }).toList(), - ), - ), - // Day title - Padding( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), - child: Row( - children: [ - Text( - '${_weekdayNamesLong[_selectedDay.weekday - 1]}, ' - '${_selectedDay.day}/${_selectedDay.month}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), - const Spacer(), - Text( - '${_dayItems.length} ${_dayItems.length == 1 ? "item" : "itens"}', - style: const TextStyle( - color: Color(0xFF666666), - fontSize: 13, - ), - ), - ], - ), - ), - // Items list - Expanded( - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _dayItems.isEmpty - ? Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.calendar_today_outlined, - size: 56, - color: Colors.grey[400], - ), - const SizedBox(height: 12), - const Text( - 'Nenhum item para este dia', + Text( + _weekdayShort[day.weekday - 1], style: TextStyle( - color: Color(0xFF666666), - fontSize: 16, + fontSize: 11, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : AppColors.textSecondary, ), ), const SizedBox(height: 4), - const Text( - 'Toque em + para adicionar', + Text( + '${day.day}', style: TextStyle( - color: Color(0xFF999999), - fontSize: 13, + fontSize: 18, + fontWeight: FontWeight.bold, + color: isSelected + ? Colors.white + : AppColors.textPrimary, ), ), ], ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _dayItems.length, - itemBuilder: (_, i) => _buildItemTile(_dayItems[i]), ), + ), + ), + ), + ); + }).toList(), + ), + ); + } + + Widget _buildDayTitle() { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + children: [ + Text( + '${_weekdayLong[_selectedDay.weekday - 1]}, ' + '${_selectedDay.day}/${_selectedDay.month}', + style: AppText.h3, + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.pill), + ), + child: Text( + '${_dayItems.length} ${_dayItems.length == 1 ? "item" : "itens"}', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), ), ], ), - floatingActionButton: FloatingActionButton( - backgroundColor: const Color(0xFF0066CC), - onPressed: _addItemsToDay, - child: const Icon(Icons.add, color: Colors.white), + ); + } + + Widget _buildEmpty() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.event_available_rounded, + color: AppColors.primary, + size: 36, + ), + ), + const SizedBox(height: 16), + const Text('Nada planeado', style: AppText.h3), + const SizedBox(height: 4), + Text( + 'Toque em + para adicionar itens', + style: AppText.caption, + ), + ], ), ); } @@ -333,58 +334,132 @@ class _WeekScreenState extends State { Widget _buildItemTile(Map item) { final images = item['item_images'] as List?; final imageUrl = (images != null && images.isNotEmpty) - ? images.first['image_url'] + ? images.first['image_url'] as String? : null; - final category = itemCategories.firstWhere( - (c) => c.id == item['categoria'], - orElse: () => itemCategories.last, - ); - - return Card( - margin: const EdgeInsets.only(bottom: 10), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - leading: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: imageUrl != null - ? Image.network( - imageUrl, + final cat = categoryById(item['categoria'] as String?); + return Dismissible( + key: ValueKey(item['id']), + direction: DismissDirection.endToStart, + background: Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 24), + alignment: Alignment.centerRight, + decoration: BoxDecoration( + color: AppColors.error, + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + child: const Icon(Icons.delete_outline, color: Colors.white), + ), + onDismissed: (_) => _removeItem(item), + child: Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: AppDecorations.card(), + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(AppRadius.md), + child: Container( width: 56, height: 56, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _icon(category.icon), - ) - : _icon(category.icon), - ), - title: Text( - item['nome'] ?? 'Sem nome', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text(category.name), - trailing: IconButton( - icon: const Icon(Icons.close, color: Colors.red), - onPressed: () => _removeItem(item), + color: cat.color.withValues(alpha: 0.15), + child: imageUrl != null + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Icon(cat.icon, color: cat.color, size: 24), + ) + : Icon(cat.icon, color: cat.color, size: 24), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['nome'] ?? '', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + color: AppColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(cat.icon, size: 12, color: cat.color), + const SizedBox(width: 4), + Text( + cat.name, + style: TextStyle( + fontSize: 12, + color: cat.color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + IconButton( + icon: const Icon( + Icons.close_rounded, + color: AppColors.textTertiary, + ), + onPressed: () => _removeItem(item), + ), + ], + ), ), ), ); } - Widget _icon(String icon) { + Widget _buildFab() { return Container( - width: 56, - height: 56, - color: const Color(0xFF0066CC).withValues(alpha: 0.1), - alignment: Alignment.center, - child: Text(icon, style: const TextStyle(fontSize: 26)), + decoration: BoxDecoration( + gradient: AppColors.brandGradient, + borderRadius: BorderRadius.circular(AppRadius.lg), + boxShadow: AppShadows.brand, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.lg), + onTap: _addItemsToDay, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 18, vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add_rounded, color: Colors.white), + SizedBox(width: 6), + Text( + 'Adicionar', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), ); } } -// ============================================= -// Bottom sheet para escolher itens -// ============================================= +// ============================================================ +// Item picker bottom sheet +// ============================================================ class _ItemPickerSheet extends StatefulWidget { final List> items; const _ItemPickerSheet({required this.items}); @@ -401,9 +476,10 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> { Widget build(BuildContext context) { final filtered = widget.items .where( - (i) => (i['nome'] ?? '').toString().toLowerCase().contains( - _query.toLowerCase(), - ), + (i) => (i['nome'] ?? '') + .toString() + .toLowerCase() + .contains(_query.toLowerCase()), ) .toList(); @@ -414,132 +490,74 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> { expand: false, builder: (_, scrollController) => Container( decoration: const BoxDecoration( - color: Color(0xFFFFE5CC), - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + color: AppColors.background, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ Container( - margin: const EdgeInsets.symmetric(vertical: 8), - width: 40, + margin: const EdgeInsets.symmetric(vertical: 10), + width: 42, height: 4, decoration: BoxDecoration( - color: Colors.grey[400], + color: AppColors.border, borderRadius: BorderRadius.circular(2), ), ), const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Adicionar itens ao dia', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 4), + child: Align( + alignment: Alignment.centerLeft, + child: Text('Adicionar itens ao dia', style: AppText.h3), ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 8), child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - ), + decoration: AppDecorations.outlined(radius: AppRadius.pill), child: TextField( onChanged: (v) => setState(() => _query = v), decoration: const InputDecoration( - prefixIcon: Icon(Icons.search), + prefixIcon: Icon( + Icons.search_rounded, + color: AppColors.textSecondary, + ), hintText: 'Pesquisar...', border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 12), + contentPadding: EdgeInsets.symmetric(vertical: 14), ), ), ), ), Expanded( child: filtered.isEmpty - ? const Center(child: Text('Nenhum item disponível')) + ? Center( + child: Text( + 'Sem itens disponíveis', + style: AppText.caption, + ), + ) : ListView.builder( controller: scrollController, + padding: const EdgeInsets.fromLTRB(20, 4, 20, 12), itemCount: filtered.length, - itemBuilder: (_, i) { - final item = filtered[i]; - final id = item['id'] as int; - final selected = _selected.contains(id); - final images = item['item_images'] as List?; - final imageUrl = (images != null && images.isNotEmpty) - ? images.first['image_url'] - : null; - final category = itemCategories.firstWhere( - (c) => c.id == item['categoria'], - orElse: () => itemCategories.last, - ); - return CheckboxListTile( - value: selected, - activeColor: const Color(0xFF0066CC), - onChanged: (v) { - setState(() { - if (v == true) { - _selected.add(id); - } else { - _selected.remove(id); - } - }); - }, - title: Text(item['nome'] ?? ''), - subtitle: Text(category.name), - secondary: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: imageUrl != null - ? Image.network( - imageUrl, - width: 48, - height: 48, - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => - Container( - width: 48, - height: 48, - color: const Color( - 0xFF0066CC, - ).withValues(alpha: 0.1), - alignment: Alignment.center, - child: Text(category.icon), - ), - ) - : Container( - width: 48, - height: 48, - color: const Color( - 0xFF0066CC, - ).withValues(alpha: 0.1), - alignment: Alignment.center, - child: Text(category.icon), - ), - ), - ); - }, + itemBuilder: (_, i) => + _buildPickerTile(filtered[i]), ), ), SafeArea( + top: false, child: Padding( - padding: const EdgeInsets.all(16), - child: SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _selected.isEmpty - ? null - : () => Navigator.pop(context, _selected.toList()), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0066CC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text( - 'Adicionar (${_selected.length})', - style: const TextStyle(color: Colors.white, fontSize: 16), - ), - ), + padding: const EdgeInsets.all(20), + child: AppButton( + label: _selected.isEmpty + ? 'Selecione itens' + : 'Adicionar (${_selected.length})', + icon: Icons.check_rounded, + onPressed: _selected.isEmpty + ? null + : () => + Navigator.pop(context, _selected.toList()), ), ), ), @@ -548,4 +566,113 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> { ), ); } + + Widget _buildPickerTile(Map item) { + final id = item['id'] as int; + final selected = _selected.contains(id); + final images = item['item_images'] as List?; + final imageUrl = (images != null && images.isNotEmpty) + ? images.first['image_url'] as String? + : null; + final cat = categoryById(item['categoria'] as String?); + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all( + color: selected ? AppColors.primary : AppColors.border, + width: selected ? 1.5 : 1, + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.md), + onTap: () { + setState(() { + if (selected) { + _selected.remove(id); + } else { + _selected.add(id); + } + }); + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(AppRadius.sm), + child: Container( + width: 48, + height: 48, + color: cat.color.withValues(alpha: 0.15), + child: imageUrl != null + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Icon(cat.icon, color: cat.color, size: 22), + ) + : Icon(cat.icon, color: cat.color, size: 22), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['nome'] ?? '', + style: const TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + cat.name, + style: TextStyle( + fontSize: 12, + color: cat.color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 180), + width: 24, + height: 24, + decoration: BoxDecoration( + color: selected + ? AppColors.primary + : Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: selected + ? AppColors.primary + : AppColors.border, + width: 2, + ), + ), + child: selected + ? const Icon( + Icons.check_rounded, + color: Colors.white, + size: 16, + ) + : null, + ), + ], + ), + ), + ), + ), + ); + } } diff --git a/lib/constants/item_categories.dart b/lib/constants/item_categories.dart index fe93459..c6f42d5 100644 --- a/lib/constants/item_categories.dart +++ b/lib/constants/item_categories.dart @@ -1,14 +1,18 @@ +import 'package:flutter/material.dart'; + class ItemCategory { final String id; final String name; - final String icon; + final IconData icon; + final Color color; final String description; final List subcategories; - ItemCategory({ + const ItemCategory({ required this.id, required this.name, required this.icon, + required this.color, required this.description, required this.subcategories, }); @@ -17,9 +21,13 @@ class ItemCategory { class Subcategory { final String id; final String name; - final String examples; + final String description; - Subcategory({required this.id, required this.name, required this.examples}); + const Subcategory({ + required this.id, + required this.name, + required this.description, + }); } class ContextTag { @@ -28,7 +36,7 @@ class ContextTag { final String description; final String examples; - ContextTag({ + const ContextTag({ required this.id, required this.name, required this.description, @@ -36,174 +44,188 @@ class ContextTag { }); } -// Categorias principais +// Lista de categorias principais final List itemCategories = [ ItemCategory( id: 'clothing', name: 'Roupa', - icon: '👕', + icon: Icons.checkroom_rounded, + color: const Color(0xFFEC4899), description: 'Peças de vestuário', subcategories: [ - Subcategory( - id: 'casual', - name: 'Casual', - examples: 't-shirts, calças de ganga, hoodies', - ), Subcategory( id: 'formal', name: 'Formal', - examples: 'fatos, camisas, vestidos de cerimónia', + description: 'fato, camisa, blazer, vestido', + ), + Subcategory( + id: 'casual', + name: 'Casual', + description: 't-shirt, jeans, hoodie', ), Subcategory( id: 'sportswear', name: 'Sportswear', - examples: 'leggings, tops de treino, shorts', + description: 'leggings, calções, top desportivo', ), Subcategory( id: 'outerwear', name: 'Outerwear', - examples: 'casacos, impermeáveis, parkas', + description: 'casaco, blusão, sobretudo', ), Subcategory( id: 'underwear', name: 'Underwear', - examples: 'roupa interior, meias', + description: 'cuecas, meias, sutiãs', ), Subcategory( id: 'sleepwear', name: 'Sleepwear', - examples: 'pijamas, roupões', + description: 'pijamas, roupões', ), ], ), ItemCategory( id: 'electronics', name: 'Eletrónica', - icon: '💻', + icon: Icons.devices_other_rounded, + color: const Color(0xFF8B5CF6), description: 'Dispositivos e acessórios tecnológicos', subcategories: [ Subcategory( id: 'computers', - name: 'Computers', - examples: 'portáteis, tablets', + name: 'Computadores', + description: 'portátil, tablet', ), Subcategory( id: 'phones', - name: 'Phones', - examples: 'smartphones, earphones', + name: 'Telemóveis', + description: 'smartphone, smartwatch', ), Subcategory( - id: 'cameras', - name: 'Cameras', - examples: 'máquinas fotográficas, action cams', + id: 'audio', + name: 'Áudio', + description: 'auscultadores, colunas', ), Subcategory( id: 'cables', - name: 'Cables', - examples: 'carregadores, cabos USB, adaptadores', + name: 'Cabos e Carregadores', + description: 'USB, power bank', + ), + Subcategory( + id: 'cameras', + name: 'Câmaras', + description: 'fotográfica, GoPro, drone', ), Subcategory( id: 'gaming', name: 'Gaming', - examples: 'consolas, comandos, jogos', - ), - Subcategory( - id: 'audio', - name: 'Audio', - examples: 'headphones, colunas bluetooth', + description: 'consola, comandos', ), ], ), ItemCategory( id: 'footwear', name: 'Calçado', - icon: '👟', + icon: Icons.hiking_rounded, + color: const Color(0xFFF59E0B), description: 'Sapatos, botas, sandálias', subcategories: [ - Subcategory( - id: 'casual', - name: 'Casual', - examples: 'sapatilhas, loafers', - ), Subcategory( id: 'formal', name: 'Formal', - examples: 'sapatos de salto, mocassins', + description: 'sapatos de vestir', + ), + Subcategory( + id: 'casual', + name: 'Casual', + description: 'sapatilhas do dia-a-dia', ), Subcategory( id: 'sport', - name: 'Sport', - examples: 'ténis de corrida, chuteiras', + name: 'Desporto', + description: 'ténis de corrida, futebol', ), Subcategory( id: 'outdoor', name: 'Outdoor', - examples: 'botas de caminhada, sandálias', + description: 'botas de montanha', + ), + Subcategory( + id: 'sandals', + name: 'Sandálias', + description: 'chinelos, sandálias', ), ], ), ItemCategory( id: 'accessories', name: 'Acessórios', - icon: '🎒', + icon: Icons.work_outline_rounded, + color: const Color(0xFF10B981), description: 'Bolsas, relógios, óculos, bijuteria', subcategories: [ Subcategory( id: 'bags', - name: 'Bags', - examples: 'mochilas, malas, bolsas', + name: 'Malas e Bolsas', + description: 'mochila, mala, carteira', ), Subcategory( id: 'watches', - name: 'Watches', - examples: 'relógios analógicos e digitais', - ), - Subcategory( - id: 'eyewear', - name: 'Eyewear', - examples: 'óculos de sol, óculos de grau', + name: 'Relógios', + description: 'relógios analógicos e digitais', ), Subcategory( id: 'jewelry', - name: 'Jewelry', - examples: 'colares, pulseiras, brincos', + name: 'Joias e Bijuteria', + description: 'colares, anéis, brincos', + ), + Subcategory( + id: 'eyewear', + name: 'Óculos', + description: 'óculos de sol, graduados', + ), + Subcategory( + id: 'hats', + name: 'Chapéus', + description: 'bonés, chapéus, gorros', ), - Subcategory(id: 'hats', name: 'Hats', examples: 'bonés, chapéus, gorros'), - Subcategory(id: 'belts', name: 'Belts', examples: 'cintos'), ], ), ItemCategory( id: 'documents', name: 'Documentos', - icon: '📄', + icon: Icons.description_rounded, + color: const Color(0xFFEF4444), description: 'Passaporte, cartões, papéis importantes', subcategories: [ Subcategory( id: 'identity', - name: 'Identity', - examples: 'passaporte, BI, carta de condução', - ), - Subcategory( - id: 'health', - name: 'Health', - examples: 'cartão de saúde, receitas', + name: 'Identidade', + description: 'cartão de cidadão, carta de condução', ), Subcategory( id: 'travel', - name: 'Travel', - examples: 'bilhetes, reservas, seguros', + name: 'Viagem', + description: 'passaporte, visto', ), Subcategory( - id: 'financial', - name: 'Financial', - examples: 'cartões de crédito/débito', + id: 'cards', + name: 'Cartões', + description: 'crédito, débito, fidelização', + ), + Subcategory( + id: 'health', + name: 'Saúde', + description: 'cartão de saúde, receitas', ), ], ), ItemCategory( id: 'other', name: 'Outros', - icon: '📦', + icon: Icons.inventory_2_rounded, + color: const Color(0xFF64748B), description: 'Tudo o resto', subcategories: [], ), @@ -291,3 +313,12 @@ List getAutoContextTags(String categoryId, String subcategoryId) { return autoTags[key] ?? []; } + +/// Helper to fetch a category by id, falling back to "Outros". +ItemCategory categoryById(String? id) { + if (id == null) return itemCategories.last; + return itemCategories.firstWhere( + (c) => c.id == id, + orElse: () => itemCategories.last, + ); +} diff --git a/lib/login/login_screen.dart b/lib/login/login_screen.dart index 9128a69..287d372 100644 --- a/lib/login/login_screen.dart +++ b/lib/login/login_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../Screens/home_screen.dart'; +import '../theme/app_theme.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -9,364 +10,15 @@ class LoginScreen extends StatefulWidget { State createState() => _LoginScreenState(); } -class _LoginScreenState extends State { - bool isLoginSelected = true; +class _LoginScreenState extends State + with SingleTickerProviderStateMixin { + bool _isLogin = true; bool _isLoading = false; - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _emailController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); + bool _obscurePassword = true; - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), // Light orange background - appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), // Blue app bar - elevation: 0, - toolbarHeight: 0, // Remove default app bar height - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 40), - - // Logo - Image.asset( - 'assets/logoDayMaker.png', - width: 220, - height: 220, - fit: BoxFit.contain, - ), - - const SizedBox(height: 24), - - // App title and subtitle - const Text( - 'DayMaker', - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Color(0xFF0066CC), - ), - ), - - const SizedBox(height: 8), - - const Text( - 'Organize sua rotina com inteligência', - style: TextStyle(fontSize: 16, color: Color(0xFF666666)), - textAlign: TextAlign.center, - ), - - const SizedBox(height: 40), - - // Toggle buttons for Login/Create Account - Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => setState(() => isLoginSelected = true), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isLoginSelected - ? const Color(0xFF0066CC) - : Colors.transparent, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - bottomLeft: Radius.circular(20), - ), - border: Border.all( - color: const Color(0xFF0066CC), - width: 2, - ), - ), - child: Text( - 'Entrar', - textAlign: TextAlign.center, - style: TextStyle( - color: isLoginSelected - ? Colors.white - : const Color(0xFF0066CC), - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => setState(() => isLoginSelected = false), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: !isLoginSelected - ? const Color(0xFF0066CC) - : Colors.transparent, - borderRadius: const BorderRadius.only( - topRight: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - border: Border.all( - color: const Color(0xFF0066CC), - width: 2, - ), - ), - child: Text( - 'Criar Conta', - textAlign: TextAlign.center, - style: TextStyle( - color: !isLoginSelected - ? Colors.white - : const Color(0xFF0066CC), - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - ), - ), - ), - ], - ), - - const SizedBox(height: 32), - - // Email field - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: TextField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.email, color: Color(0xFF666666)), - hintText: 'Digite seu email', - hintStyle: TextStyle(color: Color(0xFF999999)), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - ), - ), - - const SizedBox(height: 16), - - // Username field (only shown when Criar Conta is selected) - if (!isLoginSelected) - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: TextField( - controller: _usernameController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.person, color: Color(0xFF666666)), - hintText: 'Digite seu nome', - hintStyle: TextStyle(color: Color(0xFF999999)), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - ), - ), - - if (!isLoginSelected) const SizedBox(height: 16), - - // Password field - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: TextField( - controller: _passwordController, - obscureText: true, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.lock, color: Color(0xFF666666)), - hintText: 'Digite sua senha', - hintStyle: TextStyle(color: Color(0xFF999999)), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - ), - ), - - const SizedBox(height: 32), - - // Login/Create Account button - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: _isLoading - ? null - : () async { - if (isLoginSelected) { - await _handleLogin(); - } else { - await _handleSignUp(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0066CC), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(42), - ), - elevation: 0, - ), - child: _isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - isLoginSelected ? 'Entrar' : 'Criar Conta', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 8), - const Icon( - Icons.arrow_forward, - color: Colors.white, - size: 20, - ), - ], - ), - ), - ), - - const Spacer(), - - // Version text - const Text( - 'Versão 1.0.0', - style: TextStyle(color: Color(0xFF666666), fontSize: 14), - ), - - const SizedBox(height: 16), - ], - ), - ), - ), - ); - } - - Future _handleLogin() async { - final email = _emailController.text.trim(); - final password = _passwordController.text.trim(); - - if (email.isEmpty || password.isEmpty) { - _showErrorSnackBar('Por favor, preencha todos os campos'); - return; - } - - setState(() => _isLoading = true); - - try { - final response = await Supabase.instance.client.auth.signInWithPassword( - email: email, - password: password, - ); - - if (response.user != null) { - if (mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const HomeScreen()), - ); - } - } - } on AuthException catch (e) { - _showErrorSnackBar('Erro de login: ${e.message}'); - } catch (e) { - _showErrorSnackBar('Erro ao fazer login: $e'); - } finally { - if (mounted) { - setState(() => _isLoading = false); - } - } - } - - Future _handleSignUp() async { - final email = _emailController.text.trim(); - final username = _usernameController.text.trim(); - final password = _passwordController.text.trim(); - - if (email.isEmpty || username.isEmpty || password.isEmpty) { - _showErrorSnackBar('Por favor, preencha todos os campos'); - return; - } - - setState(() => _isLoading = true); - - try { - final response = await Supabase.instance.client.auth.signUp( - email: email, - password: password, - data: {'username': username}, - ); - - if (response.user != null) { - // Save user data to users table - await Supabase.instance.client.from('users').insert({ - 'id': response.user!.id, - 'nome': username, - 'email': email, - }); - - _showSuccessSnackBar('Conta criada com sucesso!'); - setState(() => isLoginSelected = true); - } - } on AuthException catch (e) { - _showErrorSnackBar('Erro ao criar conta: ${e.message}'); - } catch (e) { - _showErrorSnackBar('Erro ao criar conta: $e'); - } finally { - if (mounted) { - setState(() => _isLoading = false); - } - } - } - - void _showErrorSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), - ); - } - - void _showSuccessSnackBar(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.green), - ); - } + final _usernameController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); @override void dispose() { @@ -375,4 +27,277 @@ class _LoginScreenState extends State { _passwordController.dispose(); super.dispose(); } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - + MediaQuery.of(context).padding.top - + MediaQuery.of(context).padding.bottom, + ), + child: IntrinsicHeight( + child: Column( + children: [ + const SizedBox(height: 40), + _buildBrandHeader(), + const SizedBox(height: 32), + _buildModeToggle(), + const SizedBox(height: 28), + _buildForm(), + const SizedBox(height: 24), + _buildSubmitButton(), + const Spacer(), + Padding( + padding: const EdgeInsets.only(top: 24, bottom: 8), + child: Text( + 'Versão 1.0.0', + style: AppText.caption, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildBrandHeader() { + return Column( + children: [ + Container( + width: 84, + height: 84, + decoration: BoxDecoration( + gradient: AppColors.brandGradient, + borderRadius: BorderRadius.circular(24), + boxShadow: AppShadows.brand, + ), + child: const Icon( + Icons.auto_awesome_rounded, + color: Colors.white, + size: 42, + ), + ), + const SizedBox(height: 18), + const Text('DayMaker', style: AppText.h1), + const SizedBox(height: 6), + Text( + 'Organize a sua rotina com inteligência', + style: AppText.bodySecondary, + textAlign: TextAlign.center, + ), + ], + ); + } + + Widget _buildModeToggle() { + return Container( + decoration: BoxDecoration( + color: AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(AppRadius.pill), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + _toggleButton('Entrar', _isLogin, () { + setState(() => _isLogin = true); + }), + _toggleButton('Criar Conta', !_isLogin, () { + setState(() => _isLogin = false); + }), + ], + ), + ); + } + + Widget _toggleButton(String label, bool selected, VoidCallback onTap) { + return Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + decoration: BoxDecoration( + color: selected ? AppColors.surface : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadius.pill), + boxShadow: selected ? AppShadows.soft : null, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.pill), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.w600, + color: selected + ? AppColors.primary + : AppColors.textSecondary, + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildForm() { + return Column( + children: [ + _inputField( + controller: _emailController, + icon: Icons.email_outlined, + hint: 'Insira o seu email', + keyboard: TextInputType.emailAddress, + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + child: !_isLogin + ? Padding( + padding: const EdgeInsets.only(top: 12), + child: _inputField( + controller: _usernameController, + icon: Icons.person_outline, + hint: 'Insira o seu nome', + ), + ) + : const SizedBox.shrink(), + ), + const SizedBox(height: 12), + _inputField( + controller: _passwordController, + icon: Icons.lock_outline_rounded, + hint: 'Insira a sua palavra-passe', + obscure: _obscurePassword, + trailing: IconButton( + onPressed: () => + setState(() => _obscurePassword = !_obscurePassword), + icon: Icon( + _obscurePassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: AppColors.textSecondary, + ), + ), + ), + ], + ); + } + + Widget _inputField({ + required TextEditingController controller, + required IconData icon, + required String hint, + TextInputType? keyboard, + bool obscure = false, + Widget? trailing, + }) { + return Container( + decoration: AppDecorations.outlined(), + child: TextField( + controller: controller, + keyboardType: keyboard, + obscureText: obscure, + style: AppText.body, + decoration: InputDecoration( + prefixIcon: Icon(icon, color: AppColors.textSecondary), + suffixIcon: trailing, + hintText: hint, + hintStyle: TextStyle(color: AppColors.textTertiary), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + ), + ), + ); + } + + Widget _buildSubmitButton() { + return AppButton( + label: _isLogin ? 'Entrar' : 'Criar Conta', + icon: Icons.arrow_forward_rounded, + loading: _isLoading, + onPressed: () => _isLogin ? _handleLogin() : _handleSignUp(), + ); + } + + Future _handleLogin() async { + final email = _emailController.text.trim(); + final password = _passwordController.text.trim(); + if (email.isEmpty || password.isEmpty) { + AppSnack.error(context, 'Por favor, preencha todos os campos'); + return; + } + setState(() => _isLoading = true); + try { + final response = + await Supabase.instance.client.auth.signInWithPassword( + email: email, + password: password, + ); + if (response.user != null && mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } + } on AuthException catch (e) { + if (mounted) AppSnack.error(context, 'Erro de login: ${e.message}'); + } catch (e) { + if (mounted) AppSnack.error(context, 'Erro: $e'); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + Future _handleSignUp() async { + final email = _emailController.text.trim(); + final username = _usernameController.text.trim(); + final password = _passwordController.text.trim(); + if (email.isEmpty || username.isEmpty || password.isEmpty) { + AppSnack.error(context, 'Por favor, preencha todos os campos'); + return; + } + setState(() => _isLoading = true); + try { + final response = await Supabase.instance.client.auth.signUp( + email: email, + password: password, + data: {'username': username}, + ); + if (response.user != null) { + await Supabase.instance.client.from('users').insert({ + 'id': response.user!.id, + 'nome': username, + 'email': email, + }); + if (mounted) { + AppSnack.success(context, 'Conta criada com sucesso!'); + setState(() => _isLogin = true); + } + } + } on AuthException catch (e) { + if (mounted) { + AppSnack.error(context, 'Erro ao criar conta: ${e.message}'); + } + } catch (e) { + if (mounted) AppSnack.error(context, 'Erro: $e'); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..be3fc36 --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,369 @@ +import 'package:flutter/material.dart'; + +/// Design tokens for DayMaker — a single source of truth for the new look. +class AppColors { + // Brand + static const Color primary = Color(0xFF2563EB); // refined modern blue + static const Color primaryDark = Color(0xFF1D4ED8); + static const Color primaryLight = Color(0xFF60A5FA); + static const Color accent = Color(0xFFFB923C); // warm orange accent (legacy peach reborn) + + // Neutrals + static const Color background = Color(0xFFF7F8FB); + static const Color surface = Colors.white; + static const Color surfaceAlt = Color(0xFFF1F5F9); + static const Color border = Color(0xFFE2E8F0); + static const Color divider = Color(0xFFEDF2F7); + + // Text + static const Color textPrimary = Color(0xFF0F172A); + static const Color textSecondary = Color(0xFF64748B); + static const Color textTertiary = Color(0xFF94A3B8); + static const Color textOnPrimary = Colors.white; + + // Status + static const Color success = Color(0xFF10B981); + static const Color error = Color(0xFFEF4444); + static const Color warning = Color(0xFFF59E0B); + + // Gradients + static const LinearGradient brandGradient = LinearGradient( + colors: [Color(0xFF2563EB), Color(0xFF60A5FA)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + static const LinearGradient warmGradient = LinearGradient( + colors: [Color(0xFFFB923C), Color(0xFFF472B6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); +} + +class AppRadius { + static const double sm = 8; + static const double md = 12; + static const double lg = 16; + static const double xl = 20; + static const double pill = 999; +} + +class AppSpacing { + static const double xs = 4; + static const double sm = 8; + static const double md = 12; + static const double lg = 16; + static const double xl = 20; + static const double xxl = 24; + static const double huge = 32; +} + +class AppShadows { + static List soft = [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ]; + + static List medium = [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ]; + + static List brand = [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.25), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ]; +} + +class AppText { + static const TextStyle h1 = TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + height: 1.2, + ); + + static const TextStyle h2 = TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + height: 1.25, + ); + + static const TextStyle h3 = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + height: 1.3, + ); + + static const TextStyle body = TextStyle( + fontSize: 15, + color: AppColors.textPrimary, + height: 1.4, + ); + + static const TextStyle bodySecondary = TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + height: 1.4, + ); + + static const TextStyle caption = TextStyle( + fontSize: 12, + color: AppColors.textTertiary, + height: 1.3, + ); + + static const TextStyle button = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0.2, + ); + + static const TextStyle label = TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + letterSpacing: 0.3, + ); +} + +/// Shared decorations for cards and surfaces. +class AppDecorations { + static BoxDecoration card({Color? color, double radius = AppRadius.lg}) => + BoxDecoration( + color: color ?? AppColors.surface, + borderRadius: BorderRadius.circular(radius), + boxShadow: AppShadows.soft, + ); + + static BoxDecoration outlined({double radius = AppRadius.lg}) => + BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: AppColors.border), + ); + + static BoxDecoration filled({Color? color, double radius = AppRadius.md}) => + BoxDecoration( + color: color ?? AppColors.surfaceAlt, + borderRadius: BorderRadius.circular(radius), + ); +} + +/// Reusable widgets. +class AppButton extends StatelessWidget { + final String label; + final IconData? icon; + final VoidCallback? onPressed; + final bool loading; + final bool secondary; + final bool danger; + final double height; + + const AppButton({ + super.key, + required this.label, + this.icon, + this.onPressed, + this.loading = false, + this.secondary = false, + this.danger = false, + this.height = 54, + }); + + @override + Widget build(BuildContext context) { + final disabled = onPressed == null || loading; + final bg = danger + ? AppColors.error + : secondary + ? AppColors.surface + : AppColors.primary; + final fg = secondary ? AppColors.primary : Colors.white; + + return SizedBox( + width: double.infinity, + height: height, + child: AnimatedOpacity( + opacity: disabled && !loading ? 0.5 : 1, + duration: const Duration(milliseconds: 150), + child: Material( + color: bg, + borderRadius: BorderRadius.circular(AppRadius.lg), + elevation: secondary ? 0 : 0, + child: InkWell( + onTap: disabled ? null : onPressed, + borderRadius: BorderRadius.circular(AppRadius.lg), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppRadius.lg), + border: secondary + ? Border.all(color: AppColors.primary, width: 1.5) + : null, + boxShadow: secondary || danger ? null : AppShadows.brand, + ), + alignment: Alignment.center, + child: loading + ? SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator( + strokeWidth: 2.4, + valueColor: AlwaysStoppedAnimation(fg), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, color: fg, size: 20), + const SizedBox(width: 8), + ], + Text( + label, + style: AppText.button.copyWith(color: fg), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +/// Pill-style chip for filters/tags. Animated selection. +class AppChip extends StatelessWidget { + final String label; + final IconData? icon; + final bool selected; + final VoidCallback? onTap; + final Color? color; + + const AppChip({ + super.key, + required this.label, + this.icon, + this.selected = false, + this.onTap, + this.color, + }); + + @override + Widget build(BuildContext context) { + final accent = color ?? AppColors.primary; + return AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + decoration: BoxDecoration( + color: selected ? accent : AppColors.surface, + borderRadius: BorderRadius.circular(AppRadius.pill), + border: Border.all( + color: selected ? accent : AppColors.border, + width: 1.2, + ), + boxShadow: selected + ? [ + BoxShadow( + color: accent.withValues(alpha: 0.25), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ] + : null, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.pill), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 16, + color: selected ? Colors.white : accent, + ), + const SizedBox(width: 6), + ], + Text( + label, + style: TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w600, + color: selected ? Colors.white : AppColors.textPrimary, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +/// Helpers for snackbars +class AppSnack { + static void error(BuildContext context, String message) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.white), + const SizedBox(width: 10), + Expanded(child: Text(message)), + ], + ), + backgroundColor: AppColors.error, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.md), + ), + ), + ); + } + + static void success(BuildContext context, String message) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle_outline, color: Colors.white), + const SizedBox(width: 10), + Expanded(child: Text(message)), + ], + ), + backgroundColor: AppColors.success, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.md), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 053e383..dbebbc9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -404,18 +404,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -713,10 +713,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.7" typed_data: dependency: transitive description: