diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 91b263a..5c99a68 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,8 @@ + + + + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSCameraUsageDescription + O DayMaker precisa da câmara para tirares fotos dos itens que queres registar. + NSPhotoLibraryUsageDescription + O DayMaker precisa da galeria para escolheres fotos dos itens que queres registar. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/Screens/add_item_screen.dart b/lib/Screens/add_item_screen.dart index 9f87bed..4f932ce 100644 --- a/lib/Screens/add_item_screen.dart +++ b/lib/Screens/add_item_screen.dart @@ -22,10 +22,10 @@ class _AddItemScreenState extends State { bool _isLoading = false; - Future _pickImage() async { + Future _pickImage(ImageSource source) async { final picker = ImagePicker(); final image = await picker.pickImage( - source: ImageSource.gallery, + source: source, maxWidth: 1024, imageQuality: 80, ); @@ -34,6 +34,46 @@ class _AddItemScreenState extends State { } } + 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(); @@ -185,7 +225,7 @@ class _AddItemScreenState extends State { // Image picker Center( child: GestureDetector( - onTap: _pickImage, + onTap: _showImageSourcePicker, child: Container( width: 140, height: 140, @@ -297,7 +337,7 @@ class _AddItemScreenState extends State { value: _selectedCategory, hint: const Text('Selecione uma categoria'), isExpanded: true, - items: ITEM_CATEGORIES.map((category) { + items: itemCategories.map((category) { return DropdownMenuItem( value: category, child: Row( @@ -407,13 +447,13 @@ class _AddItemScreenState extends State { Wrap( spacing: 8, runSpacing: 8, - children: CONTEXT_TAGS.map((tag) { + 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).withOpacity(0.2), + selectedColor: const Color(0xFF0066CC).withValues(alpha: 0.2), checkmarkColor: const Color(0xFF0066CC), backgroundColor: Colors.white, labelStyle: TextStyle( diff --git a/lib/Screens/home_screen.dart b/lib/Screens/home_screen.dart index 8dc6e32..e34542e 100644 --- a/lib/Screens/home_screen.dart +++ b/lib/Screens/home_screen.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'perfil_screen.dart'; import 'add_item_screen.dart'; import 'item_screen.dart'; import 'week_screen.dart'; +import 'ai_chat_screen.dart'; import '../constants/item_categories.dart'; class HomeScreen extends StatefulWidget { @@ -20,6 +21,7 @@ class _HomeScreenState extends State { const _HomeContent(), const _ItemsScreen(), const _WeekScreen(), + const _AiScreen(), const _ProfileScreen(), ]; @@ -36,7 +38,10 @@ class _HomeScreenState extends State { backgroundColor: Colors.white, type: BottomNavigationBarType.fixed, items: const [ - BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'), + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Início', + ), BottomNavigationBarItem( icon: Icon(Icons.inventory_2_outlined), label: 'Itens', @@ -45,6 +50,10 @@ class _HomeScreenState extends State { 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', @@ -136,7 +145,7 @@ class _HomeContentState extends State<_HomeContent> { _isLoading = false; }); } catch (e) { - print('Error loading home: $e'); + debugPrint('Error loading home: $e'); setState(() => _isLoading = false); } } @@ -315,9 +324,9 @@ class _HomeContentState extends State<_HomeContent> { final imageUrl = (images != null && images.isNotEmpty) ? images.first['image_url'] : null; - final category = ITEM_CATEGORIES.firstWhere( + final category = itemCategories.firstWhere( (c) => c.id == item['categoria'], - orElse: () => ITEM_CATEGORIES.last, + orElse: () => itemCategories.last, ); return Container( width: 80, @@ -332,7 +341,8 @@ class _HomeContentState extends State<_HomeContent> { width: 60, height: 60, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _placeholder(category.icon), + errorBuilder: (context, error, stackTrace) => + _placeholder(category.icon), ) : _placeholder(category.icon), ), @@ -352,7 +362,7 @@ class _HomeContentState extends State<_HomeContent> { return Container( width: 60, height: 60, - color: const Color(0xFF0066CC).withOpacity(0.1), + color: const Color(0xFF0066CC).withValues(alpha: 0.1), alignment: Alignment.center, child: Text(icon, style: const TextStyle(fontSize: 26)), ); @@ -391,9 +401,9 @@ class _HomeContentState extends State<_HomeContent> { final imageUrl = (images != null && images.isNotEmpty) ? images.first['image_url'] : null; - final category = ITEM_CATEGORIES.firstWhere( + final category = itemCategories.firstWhere( (c) => c.id == item['categoria'], - orElse: () => ITEM_CATEGORIES.last, + orElse: () => itemCategories.last, ); return Container( width: 110, @@ -414,13 +424,13 @@ class _HomeContentState extends State<_HomeContent> { width: double.infinity, height: 70, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => + errorBuilder: (context, error, stackTrace) => _placeholder(category.icon), ) : Container( width: double.infinity, height: 70, - color: const Color(0xFF0066CC).withOpacity(0.1), + color: const Color(0xFF0066CC).withValues(alpha: 0.1), alignment: Alignment.center, child: Text( category.icon, @@ -468,6 +478,13 @@ class _WeekScreen extends StatelessWidget { 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(); diff --git a/lib/Screens/item_screen.dart b/lib/Screens/item_screen.dart index b4b3da3..3fccd76 100644 --- a/lib/Screens/item_screen.dart +++ b/lib/Screens/item_screen.dart @@ -56,7 +56,7 @@ class _ItemScreenState extends State { _isLoading = false; }); } catch (e) { - print('Error loading items: $e'); + debugPrint('Error loading items: $e'); setState(() => _isLoading = false); } } @@ -71,15 +71,15 @@ class _ItemScreenState extends State { String _categoryName(String? id) { if (id == null) return 'Outros'; - return ITEM_CATEGORIES - .firstWhere((c) => c.id == id, orElse: () => ITEM_CATEGORIES.last) + return itemCategories + .firstWhere((c) => c.id == id, orElse: () => itemCategories.last) .name; } String _categoryIcon(String? id) { if (id == null) return '📦'; - return ITEM_CATEGORIES - .firstWhere((c) => c.id == id, orElse: () => ITEM_CATEGORIES.last) + return itemCategories + .firstWhere((c) => c.id == id, orElse: () => itemCategories.last) .icon; } @@ -231,7 +231,7 @@ class _ItemScreenState extends State { scrollDirection: Axis.horizontal, children: [ _categoryChip(null, 'Todos', '🗂'), - ...ITEM_CATEGORIES.map( + ...itemCategories.map( (c) => _categoryChip(c.id, c.name, c.icon), ), ], @@ -324,7 +324,7 @@ class _ItemScreenState extends State { width: 72, height: 72, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => + errorBuilder: (context, error, stackTrace) => _iconPlaceholder(categoryIcon), ) : _iconPlaceholder(categoryIcon), @@ -362,7 +362,9 @@ class _ItemScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: const Color(0xFF0066CC).withOpacity(0.1), + color: const Color( + 0xFF0066CC, + ).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( @@ -430,7 +432,7 @@ class _ItemScreenState extends State { return Container( width: 72, height: 72, - color: const Color(0xFF0066CC).withOpacity(0.1), + color: const Color(0xFF0066CC).withValues(alpha: 0.1), alignment: Alignment.center, child: Text(icon, style: const TextStyle(fontSize: 32)), ); @@ -451,10 +453,10 @@ class ItemDetailScreen extends StatelessWidget { final categoryId = item['categoria'] as String?; final categoryName = categoryId == null ? 'Outros' - : ITEM_CATEGORIES + : itemCategories .firstWhere( (c) => c.id == categoryId, - orElse: () => ITEM_CATEGORIES.last, + orElse: () => itemCategories.last, ) .name; final tags = List.from(item['tags'] ?? []); @@ -482,7 +484,8 @@ class ItemDetailScreen extends StatelessWidget { width: double.infinity, height: 250, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), + errorBuilder: (context, error, stackTrace) => + const SizedBox.shrink(), ), ), const SizedBox(height: 20), @@ -504,7 +507,7 @@ class ItemDetailScreen extends StatelessWidget { label: Text(t), backgroundColor: const Color( 0xFF0066CC, - ).withOpacity(0.1), + ).withValues(alpha: 0.1), labelStyle: const TextStyle(color: Color(0xFF0066CC)), ), ) @@ -561,9 +564,9 @@ class _EditItemScreenState extends State { _nameController = TextEditingController(text: widget.item['nome'] ?? ''); final catId = widget.item['categoria'] as String?; if (catId != null) { - _selectedCategory = ITEM_CATEGORIES.firstWhere( + _selectedCategory = itemCategories.firstWhere( (c) => c.id == catId, - orElse: () => ITEM_CATEGORIES.last, + orElse: () => itemCategories.last, ); } final tags = List.from(widget.item['tags'] ?? []); @@ -653,7 +656,7 @@ class _EditItemScreenState extends State { child: DropdownButton( value: _selectedCategory, isExpanded: true, - items: ITEM_CATEGORIES + items: itemCategories .map( (c) => DropdownMenuItem( value: c, @@ -674,7 +677,7 @@ class _EditItemScreenState extends State { Wrap( spacing: 8, runSpacing: 8, - children: CONTEXT_TAGS.map((tag) { + children: contextTags.map((tag) { final selected = _selectedTags.contains(tag.id); return FilterChip( label: Text(tag.name), @@ -688,7 +691,7 @@ class _EditItemScreenState extends State { } }); }, - selectedColor: const Color(0xFF0066CC).withOpacity(0.2), + selectedColor: const Color(0xFF0066CC).withValues(alpha: 0.2), checkmarkColor: const Color(0xFF0066CC), backgroundColor: Colors.white, ); diff --git a/lib/Screens/perfil_screen.dart b/lib/Screens/perfil_screen.dart index e690fd9..727a0ea 100644 --- a/lib/Screens/perfil_screen.dart +++ b/lib/Screens/perfil_screen.dart @@ -45,7 +45,7 @@ class _PerfilScreenState extends State { }); } } catch (e) { - print('Error loading user data: $e'); + debugPrint('Error loading user data: $e'); } finally { setState(() => _isLoading = false); } diff --git a/lib/Screens/week_screen.dart b/lib/Screens/week_screen.dart index 09b2845..633aaf1 100644 --- a/lib/Screens/week_screen.dart +++ b/lib/Screens/week_screen.dart @@ -42,8 +42,11 @@ 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) => @@ -107,7 +110,7 @@ class _WeekScreenState extends State { _isLoading = false; }); } catch (e) { - print('Error loading day items: $e'); + debugPrint('Error loading day items: $e'); setState(() => _isLoading = false); } } @@ -123,8 +126,9 @@ class _WeekScreenState extends State { 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>( @@ -193,10 +197,12 @@ class _WeekScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Row( children: days.map((day) { - final isSelected = day.year == _selectedDay.year && + final isSelected = + day.year == _selectedDay.year && day.month == _selectedDay.month && day.day == _selectedDay.day; - final isToday = day.year == DateTime.now().year && + final isToday = + day.year == DateTime.now().year && day.month == DateTime.now().month && day.day == DateTime.now().day; return Expanded( @@ -280,39 +286,39 @@ class _WeekScreenState extends State { 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', - style: TextStyle( - color: Color(0xFF666666), - fontSize: 16, - ), - ), - const SizedBox(height: 4), - const Text( - 'Toque em + para adicionar', - style: TextStyle( - color: Color(0xFF999999), - fontSize: 13, - ), - ), - ], + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.calendar_today_outlined, + size: 56, + color: Colors.grey[400], ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _dayItems.length, - itemBuilder: (_, i) => _buildItemTile(_dayItems[i]), - ), + const SizedBox(height: 12), + const Text( + 'Nenhum item para este dia', + style: TextStyle( + color: Color(0xFF666666), + fontSize: 16, + ), + ), + const SizedBox(height: 4), + const Text( + 'Toque em + para adicionar', + style: TextStyle( + color: Color(0xFF999999), + fontSize: 13, + ), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _dayItems.length, + itemBuilder: (_, i) => _buildItemTile(_dayItems[i]), + ), ), ], ), @@ -326,11 +332,12 @@ 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'] : null; - final category = ITEM_CATEGORIES.firstWhere( + final imageUrl = (images != null && images.isNotEmpty) + ? images.first['image_url'] + : null; + final category = itemCategories.firstWhere( (c) => c.id == item['categoria'], - orElse: () => ITEM_CATEGORIES.last, + orElse: () => itemCategories.last, ); return Card( @@ -346,7 +353,8 @@ class _WeekScreenState extends State { width: 56, height: 56, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _icon(category.icon), + errorBuilder: (context, error, stackTrace) => + _icon(category.icon), ) : _icon(category.icon), ), @@ -367,7 +375,7 @@ class _WeekScreenState extends State { return Container( width: 56, height: 56, - color: const Color(0xFF0066CC).withOpacity(0.1), + color: const Color(0xFF0066CC).withValues(alpha: 0.1), alignment: Alignment.center, child: Text(icon, style: const TextStyle(fontSize: 26)), ); @@ -393,10 +401,9 @@ 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(); @@ -457,13 +464,12 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> { 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 = ITEM_CATEGORIES.firstWhere( + final imageUrl = (images != null && images.isNotEmpty) + ? images.first['image_url'] + : null; + final category = itemCategories.firstWhere( (c) => c.id == item['categoria'], - orElse: () => ITEM_CATEGORIES.last, + orElse: () => itemCategories.last, ); return CheckboxListTile( value: selected, @@ -487,20 +493,24 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> { width: 48, height: 48, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container( - width: 48, - height: 48, - color: const Color(0xFF0066CC) - .withOpacity(0.1), - alignment: Alignment.center, - child: Text(category.icon), - ), + 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) - .withOpacity(0.1), + color: const Color( + 0xFF0066CC, + ).withValues(alpha: 0.1), alignment: Alignment.center, child: Text(category.icon), ), @@ -527,10 +537,7 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> { ), child: Text( 'Adicionar (${_selected.length})', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), + style: const TextStyle(color: Colors.white, fontSize: 16), ), ), ), diff --git a/lib/constants/item_categories.dart b/lib/constants/item_categories.dart index 4444985..fe93459 100644 --- a/lib/constants/item_categories.dart +++ b/lib/constants/item_categories.dart @@ -37,11 +37,11 @@ class ContextTag { } // Categorias principais -final List ITEM_CATEGORIES = [ +final List itemCategories = [ ItemCategory( id: 'clothing', name: 'Roupa', - icon: '', + icon: '👕', description: 'Peças de vestuário', subcategories: [ Subcategory( @@ -79,7 +79,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'electronics', name: 'Eletrónica', - icon: '', + icon: '💻', description: 'Dispositivos e acessórios tecnológicos', subcategories: [ Subcategory( @@ -117,7 +117,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'footwear', name: 'Calçado', - icon: '', + icon: '👟', description: 'Sapatos, botas, sandálias', subcategories: [ Subcategory( @@ -145,7 +145,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'accessories', name: 'Acessórios', - icon: '', + icon: '🎒', description: 'Bolsas, relógios, óculos, bijuteria', subcategories: [ Subcategory( @@ -175,7 +175,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'documents', name: 'Documentos', - icon: '', + icon: '📄', description: 'Passaporte, cartões, papéis importantes', subcategories: [ Subcategory( @@ -203,14 +203,14 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'other', name: 'Outros', - icon: '', + icon: '📦', description: 'Tudo o resto', subcategories: [], ), ]; // Tags de contexto -final List CONTEXT_TAGS = [ +final List contextTags = [ ContextTag( id: 'travel', name: 'Viagem', diff --git a/pubspec.lock b/pubspec.lock index dbebbc9..053e383 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -404,18 +404,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -713,10 +713,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" typed_data: dependency: transitive description: