tasq/lib/widgets/announcement_banner.dart

129 lines
4.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/announcements_provider.dart';
/// A persistent, globally visible banner that appears at the top of every
/// authenticated screen when one or more [Announcement]s have an active banner.
///
/// Mirrors the pattern used by [PassSlipCountdownBanner] and
/// [ShiftCountdownBanner]. Registered in [_ShellBackground] so it wraps all
/// shell-route screens.
///
/// Tapping the banner navigates to `/announcements`. The X button dismisses
/// the leading announcement for the current session; if more remain they
/// continue to be shown.
class AnnouncementBanner extends ConsumerStatefulWidget {
const AnnouncementBanner({required this.child, super.key});
final Widget child;
@override
ConsumerState<AnnouncementBanner> createState() =>
_AnnouncementBannerState();
}
class _AnnouncementBannerState extends ConsumerState<AnnouncementBanner> {
/// IDs of banners the user has dismissed for this session.
final Set<String> _sessionDismissed = {};
@override
Widget build(BuildContext context) {
final visible = ref
.watch(activeBannerAnnouncementsProvider)
.where((a) => !_sessionDismissed.contains(a.id))
.toList();
if (visible.isEmpty) return widget.child;
final first = visible.first;
final extra = visible.length - 1;
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return Column(
children: [
Material(
child: InkWell(
onTap: () => context.go('/announcements'),
child: Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(color: cs.primaryContainer),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.campaign_rounded,
size: 22,
color: cs.onPrimaryContainer,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
extra > 0
? 'Announcement · +$extra more'
: 'Announcement',
style: tt.labelSmall?.copyWith(
color:
cs.onPrimaryContainer.withValues(alpha: 0.75),
letterSpacing: 0.3,
),
),
const SizedBox(height: 1),
Text(
first.title,
style: tt.bodyMedium?.copyWith(
color: cs.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Text(
'Tap to view',
style: tt.bodySmall?.copyWith(
color: cs.onPrimaryContainer.withValues(alpha: 0.7),
),
),
Icon(
Icons.chevron_right,
size: 18,
color: cs.onPrimaryContainer.withValues(alpha: 0.7),
),
const SizedBox(width: 4),
// Dismiss for session
InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () =>
setState(() => _sessionDismissed.add(first.id)),
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Icons.close,
size: 18,
color: cs.onPrimaryContainer,
),
),
),
],
),
),
),
),
Expanded(child: widget.child),
],
);
}
}