122 lines
3.6 KiB
Dart
122 lines
3.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../core/theme/app_dimens.dart';
|
|
import '../providers/cart_controller.dart';
|
|
|
|
/// Brief floating confirmation after a scan.
|
|
///
|
|
/// Deliberately not a dialog: the cashier must never have to dismiss anything
|
|
/// between items.
|
|
class ScanToast extends ConsumerStatefulWidget {
|
|
const ScanToast({super.key});
|
|
|
|
@override
|
|
ConsumerState<ScanToast> createState() => _ScanToastState();
|
|
}
|
|
|
|
class _ScanToastState extends ConsumerState<ScanToast> {
|
|
Timer? _timer;
|
|
ScanFeedback? _visible;
|
|
|
|
void _show(ScanFeedback feedback) {
|
|
_timer?.cancel();
|
|
setState(() => _visible = feedback);
|
|
_timer = Timer(
|
|
const Duration(milliseconds: 1600),
|
|
() {
|
|
if (mounted) setState(() => _visible = null);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
ref.listen<ScanFeedback?>(scanFeedbackProvider, (prev, next) {
|
|
if (next != null) _show(next);
|
|
});
|
|
|
|
final feedback = _visible;
|
|
if (feedback == null) return const SizedBox.shrink();
|
|
|
|
final success = feedback.isSuccess;
|
|
final color = success ? AppColors.success : AppColors.danger;
|
|
final product = feedback.product;
|
|
|
|
final message = feedback.message ??
|
|
switch (feedback.outcome) {
|
|
ScanOutcome.added => 'Added to bill',
|
|
ScanOutcome.incremented => 'Quantity updated',
|
|
ScanOutcome.notFound => 'Product not found',
|
|
ScanOutcome.outOfStock => 'Out of stock',
|
|
};
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.xxl),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.xl,
|
|
vertical: AppSpacing.md,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.textPrimary,
|
|
borderRadius: AppRadius.brPill,
|
|
boxShadow: AppColors.shadowLg,
|
|
),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Container(
|
|
width: 26,
|
|
height: 26,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
child: Icon(
|
|
success ? Icons.check_rounded : Icons.priority_high_rounded,
|
|
size: 17,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.md),
|
|
if (product != null) ...[
|
|
Text(product.emoji, style: const TextStyle(fontSize: 17)),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 220),
|
|
child: Text(
|
|
product.name,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 14.5,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Container(width: 1, height: 16, color: Colors.white24),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
],
|
|
Text(
|
|
message,
|
|
style: TextStyle(
|
|
color: Colors.white.withValues(alpha: 0.85),
|
|
fontSize: 13.5,
|
|
),
|
|
),
|
|
]),
|
|
)
|
|
.animate(key: ValueKey(feedback.stamp))
|
|
.fadeIn(duration: 140.ms)
|
|
.slideY(begin: 0.4, end: 0, curve: Curves.easeOutBack)
|
|
.then(delay: 1200.ms)
|
|
.fadeOut(duration: 250.ms);
|
|
}
|
|
}
|