Files
tasq/lib/utils/snackbar.dart
T
2026-04-27 06:20:39 +08:00

274 lines
8.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart' show PostgrestException;
/// A global messenger key used to show snackbars from contexts without a
/// Scaffold (e.g. dialogs, background callbacks, or tests).
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
/// Helper wrappers around `awesome_snackbar_content` so that callers
/// can show snackbars with a consistent look and only specify a message and
/// semantic type.
/// Enumeration used by callers; the mapping to the packages
/// [ContentType] is internal.
enum SnackType { success, info, warning, error }
ContentType _mapSnackType(SnackType t) {
switch (t) {
case SnackType.success:
return ContentType.success;
case SnackType.info:
return ContentType.help;
case SnackType.warning:
return ContentType.warning;
case SnackType.error:
return ContentType.failure;
}
}
/// Core function used by all of the convenience helpers below.
void showAwesomeSnackBar(
BuildContext context, {
required String title,
required String message,
required SnackType snackType,
bool retry = true,
}) {
// Add margin and padding so even very short messages feel substantial.
final snackBar = SnackBar(
elevation: 0,
behavior: SnackBarBehavior.floating,
// give floating snackbar some breathing room
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: AwesomeSnackbarContent(
title: title,
message: message,
contentType: _mapSnackType(snackType),
),
),
);
// Prefer a local ScaffoldMessenger when available, but prefer the global
// `scaffoldMessengerKey` if it's already ready. This increases reliability
// when callers are inside dialogs or other overlays.
final globalMessenger = scaffoldMessengerKey.currentState;
final localMessenger = ScaffoldMessenger.maybeOf(context);
final messenger = localMessenger ?? globalMessenger;
if (messenger == null) {
if (kDebugMode) {
debugPrint(
'showAwesomeSnackBar: no messenger available; scheduling fallback',
);
}
// If the scaffold messenger is not available yet, schedule a post-frame
// callback that uses the global messenger (if it becomes available).
if (retry) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final gm = scaffoldMessengerKey.currentState;
if (gm != null) {
try {
gm
..hideCurrentSnackBar()
..showSnackBar(snackBar);
if (kDebugMode) {
debugPrint(
'showAwesomeSnackBar: shown via global messenger (post-frame)',
);
}
} catch (e) {
if (kDebugMode) {
debugPrint('showAwesomeSnackBar: post-frame show failed: $e');
}
}
} else if (kDebugMode) {
debugPrint(
'showAwesomeSnackBar: global messenger still null after frame',
);
}
});
}
return;
}
try {
messenger
..hideCurrentSnackBar()
..showSnackBar(snackBar);
if (kDebugMode) {
debugPrint(
'showAwesomeSnackBar: shown via ${identical(messenger, globalMessenger) ? 'global' : 'local'} messenger',
);
}
} catch (e) {
if (kDebugMode) debugPrint('showAwesomeSnackBar: show failed: $e');
}
}
void showSuccessSnackBar(BuildContext context, String message) {
showAwesomeSnackBar(
context,
title: 'Success',
message: message,
snackType: SnackType.success,
);
}
void showErrorSnackBar(BuildContext context, String message) {
showAwesomeSnackBar(
context,
title: 'Error',
message: message,
snackType: SnackType.error,
);
}
void showInfoSnackBar(BuildContext context, String message) {
showAwesomeSnackBar(
context,
title: 'Info',
message: message,
snackType: SnackType.info,
);
}
void showWarningSnackBar(BuildContext context, String message) {
showAwesomeSnackBar(
context,
title: 'Warning',
message: message,
snackType: SnackType.warning,
);
}
/// Global helpers that use the app-level `scaffoldMessengerKey` directly.
/// Use these from places where a valid `BuildContext` with a Scaffold may
/// not be available (e.g. dialog builders or background callbacks).
void showAwesomeSnackBarGlobal({
required String title,
required String message,
required SnackType snackType,
}) {
final gm = scaffoldMessengerKey.currentState;
if (gm == null) {
if (kDebugMode) {
debugPrint('showAwesomeSnackBarGlobal: global messenger null');
}
return;
}
final snackBar = SnackBar(
elevation: 0,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: AwesomeSnackbarContent(
title: title,
message: message,
contentType: _mapSnackType(snackType),
),
),
);
try {
gm
..hideCurrentSnackBar()
..showSnackBar(snackBar);
if (kDebugMode) {
debugPrint('showAwesomeSnackBarGlobal: shown');
}
} catch (e) {
if (kDebugMode) {
debugPrint('showAwesomeSnackBarGlobal: show failed: $e');
}
}
}
void showSuccessSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
title: 'Success',
message: message,
snackType: SnackType.success,
);
void showErrorSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
title: 'Error',
message: message,
snackType: SnackType.error,
);
void showInfoSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
title: 'Info',
message: message,
snackType: SnackType.info,
);
void showWarningSnackBarGlobal(String message) => showAwesomeSnackBarGlobal(
title: 'Warning',
message: message,
snackType: SnackType.warning,
);
/// Returns true when [error] is a network-connectivity failure (socket closed,
/// no route to host, failed DNS lookup, etc.).
///
/// When this is true in a Brick-enabled app, the write was already queued in
/// the offline SQLite store and will sync automatically when the device
/// reconnects — so the UI should treat it as a success rather than an error.
bool isOfflineSaveError(Object error) {
if (error is PostgrestException) {
// Null code = no HTTP response received at all (pure network failure).
if (error.code == null) return true;
// Brick's RestOfflineQueueClient catches network errors and returns a
// synthetic StreamedResponse(501, body: 'unknown internal error') rather
// than rethrowing. PostgREST then parses this as
// PostgrestException(code: '501', message: 'unknown internal error').
// This is the primary offline indicator when Supabase is used with Brick.
if (error.code == '501' && error.message == 'unknown internal error') {
return true;
}
// Fallback: Supabase may wrap a SocketException in a PostgrestException
// message field before it reaches Brick's queue (rare, but defensive).
final pmsg = error.message.toLowerCase();
if (pmsg.contains('socketexception') ||
pmsg.contains('failed host lookup') ||
pmsg.contains('connection refused') ||
pmsg.contains('network is unreachable') ||
pmsg.contains('no route to host') ||
pmsg.contains('connection timed out') ||
pmsg.contains('failed to connect')) {
return true;
}
}
// Non-PostgrestException network errors (SocketException, ClientException)
// that propagate before Brick's HTTP interceptor can catch them.
final msg = error.toString().toLowerCase();
return msg.contains('socketexception') ||
msg.contains('socket') && msg.contains('exception') ||
msg.contains('connection refused') ||
msg.contains('network is unreachable') ||
msg.contains('no route to host') ||
msg.contains('failed host lookup') ||
msg.contains('connection reset') ||
msg.contains('broken pipe') ||
msg.contains('connection timed out') ||
msg.contains('clientexception') && msg.contains('network') ||
msg.contains('failed to connect') ||
msg.contains('errno = 111') || // ECONNREFUSED
msg.contains('errno = 101'); // ENETUNREACH
}