import 'package:flutter/material.dart';
import '../../core/errors/api_error_code.dart';
import '../../l10n/app_localizations.dart';
import '../../models/notification_models.dart';
import '../../repositories/image_suggestion_repository.dart';
import '../../repositories/notifications_repository.dart';
import '../../shared/states/app_state_widgets.dart';
import '../image_suggestion/image_suggestion_status_page.dart';
class NotificationInboxPage extends StatefulWidget {
const NotificationInboxPage({
super.key,
required this.repository,
this.imageSuggestionRepository,
});
final NotificationsRepository repository;
final ImageSuggestionRepository? imageSuggestionRepository;
@override
State<NotificationInboxPage> createState() => _NotificationInboxPageState();
}
class _NotificationInboxPageState extends State<NotificationInboxPage> {
late Future<NotificationList> _notifications;
String? _statusFilter;
@override
void initState() {
super.initState();
_notifications = _loadNotifications();
}
void _retry() {
setState(() {
_notifications = _loadNotifications();
});
}
Future<NotificationList> _loadNotifications() {
return widget.repository.list(status: _statusFilter);
}
void _selectFilter(String? status) {
if (_statusFilter == status) return;
setState(() {
_statusFilter = status;
_notifications = _loadNotifications();
});
}
Future<bool> _markRead(NotificationData item) async {
if (item.status == 'read') return true;
try {
await widget.repository.markRead(item.id);
if (mounted) _retry();
return true;
} on ApiException catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_errorText(AppLocalizations.of(context), error.code)),
),
);
}
return false;
}
}
Future<void> _open(NotificationData item) async {
if (!await _markRead(item) || !mounted) return;
if (item.type != 'image_suggestion_status' ||
widget.imageSuggestionRepository == null) {
return;
}
final suggestionId = int.tryParse(item.payload['suggestion_id'] ?? '');
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ImageSuggestionStatusPage(
repository: widget.imageSuggestionRepository!,
highlightedId: suggestionId,
),
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.notificationsTitle)),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 860),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
Icon(
Icons.notifications_none_outlined,
size: 30,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 14),
Expanded(
child: Text(
_statusFilter == 'unread'
? l10n.notificationsFilterUnread
: l10n.notificationsTitle,
style: Theme.of(context).textTheme.titleLarge,
),
),
ChoiceChip(
label: Text(
_statusFilter == 'unread'
? l10n.notificationsFilterUnread
: l10n.notificationsFilterAll,
),
selected: true,
onSelected: (_) => _showFilterMenu(context),
),
],
),
),
),
),
Expanded(
child: FutureBuilder<NotificationList>(
future: _notifications,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const LoadingState();
}
if (snapshot.hasError) {
final error = snapshot.error;
final code = error is ApiException
? error.code
: ApiErrorCode.unknown;
if (code == ApiErrorCode.permissionDenied) {
return const AuthRequiredState();
}
return ErrorState(onRetry: _retry, code: code);
}
final items = snapshot.data?.items ?? const [];
if (items.isEmpty) {
return EmptyState(
title: l10n.notificationsEmptyTitle,
body: l10n.notificationsEmptyBody,
);
}
final unread = items
.where((item) => item.status == 'unread')
.length;
return RefreshIndicator(
onRefresh: () async => _retry(),
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
itemCount: items.length + 1,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
l10n.notificationsUnreadCount(unread),
style: Theme.of(context).textTheme.bodySmall,
),
);
}
final item = items[index - 1];
final unreadItem = item.status == 'unread';
return Card(
color: unreadItem
? Theme.of(
context,
).colorScheme.surfaceContainerHighest
: null,
child: ListTile(
leading: Icon(
unreadItem
? Icons.notifications_active_outlined
: Icons.notifications_none_outlined,
),
title: Text(
_title(l10n, item),
style: unreadItem
? const TextStyle(
fontWeight: FontWeight.w700,
)
: null,
),
subtitle: Text(_body(l10n, item)),
trailing: unreadItem
? Icon(
Icons.circle,
size: 10,
color: Theme.of(
context,
).colorScheme.primary,
)
: null,
onTap: () => _open(item),
),
);
},
),
);
},
),
),
],
),
),
),
);
}
Future<void> _showFilterMenu(BuildContext context) async {
final selected = await showModalBottomSheet<String>(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.all_inbox_outlined),
title: Text(AppLocalizations.of(context).notificationsFilterAll),
onTap: () => Navigator.of(context).pop('all'),
),
ListTile(
leading: const Icon(Icons.mark_email_unread_outlined),
title: Text(
AppLocalizations.of(context).notificationsFilterUnread,
),
onTap: () => Navigator.of(context).pop('unread'),
),
],
),
),
);
if (mounted && selected != null) {
_selectFilter(selected == 'unread' ? 'unread' : null);
}
}
String _title(AppLocalizations l10n, NotificationData item) {
if (item.type == 'image_suggestion_status') {
return l10n.notificationImageSuggestionTitle;
}
return l10n.notificationsTitle;
}
String _body(AppLocalizations l10n, NotificationData item) {
final status = item.payload['status'] ?? '';
return item.type == 'image_suggestion_status'
? l10n.notificationImageSuggestionBody(_statusLabel(l10n, status))
: l10n.notificationGenericBody;
}
String _statusLabel(AppLocalizations l10n, String status) {
return switch (status) {
'submitted' => l10n.imageSuggestStatusSubmitted,
'in_review' => l10n.imageSuggestStatusInReview,
'accepted' => l10n.imageSuggestStatusAccepted,
'rejected' => l10n.imageSuggestStatusRejected,
'needs_info' => l10n.imageSuggestStatusNeedsInfo,
_ => l10n.imageSuggestStatusPending,
};
}
String _errorText(AppLocalizations l10n, ApiErrorCode code) {
return switch (code) {
ApiErrorCode.permissionDenied => l10n.errorPermissionDenied,
ApiErrorCode.conflict => l10n.errorConflict,
ApiErrorCode.rateLimited => l10n.errorRateLimited,
ApiErrorCode.validation => l10n.errorValidation,
_ => l10n.errorBody,
};
}
}
