tasq/lib/widgets/reconnect_overlay.dart

81 lines
2.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/realtime_controller.dart';
/// Subtle, non-blocking connection status indicator.
/// Shows in the bottom-right corner when streams are recovering/stale.
/// Displays which channels are reconnecting so the user knows what to expect.
class ReconnectIndicator extends ConsumerWidget {
const ReconnectIndicator({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ctrl = ref.watch(realtimeControllerProvider);
// Hide when not recovering
if (!ctrl.isAnyStreamRecovering) {
return const SizedBox.shrink();
}
// Build a human-readable label for recovering channels.
final channels = ctrl.recoveringChannels;
final label = channels.length <= 2
? channels.map(_humanize).join(', ')
: '${channels.length} channels';
return Positioned(
bottom: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(
Theme.of(context).colorScheme.primary,
),
),
),
const SizedBox(width: 8),
Flexible(
child: Text(
'Reconnecting $label',
style: Theme.of(context).textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
/// Converts a channel name like 'task_assignments' to 'task assignments'.
static String _humanize(String channel) {
// Strip instance suffixes like 'ticket_messages:abc123'
final base = channel.contains(':') ? channel.split(':').first : channel;
return base.replaceAll('_', ' ');
}
}