second commit
This commit is contained in:
121
lib/presentation/pos/widgets/scan_toast.dart
Normal file
121
lib/presentation/pos/widgets/scan_toast.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user