Replace SnackBars with custom iOS-style top alerts with icons
This commit is contained in:
parent
4acfdb9c4a
commit
67142977c0
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
|
class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
|
||||||
|
|
@ -34,3 +35,154 @@ class FadeSlidePageRoute<T> extends PageRouteBuilder<T> {
|
||||||
reverseTransitionDuration: const Duration(milliseconds: 200),
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showTopAlert(
|
||||||
|
BuildContext context,
|
||||||
|
String message, {
|
||||||
|
bool isError = false,
|
||||||
|
IconData? icon,
|
||||||
|
}) {
|
||||||
|
final overlayState = Overlay.of(context);
|
||||||
|
late OverlayEntry overlayEntry;
|
||||||
|
|
||||||
|
overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) => _TopAlertWidget(
|
||||||
|
message: message,
|
||||||
|
isError: isError,
|
||||||
|
icon: icon,
|
||||||
|
onDismiss: () {
|
||||||
|
overlayEntry.remove();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
overlayState.insert(overlayEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopAlertWidget extends StatefulWidget {
|
||||||
|
final String message;
|
||||||
|
final bool isError;
|
||||||
|
final IconData? icon;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
const _TopAlertWidget({
|
||||||
|
required this.message,
|
||||||
|
required this.isError,
|
||||||
|
this.icon,
|
||||||
|
required this.onDismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TopAlertWidget> createState() => _TopAlertWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopAlertWidgetState extends State<_TopAlertWidget> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _slideAnimation;
|
||||||
|
late Animation<double> _opacityAnimation;
|
||||||
|
Timer? _dismissTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
);
|
||||||
|
|
||||||
|
_opacityAnimation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.forward();
|
||||||
|
|
||||||
|
_dismissTimer = Timer(const Duration(milliseconds: 2800), () {
|
||||||
|
if (mounted) {
|
||||||
|
_controller.reverse().then((_) {
|
||||||
|
widget.onDismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_dismissTimer?.cancel();
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final topPadding = mediaQuery.padding.top + 16.0;
|
||||||
|
|
||||||
|
IconData alertIcon = widget.icon ??
|
||||||
|
(widget.isError ? Icons.error_outline : Icons.check_circle_outline);
|
||||||
|
Color backgroundColor = widget.isError ? const Color(0xFFBA1A20) : const Color(0xFF2E7D32);
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Positioned(
|
||||||
|
top: topPadding * _slideAnimation.value - 80.0 * (1.0 - _slideAnimation.value),
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _opacityAnimation.value,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 450),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.12),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
alertIcon,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.message,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../route_transitions.dart';
|
||||||
|
|
||||||
class ProductItem {
|
class ProductItem {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -160,13 +161,7 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showErrorSnackBar(String message) {
|
void _showErrorSnackBar(String message) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(context, message, isError: true);
|
||||||
SnackBar(
|
|
||||||
content: Text(message),
|
|
||||||
backgroundColor: Colors.red[800],
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double get _subtotal {
|
double get _subtotal {
|
||||||
|
|
@ -944,12 +939,10 @@ class _BillingDashboardScreenState extends State<BillingDashboardScreen> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text('🧾 Thermal Printed: $_invoiceNumber successfully!'),
|
'Thermal Printed: $_invoiceNumber successfully!',
|
||||||
backgroundColor: const Color(0xFF10B981),
|
icon: Icons.print,
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.print, size: 16),
|
icon: const Icon(Icons.print, size: 16),
|
||||||
|
|
|
||||||
|
|
@ -90,29 +90,25 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_uploadedDocs[docName] = file.name;
|
_uploadedDocs[docName] = file.name;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text('✅ Successfully loaded $docName!'),
|
'Successfully loaded $docName!',
|
||||||
backgroundColor: Colors.green,
|
icon: Icons.check_circle_outline,
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text('Failed to pick image: $e'),
|
'Failed to pick image: $e',
|
||||||
behavior: SnackBarBehavior.floating,
|
isError: true,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('Permission denied. Cannot load document image.'),
|
'Permission denied. Cannot load document image.',
|
||||||
behavior: SnackBarBehavior.floating,
|
isError: true,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,11 +294,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_uploadedDocs.remove(docName);
|
_uploadedDocs.remove(docName);
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text('$docName attachment removed.'),
|
'$docName attachment removed.',
|
||||||
behavior: SnackBarBehavior.floating,
|
icon: Icons.delete_outline,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -590,12 +585,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('GSTIN Verified Successfully!'),
|
'GSTIN Verified Successfully!',
|
||||||
backgroundColor: Colors.green,
|
icon: Icons.verified_outlined,
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -628,12 +621,10 @@ class _BusinessSetupScreenState extends State<BusinessSetupScreen> {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('📍 Location detected: Sector 62, Noida, UP'),
|
'Location detected: Sector 62, Noida, UP',
|
||||||
backgroundColor: Colors.green,
|
icon: Icons.my_location_outlined,
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
if (status.isGranted || kIsWeb) {
|
if (status.isGranted || kIsWeb) {
|
||||||
_showContactPicker();
|
_showContactPicker();
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('Contacts permission was denied. Please enter the number manually or enable it in Settings.'),
|
'Contacts permission was denied. Please enter the number manually or enable it in Settings.',
|
||||||
behavior: SnackBarBehavior.floating,
|
isError: true,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -402,12 +401,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isValid = true;
|
_isValid = true;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('⚡ Phone verified automatically with Truecaller! Joining PozoApp...'),
|
'Phone verified automatically with Truecaller! Joining PozoApp...',
|
||||||
backgroundColor: Color(0xFF005FAF),
|
icon: Icons.verified_user_outlined,
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
@ -610,11 +607,10 @@ class _MobileLoginScreenState extends State<MobileLoginScreen> {
|
||||||
SizedBox(height: gap / 2),
|
SizedBox(height: gap / 2),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('💬 Connecting with Pozo Support... Please wait.'),
|
'Connecting with Pozo Support... Please wait.',
|
||||||
behavior: SnackBarBehavior.floating,
|
icon: Icons.support_agent_outlined,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
|
|
|
||||||
|
|
@ -293,13 +293,10 @@ class SetupCompleteScreen extends StatelessWidget {
|
||||||
// Primary Launcher CTA
|
// Primary Launcher CTA
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showTopAlert(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('⚡ Launching Register Dashboard... Enjoy billing!'),
|
'Launching Register Dashboard... Enjoy billing!',
|
||||||
behavior: SnackBarBehavior.floating,
|
icon: Icons.rocket_launch_outlined,
|
||||||
backgroundColor: Colors.green,
|
|
||||||
duration: Duration(milliseconds: 1500),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue