356 lines
11 KiB
Dart
356 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import '../../core/services/notification_service.dart';
|
|
import '../../core/models/notification.dart';
|
|
import '../../core/utils/snackbar_util.dart';
|
|
import '../../shared/widgets/skeleton_loader.dart';
|
|
import '../../shared/widgets/empty_state_widget.dart';
|
|
|
|
class NotificationCenterPage extends StatefulWidget {
|
|
const NotificationCenterPage({super.key});
|
|
|
|
@override
|
|
State<NotificationCenterPage> createState() => _NotificationCenterPageState();
|
|
}
|
|
|
|
class _NotificationCenterPageState extends State<NotificationCenterPage> {
|
|
final _scrollController = ScrollController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Initialize service if not already
|
|
if (!Get.isRegistered<NotificationService>()) {
|
|
Get.put(NotificationService());
|
|
}
|
|
|
|
// Load notifications
|
|
NotificationService.to.loadNotifications(refresh: true);
|
|
|
|
// Setup infinite scroll
|
|
_scrollController.addListener(_onScroll);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent * 0.8) {
|
|
NotificationService.to.loadMore();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Notifications'),
|
|
actions: [
|
|
// Mark all as read
|
|
Obx(() {
|
|
final unreadCount = NotificationService.to.unreadCount.value;
|
|
if (unreadCount > 0) {
|
|
return IconButton(
|
|
icon: const Icon(Icons.done_all_rounded),
|
|
tooltip: 'Mark all as read',
|
|
onPressed: () async {
|
|
final success = await NotificationService.to.markAllAsRead();
|
|
if (success) {
|
|
SnackbarUtil.showSuccess('All notifications marked as read');
|
|
}
|
|
},
|
|
);
|
|
}
|
|
return const SizedBox();
|
|
}),
|
|
],
|
|
),
|
|
body: Obx(() {
|
|
final service = NotificationService.to;
|
|
|
|
// Loading state (first load)
|
|
if (service.isLoading.value && service.notifications.isEmpty) {
|
|
return _buildSkeleton();
|
|
}
|
|
|
|
// Empty state
|
|
if (service.notifications.isEmpty && !service.isLoading.value) {
|
|
return EmptyStateWidget(
|
|
type: EmptyStateType.noActivities,
|
|
customTitle: 'No Notifications',
|
|
customMessage: 'You\'re all caught up!\nNotifications will appear here.',
|
|
onActionPressed: () => service.refresh(),
|
|
actionLabel: 'Refresh',
|
|
);
|
|
}
|
|
|
|
// Notifications list
|
|
return RefreshIndicator(
|
|
onRefresh: () => service.refresh(),
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
padding: EdgeInsets.symmetric(vertical: 8.h),
|
|
itemCount: service.notifications.length + (service.hasMore.value ? 1 : 0),
|
|
itemBuilder: (context, index) {
|
|
// Loading indicator at bottom
|
|
if (index == service.notifications.length) {
|
|
return Padding(
|
|
padding: EdgeInsets.all(16.w),
|
|
child: const Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
final notification = service.notifications[index];
|
|
return _buildNotificationCard(notification);
|
|
},
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
Widget _buildNotificationCard(NotificationModel notification) {
|
|
return Dismissible(
|
|
key: Key(notification.id),
|
|
direction: DismissDirection.endToStart,
|
|
background: Container(
|
|
alignment: Alignment.centerRight,
|
|
padding: EdgeInsets.only(right: 20.w),
|
|
color: Colors.red.shade600,
|
|
child: const Icon(Icons.delete, color: Colors.white),
|
|
),
|
|
onDismissed: (direction) async {
|
|
await NotificationService.to.deleteNotification(notification.id);
|
|
SnackbarUtil.showInfo('Notification deleted');
|
|
},
|
|
child: Card(
|
|
margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
|
|
color: notification.isUnread
|
|
? notification.getColor().withOpacity(0.05)
|
|
: null,
|
|
child: InkWell(
|
|
onTap: () => _handleNotificationTap(notification),
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.w),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Icon
|
|
Container(
|
|
padding: EdgeInsets.all(12.w),
|
|
decoration: BoxDecoration(
|
|
color: notification.getColor().withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
child: Icon(
|
|
notification.getIcon(),
|
|
color: notification.getColor(),
|
|
size: 24.w,
|
|
),
|
|
),
|
|
SizedBox(width: 16.w),
|
|
|
|
// Content
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
notification.title,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: notification.isUnread
|
|
? FontWeight.bold
|
|
: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
),
|
|
if (notification.isUnread)
|
|
Container(
|
|
width: 8.w,
|
|
height: 8.h,
|
|
decoration: BoxDecoration(
|
|
color: notification.getColor(),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 6.h),
|
|
Text(
|
|
notification.message,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey.shade600,
|
|
height: 1.4,
|
|
),
|
|
maxLines: 3,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Row(
|
|
children: [
|
|
if (notification.groupName != null) ...[
|
|
Icon(
|
|
Icons.group,
|
|
size: 14.w,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
SizedBox(width: 4.w),
|
|
Text(
|
|
notification.groupName!,
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
],
|
|
Icon(
|
|
Icons.access_time_rounded,
|
|
size: 14.w,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
SizedBox(width: 4.w),
|
|
Text(
|
|
notification.getTimeAgo(),
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleNotificationTap(NotificationModel notification) {
|
|
// Mark as read if unread
|
|
if (notification.isUnread) {
|
|
NotificationService.to.markAsRead(notification.id);
|
|
}
|
|
|
|
// Show full notification details
|
|
_showNotificationDetails(notification);
|
|
}
|
|
|
|
void _showNotificationDetails(NotificationModel notification) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(
|
|
notification.getIcon(),
|
|
color: notification.getColor(),
|
|
size: 24.w,
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Text(
|
|
notification.title,
|
|
style: TextStyle(fontSize: 18.sp),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
notification.message,
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
if (notification.groupName != null)
|
|
_buildDetailRow(
|
|
'Group',
|
|
notification.groupName!,
|
|
Icons.group,
|
|
),
|
|
_buildDetailRow(
|
|
'Date',
|
|
notification.getTimeAgo(),
|
|
Icons.access_time_rounded,
|
|
),
|
|
_buildDetailRow(
|
|
'Channel',
|
|
notification.channel.toUpperCase(),
|
|
Icons.send_rounded,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Close'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow(String label, String value, IconData icon) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: 8.h),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 16.w, color: Colors.grey.shade600),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'$label: ',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSkeleton() {
|
|
return ListView.builder(
|
|
padding: EdgeInsets.symmetric(vertical: 8.h),
|
|
itemCount: 10,
|
|
itemBuilder: (context, index) => const SkeletonListItem(),
|
|
);
|
|
}
|
|
}
|
|
|