IA Funcinonal
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/ai_recommendation_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class AiChatScreen extends StatefulWidget {
|
||||
const AiChatScreen({super.key});
|
||||
@@ -21,19 +22,25 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
];
|
||||
bool _isLoading = false;
|
||||
|
||||
Future<void> _sendMessage([String? suggestion]) async {
|
||||
Future<void> _sendMessage({
|
||||
String? suggestion,
|
||||
bool silent = false,
|
||||
bool hideUserMessage = false,
|
||||
}) async {
|
||||
final text = (suggestion ?? _controller.text).trim();
|
||||
if (text.isEmpty || _isLoading) return;
|
||||
|
||||
setState(() {
|
||||
_messages.add(_ChatMessage(text: text, isUser: true));
|
||||
if (!hideUserMessage) {
|
||||
_messages.add(_ChatMessage(text: text, isUser: true));
|
||||
}
|
||||
_isLoading = true;
|
||||
});
|
||||
_controller.clear();
|
||||
_scrollToBottom();
|
||||
|
||||
try {
|
||||
final response = await _service.recommendForOccasion(text);
|
||||
final response = await _service.sendMessage(text, silent: silent);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_messages.add(_ChatMessage(text: response, isUser: false));
|
||||
@@ -43,7 +50,7 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
setState(() {
|
||||
_messages.add(
|
||||
_ChatMessage(
|
||||
text: 'Não consegui gerar uma recomendação agora. Tenta novamente.',
|
||||
text: 'Nao consegui gerar uma recomendacao agora. Tenta novamente.',
|
||||
isUser: false,
|
||||
),
|
||||
);
|
||||
@@ -77,24 +84,16 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFFFE5CC),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF0066CC),
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'DayMaker IA',
|
||||
style: TextStyle(color: Colors.white, fontSize: 20),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildSuggestions(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
itemCount: _messages.length + (_isLoading ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (_isLoading && index == _messages.length) {
|
||||
@@ -111,6 +110,54 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.brandGradient,
|
||||
boxShadow: AppShadows.brand,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.22),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'DayMaker IA',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Pergunta-me sobre o teu dia ou viagem',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestions() {
|
||||
final suggestions = [
|
||||
'Viagem para Itália',
|
||||
@@ -120,19 +167,18 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
color: const Color(0xFFFFE5CC),
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
|
||||
color: AppColors.background,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: suggestions.map((suggestion) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
backgroundColor: Colors.white,
|
||||
side: const BorderSide(color: Color(0xFFE0E0E0)),
|
||||
label: Text(suggestion),
|
||||
onPressed: () => _sendMessage(suggestion),
|
||||
child: AppChip(
|
||||
label: suggestion,
|
||||
icon: Icons.bolt_rounded,
|
||||
onTap: () => _sendMessage(suggestion: suggestion),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -143,15 +189,25 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
|
||||
Widget _buildInput() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: AppColors.surfaceAlt,
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
@@ -159,8 +215,10 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
maxLines: 4,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
style: AppText.body,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Ex: viagem para Itália de 5 dias...',
|
||||
hintText: 'Escreve uma mensagem...',
|
||||
hintStyle: TextStyle(color: AppColors.textTertiary),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
@@ -170,18 +228,27 @@ class _AiChatScreenState extends State<AiChatScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : () => _sendMessage(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0066CC),
|
||||
shape: const CircleBorder(),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.brandGradient,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: AppShadows.brand,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: _isLoading ? null : () => _sendMessage(),
|
||||
child: const Icon(
|
||||
Icons.send_rounded,
|
||||
color: Colors.white,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.send, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -204,13 +271,8 @@ class _MessageBubble extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final alignment = message.isUser
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft;
|
||||
final backgroundColor = message.isUser
|
||||
? const Color(0xFF0066CC)
|
||||
: Colors.white;
|
||||
final textColor = message.isUser ? Colors.white : const Color(0xFF333333);
|
||||
final isUser = message.isUser;
|
||||
final alignment = isUser ? Alignment.centerRight : Alignment.centerLeft;
|
||||
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
@@ -219,14 +281,25 @@ class _MessageBubble extends StatelessWidget {
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.78,
|
||||
),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
gradient: isUser ? AppColors.brandGradient : null,
|
||||
color: isUser ? null : AppColors.surface,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(AppRadius.lg),
|
||||
topRight: const Radius.circular(AppRadius.lg),
|
||||
bottomLeft: Radius.circular(isUser ? AppRadius.lg : 4),
|
||||
bottomRight: Radius.circular(isUser ? 4 : AppRadius.lg),
|
||||
),
|
||||
boxShadow: isUser ? AppShadows.brand : AppShadows.soft,
|
||||
),
|
||||
child: Text(
|
||||
message.text,
|
||||
style: TextStyle(color: textColor, fontSize: 15, height: 1.35),
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : AppColors.textPrimary,
|
||||
fontSize: 15,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -242,15 +315,24 @@ class _TypingBubble extends StatelessWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: AppColors.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppRadius.lg),
|
||||
topRight: Radius.circular(AppRadius.lg),
|
||||
bottomLeft: Radius.circular(4),
|
||||
bottomRight: Radius.circular(AppRadius.lg),
|
||||
),
|
||||
boxShadow: AppShadows.soft,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../constants/item_categories.dart';
|
||||
import '../services/ai_recommendation_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'add_item_screen.dart';
|
||||
import 'ai_chat_screen.dart';
|
||||
import 'item_screen.dart';
|
||||
import 'perfil_screen.dart';
|
||||
import 'week_screen.dart';
|
||||
@@ -21,6 +23,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
_HomeContent(),
|
||||
ItemScreen(),
|
||||
WeekScreen(),
|
||||
AiChatScreen(),
|
||||
PerfilScreen(),
|
||||
];
|
||||
|
||||
@@ -55,7 +58,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
_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'),
|
||||
_navItem(3, Icons.auto_awesome_rounded, 'IA'),
|
||||
_navItem(4, Icons.person_rounded, 'Perfil'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -87,16 +91,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color:
|
||||
selected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight:
|
||||
selected ? FontWeight.w700 : FontWeight.w500,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
@@ -198,9 +200,8 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_itemCount = all.length;
|
||||
_userName = userRow?['nome'] ??
|
||||
user.email?.split('@').first ??
|
||||
'Utilizador';
|
||||
_userName =
|
||||
userRow?['nome'] ?? user.email?.split('@').first ?? 'Utilizador';
|
||||
_recentItems = List<Map<String, dynamic>>.from(recent);
|
||||
_todayItems = todayItems;
|
||||
_isLoading = false;
|
||||
@@ -227,6 +228,8 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
_buildGreeting(),
|
||||
const SizedBox(height: 20),
|
||||
_buildHeroCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildAiSuggestionButton(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('Hoje'),
|
||||
const SizedBox(height: 12),
|
||||
@@ -249,23 +252,17 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
final saudacao = hour < 12
|
||||
? 'Bom dia'
|
||||
: hour < 19
|
||||
? 'Boa tarde'
|
||||
: 'Boa noite';
|
||||
? 'Boa tarde'
|
||||
: 'Boa noite';
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
saudacao,
|
||||
style: AppText.bodySecondary,
|
||||
),
|
||||
Text(saudacao, style: AppText.bodySecondary),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_userName.isEmpty ? 'Olá!' : _userName,
|
||||
style: AppText.h2,
|
||||
),
|
||||
Text(_userName.isEmpty ? 'Olá!' : _userName, style: AppText.h2),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -413,10 +410,7 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Nada planeado para hoje',
|
||||
style: AppText.body,
|
||||
),
|
||||
Text('Nada planeado para hoje', style: AppText.body),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Vá à aba Semana para organizar',
|
||||
@@ -454,17 +448,11 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
color: AppColors.accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add_box_rounded,
|
||||
color: AppColors.accent,
|
||||
),
|
||||
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,
|
||||
),
|
||||
child: Text('Adicione o seu primeiro item', style: AppText.body),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -509,20 +497,12 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Center(
|
||||
child: Icon(
|
||||
cat.icon,
|
||||
color: cat.color,
|
||||
size: 32,
|
||||
),
|
||||
errorBuilder: (_, _, _) => Center(
|
||||
child: Icon(cat.icon, color: cat.color, size: 32),
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: Icon(
|
||||
cat.icon,
|
||||
color: cat.color,
|
||||
size: 32,
|
||||
),
|
||||
child: Icon(cat.icon, color: cat.color, size: 32),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -569,7 +549,7 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 3,
|
||||
itemBuilder: (_, __) => Container(
|
||||
itemBuilder: (_, _) => Container(
|
||||
width: 110,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -581,6 +561,127 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAiSuggestionButton() {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
onTap: () async {
|
||||
final service = AiRecommendationService();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
final response = await service.sendMessage(
|
||||
'vou fazer uma viagem de 4 horas de onibus',
|
||||
silent: true,
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, color: AppColors.primary),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text('Sugestao da IA', style: AppText.h3),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(response, style: AppText.body),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.warmGradient,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.accent.withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.25),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.auto_awesome,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Pedir sugestao a IA',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Monta um outfit para o teu dia',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 16,
|
||||
color: Colors.white70,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddCta() {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -588,9 +689,7 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.push(
|
||||
MaterialPageRoute(builder: (_) => const AddItemScreen()),
|
||||
)
|
||||
.push(MaterialPageRoute(builder: (_) => const AddItemScreen()))
|
||||
.then((_) => _loadData());
|
||||
},
|
||||
child: Container(
|
||||
@@ -613,8 +712,11 @@ class _HomeContentState extends State<_HomeContent> {
|
||||
gradient: AppColors.brandGradient,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child:
|
||||
const Icon(Icons.add_rounded, color: Colors.white, size: 24),
|
||||
child: const Icon(
|
||||
Icons.add_rounded,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
const Expanded(
|
||||
|
||||
@@ -21,12 +21,15 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
List<Map<String, dynamic>> get _filteredItems {
|
||||
return _items.where((item) {
|
||||
final name = (item['nome'] ?? '').toString().toLowerCase();
|
||||
final tags =
|
||||
List<String>.from(item['tags'] ?? []).join(' ').toLowerCase();
|
||||
final matchesSearch = _searchQuery.isEmpty ||
|
||||
final tags = List<String>.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();
|
||||
@@ -110,8 +113,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
ItemDetailScreen(item: item, imageUrl: _imageUrl(item)),
|
||||
builder: (_) => ItemDetailScreen(item: item, imageUrl: _imageUrl(item)),
|
||||
),
|
||||
).then((_) => _loadItems());
|
||||
}
|
||||
@@ -140,16 +142,14 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _items.isEmpty
|
||||
? _buildEmpty()
|
||||
: _filteredItems.isEmpty
|
||||
? _buildNoResults()
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadItems,
|
||||
color: AppColors.primary,
|
||||
child: _gridView
|
||||
? _buildGrid()
|
||||
: _buildList(),
|
||||
),
|
||||
? _buildEmpty()
|
||||
: _filteredItems.isEmpty
|
||||
? _buildNoResults()
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadItems,
|
||||
color: AppColors.primary,
|
||||
child: _gridView ? _buildGrid() : _buildList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -211,8 +211,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color:
|
||||
selected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -266,8 +265,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
icon: c.icon,
|
||||
color: c.color,
|
||||
selected: _selectedCategoryFilter == c.id,
|
||||
onTap: () =>
|
||||
setState(() => _selectedCategoryFilter = c.id),
|
||||
onTap: () => setState(() => _selectedCategoryFilter = c.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -327,7 +325,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.78,
|
||||
childAspectRatio: 0.50,
|
||||
),
|
||||
itemCount: _filteredItems.length,
|
||||
itemBuilder: (_, i) => _buildGridCard(_filteredItems[i]),
|
||||
@@ -368,18 +366,13 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Center(child: Icon(cat.icon, color: cat.color, size: 40)),
|
||||
errorBuilder: (_, __, ___) => Center(
|
||||
child: Icon(cat.icon, color: cat.color, size: 40),
|
||||
),
|
||||
)
|
||||
else
|
||||
Center(
|
||||
child: Icon(cat.icon, color: cat.color, size: 40),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: _moreButton(item),
|
||||
),
|
||||
Center(child: Icon(cat.icon, color: cat.color, size: 40)),
|
||||
Positioned(top: 8, right: 8, child: _moreButton(item)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -463,11 +456,8 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Icon(
|
||||
cat.icon,
|
||||
color: cat.color,
|
||||
size: 28,
|
||||
),
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Icon(cat.icon, color: cat.color, size: 28),
|
||||
)
|
||||
: Icon(cat.icon, color: cat.color, size: 28),
|
||||
),
|
||||
@@ -525,7 +515,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
|
||||
Widget _moreButton(Map<String, dynamic> item) {
|
||||
return Material(
|
||||
color: Colors.white.withValues(alpha: 0.85),
|
||||
color: Colors.white.withValues(alpha: 0.75),
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
@@ -575,15 +565,10 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
_openItem(item);
|
||||
},
|
||||
),
|
||||
_actionTile(
|
||||
Icons.edit_outlined,
|
||||
'Editar',
|
||||
AppColors.primary,
|
||||
() {
|
||||
Navigator.pop(ctx);
|
||||
_editItem(item);
|
||||
},
|
||||
),
|
||||
_actionTile(Icons.edit_outlined, 'Editar', AppColors.primary, () {
|
||||
Navigator.pop(ctx);
|
||||
_editItem(item);
|
||||
}),
|
||||
_actionTile(
|
||||
Icons.delete_outline_rounded,
|
||||
'Apagar',
|
||||
@@ -619,10 +604,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
||||
),
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: color, fontWeight: FontWeight.w600),
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
@@ -714,13 +696,12 @@ class ItemDetailScreen extends StatelessWidget {
|
||||
Image.network(
|
||||
imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Center(child: Icon(cat.icon, color: cat.color, size: 80)),
|
||||
errorBuilder: (_, __, ___) => Center(
|
||||
child: Icon(cat.icon, color: cat.color, size: 80),
|
||||
),
|
||||
)
|
||||
else
|
||||
Center(
|
||||
child: Icon(cat.icon, color: cat.color, size: 80),
|
||||
),
|
||||
Center(child: Icon(cat.icon, color: cat.color, size: 80)),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -786,8 +767,9 @@ class ItemDetailScreen extends StatelessWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppRadius.pill),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppRadius.pill,
|
||||
),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Text(
|
||||
@@ -853,11 +835,14 @@ class _EditItemScreenState extends State<EditItemScreen> {
|
||||
}
|
||||
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']);
|
||||
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);
|
||||
|
||||
@@ -214,7 +214,7 @@ class _PerfilScreenState extends State<PerfilScreen> {
|
||||
? Image.network(
|
||||
_avatarUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
errorBuilder: (_, _, _) => Container(
|
||||
color: AppColors.surface,
|
||||
child: const Icon(
|
||||
Icons.person_rounded,
|
||||
|
||||
@@ -15,7 +15,15 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
List<Map<String, dynamic>> _dayItems = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
static const _weekdayShort = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
|
||||
static const _weekdayShort = [
|
||||
'Seg',
|
||||
'Ter',
|
||||
'Qua',
|
||||
'Qui',
|
||||
'Sex',
|
||||
'Sáb',
|
||||
'Dom',
|
||||
];
|
||||
static const _weekdayLong = [
|
||||
'Segunda',
|
||||
'Terça',
|
||||
@@ -34,8 +42,11 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
|
||||
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) =>
|
||||
@@ -108,8 +119,9 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
.eq('user_id', user.id);
|
||||
final available = List<Map<String, dynamic>>.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<List<int>>(
|
||||
@@ -122,8 +134,9 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
|
||||
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) {
|
||||
@@ -163,15 +176,12 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
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]),
|
||||
),
|
||||
? _buildEmpty()
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 120),
|
||||
itemCount: _dayItems.length,
|
||||
itemBuilder: (_, i) => _buildItemTile(_dayItems[i]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -188,10 +198,7 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
children: [
|
||||
Text('Minha Semana', style: AppText.h2),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Planeie o que precisa para cada dia',
|
||||
style: AppText.caption,
|
||||
),
|
||||
Text('Planeie o que precisa para cada dia', style: AppText.caption),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -202,11 +209,13 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
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 today = DateTime.now();
|
||||
final isToday = day.year == today.year &&
|
||||
final isToday =
|
||||
day.year == today.year &&
|
||||
day.month == today.month &&
|
||||
day.day == today.day;
|
||||
return Expanded(
|
||||
@@ -322,10 +331,7 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
const SizedBox(height: 16),
|
||||
const Text('Nada planeado', style: AppText.h3),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Toque em + para adicionar itens',
|
||||
style: AppText.caption,
|
||||
),
|
||||
Text('Toque em + para adicionar itens', style: AppText.caption),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -369,7 +375,7 @@ class _WeekScreenState extends State<WeekScreen> {
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
errorBuilder: (_, _, _) =>
|
||||
Icon(cat.icon, color: cat.color, size: 24),
|
||||
)
|
||||
: Icon(cat.icon, color: cat.color, size: 24),
|
||||
@@ -476,10 +482,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();
|
||||
|
||||
@@ -541,8 +546,7 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> {
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (_, i) =>
|
||||
_buildPickerTile(filtered[i]),
|
||||
itemBuilder: (_, i) => _buildPickerTile(filtered[i]),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
@@ -556,8 +560,7 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> {
|
||||
icon: Icons.check_rounded,
|
||||
onPressed: _selected.isEmpty
|
||||
? null
|
||||
: () =>
|
||||
Navigator.pop(context, _selected.toList()),
|
||||
: () => Navigator.pop(context, _selected.toList()),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -612,7 +615,7 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> {
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
errorBuilder: (_, _, _) =>
|
||||
Icon(cat.icon, color: cat.color, size: 22),
|
||||
)
|
||||
: Icon(cat.icon, color: cat.color, size: 22),
|
||||
@@ -649,14 +652,10 @@ class _ItemPickerSheetState extends State<_ItemPickerSheet> {
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: Colors.transparent,
|
||||
color: selected ? AppColors.primary : Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.border,
|
||||
color: selected ? AppColors.primary : AppColors.border,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user