541 lines
18 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|