Files
BudgetApp/lib/pages/sms/widgets/sms_message_widget.dart
T
2025-08-20 18:28:10 +03:00

541 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:budget_app/logic/sms/item/sms_item_cubit.dart';
import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/pages/sms/widgets/sms_settings_dialog.dart';
import 'package:budget_app/l10n/app_localizations.dart';
class SmsMessageWidget extends StatelessWidget {
final SmsMessage message;
const SmsMessageWidget({
super.key,
required this.message,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
return BlocBuilder<SmsItemCubit, SmsItemState>(
builder: (context, state) {
// Получаем актуальное сообщение из кубита
final currentMessage = context.read<SmsItemCubit>().currentMessage;
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: _getGradientForStatus(currentMessage.status, context),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Builder(
builder: (context) => Icon(
Icons.account_circle,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[600],
size: 20,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
currentMessage.sender ?? loc.unknownSender,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
],
),
const SizedBox(height: 8),
Builder(
builder: (context) => Text(
currentMessage.body ?? '',
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[300]
: Colors.grey[700],
fontSize: 14,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(width: 8),
_buildStatusIndicator(currentMessage.status, context, loc),
const SizedBox(width: 8),
_buildMenuButton(context, currentMessage, state, loc),
],
),
const SizedBox(height: 12),
_buildBottomRow(currentMessage, state, context, loc),
],
),
),
),
);
},
);
}
LinearGradient _getGradientForStatus(SmsStatus status, BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
switch (status) {
case SmsStatus.pending:
return LinearGradient(
colors: isDark ? [
Colors.grey.shade800,
Colors.grey.shade700,
] : [
Colors.grey.shade50,
Colors.grey.shade100,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case SmsStatus.processed:
return LinearGradient(
colors: isDark ? [
Colors.grey.shade900,
Colors.grey.shade800,
] : [
Colors.grey.shade200,
Colors.grey.shade300,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case SmsStatus.ignored:
return LinearGradient(
colors: isDark ? [
Colors.grey.shade900,
Colors.grey.shade800,
] : [
Colors.grey.shade100,
Colors.grey.shade200,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case SmsStatus.error:
return LinearGradient(
colors: isDark ? [
Colors.grey.shade800,
Colors.grey.shade700,
] : [
Colors.grey.shade100,
Colors.grey.shade200,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
}
Widget _buildStatusIndicator(SmsStatus status, BuildContext context, AppLocalizations loc) {
final isDark = Theme.of(context).brightness == Brightness.dark;
Color color;
IconData icon;
String label;
switch (status) {
case SmsStatus.pending:
color = isDark ? Colors.grey.shade400 : Colors.grey.shade600;
icon = Icons.access_time;
label = loc.pendingStatus;
break;
case SmsStatus.processed:
color = isDark ? Colors.grey.shade300 : Colors.black87;
icon = Icons.check_circle;
label = loc.processedStatus;
break;
case SmsStatus.ignored:
color = isDark ? Colors.grey.shade500 : Colors.grey.shade700;
icon = Icons.block;
label = loc.ignoredStatus;
break;
case SmsStatus.error:
color = isDark ? Colors.grey.shade400 : Colors.grey.shade600;
icon = Icons.error;
label = loc.errorStatus;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildBottomRow(SmsMessage message, SmsItemState state, BuildContext context, AppLocalizations loc) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (message.date != null)
Text(
_formatDate(message.date!, loc),
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[600],
fontSize: 12,
),
),
if (state is SmsItemProcessing)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
// Показываем ошибку из состояния или из сохраненного сообщения
if ((state is SmsItemError) || (message.status == SmsStatus.error && message.errorMessage != null)) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade800
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade600
: Colors.grey.shade400,
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade400
: Colors.grey.shade700,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
// Используем ошибку из состояния, если она есть, иначе из сохраненного сообщения
state is SmsItemError ? state.error : message.errorMessage!,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey.shade300
: Colors.grey.shade700,
fontSize: 12,
),
),
),
],
),
),
],
],
);
}
Widget _buildMenuButton(BuildContext context, SmsMessage message, SmsItemState state, AppLocalizations loc) {
// Don't show menu if processing
if (state is SmsItemProcessing) {
return const SizedBox.shrink();
}
return PopupMenuButton<String>(
key: Key('sms_menu_${message.id}'),
icon: const Icon(Icons.more_vert, size: 20),
tooltip: 'Действия с сообщением',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 200,
maxWidth: 250,
),
onSelected: (String value) {
_handleMenuAction(context, value, message);
},
itemBuilder: (BuildContext context) {
List<PopupMenuEntry<String>> items = [];
// Always show settings and processing options first
items.addAll([
const PopupMenuItem<String>(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings, color: Colors.blue),
SizedBox(width: 8),
Text('Настройка правил'),
],
),
),
const PopupMenuItem<String>(
value: 'process',
child: Row(
children: [
Icon(Icons.auto_awesome, color: Colors.purple),
SizedBox(width: 8),
Text('Обработать сообщение'),
],
),
),
]);
// Add divider if we have items
if (items.isNotEmpty) {
items.add(const PopupMenuDivider());
}
// Show different options based on status
if (message.status == SmsStatus.pending) {
items.addAll([
PopupMenuItem<String>(
value: 'create_transaction',
child: Row(
children: [
Icon(Icons.add_card, color: Colors.green),
SizedBox(width: 8),
Text(loc.createTransactionAction),
],
),
),
PopupMenuItem<String>(
value: 'ignore',
child: Row(
children: [
Icon(Icons.block, color: Colors.orange),
SizedBox(width: 8),
Text(loc.ignoreAction),
],
),
),
]);
} else if (message.status == SmsStatus.processed) {
items.add(
PopupMenuItem<String>(
value: 'view_transaction',
child: Row(
children: [
Icon(Icons.visibility, color: Colors.blue),
SizedBox(width: 8),
Text(loc.viewTransaction),
],
),
),
);
} else if (message.status == SmsStatus.ignored) {
items.add(
PopupMenuItem<String>(
value: 'unignore',
child: Row(
children: [
Icon(Icons.restore, color: Colors.blue),
SizedBox(width: 8),
Text(loc.returnToProcessing),
],
),
),
);
} else if (message.status == SmsStatus.error) {
items.addAll([
PopupMenuItem<String>(
value: 'retry',
child: Row(
children: [
Icon(Icons.refresh, color: Colors.blue),
SizedBox(width: 8),
Text(loc.retry),
],
),
),
PopupMenuItem<String>(
value: 'ignore',
child: Row(
children: [
Icon(Icons.block, color: Colors.orange),
SizedBox(width: 8),
Text(loc.ignoreAction),
],
),
),
]);
}
// Always add delete option
if (items.isNotEmpty) {
items.add(const PopupMenuDivider());
}
items.add(
PopupMenuItem<String>(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text(loc.delete, style: TextStyle(color: Colors.red)),
],
),
),
);
return items;
},
);
}
void _handleMenuAction(BuildContext context, String action, SmsMessage message) {
final cubit = context.read<SmsItemCubit>();
final loc = AppLocalizations.of(context)!;
switch (action) {
case 'settings':
_showSmsSettings(context, message);
break;
case 'process':
cubit.createTransaction(loc);
break;
case 'create_transaction':
cubit.createTransaction(loc);
break;
case 'ignore':
cubit.ignoreSms();
break;
case 'view_transaction':
_showTransactionDetails(context, message);
break;
case 'unignore':
// TODO: Implement unignore functionality
_showNotImplementedMessage(context, loc.returnToProcessingFeature);
break;
case 'retry':
cubit.createTransaction(loc);
break;
case 'delete':
_showDeleteConfirmation(context, message);
break;
}
}
void _showSmsSettings(BuildContext context, SmsMessage message) {
final loc = AppLocalizations.of(context)!;
showDialog(
context: context,
builder: (context) => SmsSettingsDialog(
sender: message.sender ?? loc.unknownSender,
),
);
}
void _showTransactionDetails(BuildContext context, SmsMessage message) {
final loc = AppLocalizations.of(context)!;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(loc.transactionDetails),
content: Text(loc.transactionForSms(message.sender ?? loc.unknownSender)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(loc.close),
),
],
),
);
}
void _showNotImplementedMessage(BuildContext context, String feature) {
final loc = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(loc.notImplemented(feature)),
duration: const Duration(seconds: 2),
),
);
}
void _showDeleteConfirmation(BuildContext context, SmsMessage message) {
final loc = AppLocalizations.of(context)!;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(loc.deleteMessage),
content: Text(loc.deleteConfirmation),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(loc.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_showNotImplementedMessage(context, loc.deleteSmsFeature);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(loc.delete),
),
],
),
);
}
String _formatDate(DateTime dateTime, AppLocalizations loc) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays > 0) {
return loc.daysAgo(difference.inDays);
} else if (difference.inHours > 0) {
return loc.hoursAgo(difference.inHours);
} else if (difference.inMinutes > 0) {
return loc.minutesAgo(difference.inMinutes);
} else {
return loc.justNow;
}
}
}