145 lines
3.8 KiB
Dart
145 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../providers/connectivity_provider.dart';
|
|
|
|
/// Two-state "this item is not yet synced" indicator.
|
|
///
|
|
/// Renders nothing when [isPending] is false. Otherwise shows:
|
|
/// - **Amber** chip with a static sync icon when the device is offline
|
|
/// ("Pending sync" — queued, will sync when network returns).
|
|
/// - **Blue** chip with a rotating sync icon when the device is online
|
|
/// ("Syncing…" — replay-in-flight or Brick queue draining).
|
|
///
|
|
/// Use [compact] for dense list rows where only the icon is needed.
|
|
class SyncPendingBadge extends ConsumerWidget {
|
|
/// Whether the entity associated with this badge is still queued for sync.
|
|
/// Caller is responsible for deriving this from the appropriate state
|
|
/// provider (e.g., `offlinePendingTasksProvider.any((t) => t.id == taskId)`).
|
|
final bool isPending;
|
|
|
|
/// Compact (icon-only) variant for dense list rows.
|
|
final bool compact;
|
|
|
|
/// Optional override label — defaults to "Pending sync" (offline) or
|
|
/// "Syncing…" (online).
|
|
final String? label;
|
|
|
|
const SyncPendingBadge({
|
|
super.key,
|
|
required this.isPending,
|
|
this.compact = false,
|
|
this.label,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
if (!isPending) return const SizedBox.shrink();
|
|
|
|
final isOnline = ref.watch(isOnlineProvider);
|
|
final scheme = Theme.of(context).colorScheme;
|
|
|
|
final Color bg;
|
|
final Color fg;
|
|
final IconData icon;
|
|
final String text;
|
|
final bool spin;
|
|
|
|
if (isOnline) {
|
|
// Replay-in-flight: blue, animated.
|
|
bg = scheme.primaryContainer;
|
|
fg = scheme.onPrimaryContainer;
|
|
icon = Icons.sync;
|
|
text = label ?? 'Syncing…';
|
|
spin = true;
|
|
} else {
|
|
// Queued offline: amber, static.
|
|
bg = Colors.amber.shade100;
|
|
fg = Colors.amber.shade900;
|
|
icon = Icons.sync;
|
|
text = label ?? 'Pending sync';
|
|
spin = false;
|
|
}
|
|
|
|
final iconWidget = spin
|
|
? _RotatingIcon(icon: icon, color: fg, size: compact ? 12 : 14)
|
|
: Icon(icon, color: fg, size: compact ? 12 : 14);
|
|
|
|
if (compact) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: bg,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: iconWidget,
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: bg,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
iconWidget,
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
text,
|
|
style: TextStyle(
|
|
color: fg,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RotatingIcon extends StatefulWidget {
|
|
final IconData icon;
|
|
final Color color;
|
|
final double size;
|
|
const _RotatingIcon({
|
|
required this.icon,
|
|
required this.color,
|
|
required this.size,
|
|
});
|
|
|
|
@override
|
|
State<_RotatingIcon> createState() => _RotatingIconState();
|
|
}
|
|
|
|
class _RotatingIconState extends State<_RotatingIcon>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _ctrl;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ctrl =
|
|
AnimationController(vsync: this, duration: const Duration(seconds: 2))
|
|
..repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_ctrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RotationTransition(
|
|
turns: _ctrl,
|
|
child: Icon(widget.icon, color: widget.color, size: widget.size),
|
|
);
|
|
}
|
|
}
|