refactor: cleanup unsued files
This commit is contained in:
@@ -1,435 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/theme_extensions.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
/// Enhanced keyboard handling utilities for better UX
|
||||
class KeyboardUtils {
|
||||
KeyboardUtils._();
|
||||
|
||||
/// Dismiss keyboard with haptic feedback
|
||||
static void dismissKeyboard(BuildContext context) {
|
||||
final currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
// Add haptic feedback on iOS
|
||||
if (Platform.isIOS) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Force dismiss keyboard immediately
|
||||
static void forceDismissKeyboard() {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
SystemChannels.textInput.invokeMethod('TextInput.hide');
|
||||
}
|
||||
|
||||
/// Check if keyboard is currently visible
|
||||
static bool isKeyboardVisible(BuildContext context) {
|
||||
return MediaQuery.of(context).viewInsets.bottom > 0;
|
||||
}
|
||||
|
||||
/// Get keyboard height
|
||||
static double getKeyboardHeight(BuildContext context) {
|
||||
return MediaQuery.of(context).viewInsets.bottom;
|
||||
}
|
||||
|
||||
/// Move focus to next field
|
||||
static void nextFocus(BuildContext context) {
|
||||
FocusScope.of(context).nextFocus();
|
||||
}
|
||||
|
||||
/// Move focus to previous field
|
||||
static void previousFocus(BuildContext context) {
|
||||
FocusScope.of(context).previousFocus();
|
||||
}
|
||||
|
||||
/// Request focus for a specific node
|
||||
static void requestFocus(BuildContext context, FocusNode focusNode) {
|
||||
FocusScope.of(context).requestFocus(focusNode);
|
||||
}
|
||||
|
||||
/// Create a tap detector that dismisses keyboard when tapping outside text fields
|
||||
static Widget dismissKeyboardOnTap({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: () => dismissKeyboard(context),
|
||||
// Let children handle taps first (e.g., TextField gains focus)
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget that automatically adjusts for keyboard visibility
|
||||
class KeyboardAware extends StatefulWidget {
|
||||
final Widget child;
|
||||
final EdgeInsets? padding;
|
||||
final bool maintainBottomViewPadding;
|
||||
final Duration animationDuration;
|
||||
final Curve animationCurve;
|
||||
|
||||
const KeyboardAware({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.maintainBottomViewPadding = true,
|
||||
this.animationDuration = const Duration(milliseconds: 250),
|
||||
this.animationCurve = Curves.easeInOut,
|
||||
});
|
||||
|
||||
@override
|
||||
State<KeyboardAware> createState() => _KeyboardAwareState();
|
||||
}
|
||||
|
||||
class _KeyboardAwareState extends State<KeyboardAware>
|
||||
with WidgetsBindingObserver {
|
||||
double _keyboardHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
super.didChangeMetrics();
|
||||
final newKeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
if (newKeyboardHeight != _keyboardHeight) {
|
||||
setState(() {
|
||||
_keyboardHeight = newKeyboardHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedPadding(
|
||||
duration: widget.animationDuration,
|
||||
curve: widget.animationCurve,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: widget.maintainBottomViewPadding ? _keyboardHeight : 0,
|
||||
).add(widget.padding ?? EdgeInsets.zero),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced text field with better keyboard handling
|
||||
class EnhancedTextField extends StatefulWidget {
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final String? hintText;
|
||||
final String? labelText;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final VoidCallback? onTap;
|
||||
final bool obscureText;
|
||||
final bool enabled;
|
||||
final int? maxLines;
|
||||
final int? minLines;
|
||||
final EdgeInsets? contentPadding;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final bool autofocus;
|
||||
final bool dismissKeyboardOnSubmit;
|
||||
|
||||
const EnhancedTextField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.hintText,
|
||||
this.labelText,
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.onTap,
|
||||
this.obscureText = false,
|
||||
this.enabled = true,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.contentPadding,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.autofocus = false,
|
||||
this.dismissKeyboardOnSubmit = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EnhancedTextField> createState() => _EnhancedTextFieldState();
|
||||
}
|
||||
|
||||
class _EnhancedTextFieldState extends State<EnhancedTextField> {
|
||||
late FocusNode _focusNode;
|
||||
bool _hasFocus = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_focusNode.addListener(_onFocusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_onFocusChanged);
|
||||
if (widget.focusNode == null) {
|
||||
_focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
setState(() {
|
||||
_hasFocus = _focusNode.hasFocus;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleSubmitted(String value) {
|
||||
widget.onSubmitted?.call(value);
|
||||
|
||||
if (widget.dismissKeyboardOnSubmit) {
|
||||
KeyboardUtils.dismissKeyboard(context);
|
||||
}
|
||||
|
||||
// Add haptic feedback
|
||||
if (Platform.isIOS) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _hasFocus
|
||||
? context.conduitTheme.buttonPrimary
|
||||
: context.conduitTheme.inputBorder,
|
||||
width: _hasFocus ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
obscureText: widget.obscureText,
|
||||
enabled: widget.enabled,
|
||||
autofocus: widget.autofocus,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontSize: AppTypography.bodyLarge,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
labelText: widget.labelText,
|
||||
hintStyle: TextStyle(color: context.conduitTheme.inputPlaceholder),
|
||||
labelStyle: TextStyle(
|
||||
color: _hasFocus
|
||||
? context.conduitTheme.buttonPrimary
|
||||
: context.conduitTheme.textSecondary,
|
||||
),
|
||||
prefixIcon: widget.prefixIcon,
|
||||
suffixIcon: widget.suffixIcon,
|
||||
contentPadding:
|
||||
widget.contentPadding ??
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
vertical: Spacing.sm,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
onChanged: widget.onChanged,
|
||||
onSubmitted: _handleSubmitted,
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Smart keyboard handler that manages multiple text fields
|
||||
class SmartKeyboardHandler extends StatefulWidget {
|
||||
final List<FocusNode> focusNodes;
|
||||
final Widget child;
|
||||
final VoidCallback? onDone;
|
||||
|
||||
const SmartKeyboardHandler({
|
||||
super.key,
|
||||
required this.focusNodes,
|
||||
required this.child,
|
||||
this.onDone,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SmartKeyboardHandler> createState() => _SmartKeyboardHandlerState();
|
||||
}
|
||||
|
||||
class _SmartKeyboardHandlerState extends State<SmartKeyboardHandler> {
|
||||
int _currentIndex = -1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupFocusListeners();
|
||||
}
|
||||
|
||||
void _setupFocusListeners() {
|
||||
for (int i = 0; i < widget.focusNodes.length; i++) {
|
||||
widget.focusNodes[i].addListener(() => _onFocusChanged(i));
|
||||
}
|
||||
}
|
||||
|
||||
void _onFocusChanged(int index) {
|
||||
if (widget.focusNodes[index].hasFocus) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _moveToNext() {
|
||||
if (_currentIndex < widget.focusNodes.length - 1) {
|
||||
KeyboardUtils.requestFocus(context, widget.focusNodes[_currentIndex + 1]);
|
||||
} else {
|
||||
KeyboardUtils.dismissKeyboard(context);
|
||||
widget.onDone?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void _moveToPrevious() {
|
||||
if (_currentIndex > 0) {
|
||||
KeyboardUtils.requestFocus(context, widget.focusNodes[_currentIndex - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.tab) {
|
||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||
_moveToPrevious();
|
||||
} else {
|
||||
_moveToNext();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final focusNode in widget.focusNodes) {
|
||||
focusNode.removeListener(() {});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard-aware scroll view that adjusts scroll position
|
||||
class KeyboardAwareScrollView extends StatefulWidget {
|
||||
final ScrollController? controller;
|
||||
final Widget child;
|
||||
final EdgeInsets? padding;
|
||||
final bool reverse;
|
||||
final Duration animationDuration;
|
||||
|
||||
const KeyboardAwareScrollView({
|
||||
super.key,
|
||||
this.controller,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.reverse = false,
|
||||
this.animationDuration = const Duration(milliseconds: 300),
|
||||
});
|
||||
|
||||
@override
|
||||
State<KeyboardAwareScrollView> createState() =>
|
||||
_KeyboardAwareScrollViewState();
|
||||
}
|
||||
|
||||
class _KeyboardAwareScrollViewState extends State<KeyboardAwareScrollView>
|
||||
with WidgetsBindingObserver {
|
||||
late ScrollController _scrollController;
|
||||
FocusNode? _currentFocus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = widget.controller ?? ScrollController();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
if (widget.controller == null) {
|
||||
_scrollController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
super.didChangeMetrics();
|
||||
_adjustScrollPosition();
|
||||
}
|
||||
|
||||
void _adjustScrollPosition() {
|
||||
final focus = FocusManager.instance.primaryFocus;
|
||||
if (focus != null && focus != _currentFocus) {
|
||||
_currentFocus = focus;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
if (keyboardHeight > 0) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.offset + keyboardHeight / 2,
|
||||
duration: widget.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
reverse: widget.reverse,
|
||||
padding: widget.padding,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../theme/theme_extensions.dart';
|
||||
import 'improved_loading_states.dart';
|
||||
|
||||
/// Cached network image widget with progressive loading and error handling
|
||||
class CachedImage extends StatelessWidget {
|
||||
final String imageUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
final Widget? placeholder;
|
||||
final Widget? errorWidget;
|
||||
final Duration fadeInDuration;
|
||||
final Duration fadeOutDuration;
|
||||
final bool enableMemoryCache;
|
||||
final int? maxWidthDiskCache;
|
||||
final int? maxHeightDiskCache;
|
||||
|
||||
const CachedImage({
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholder,
|
||||
this.errorWidget,
|
||||
this.fadeInDuration = const Duration(milliseconds: 300),
|
||||
this.fadeOutDuration = const Duration(milliseconds: 100),
|
||||
this.enableMemoryCache = true,
|
||||
this.maxWidthDiskCache,
|
||||
this.maxHeightDiskCache,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
fadeInDuration: fadeInDuration,
|
||||
fadeOutDuration: fadeOutDuration,
|
||||
placeholder: placeholder != null
|
||||
? (context, url) => placeholder!
|
||||
: _buildDefaultPlaceholder,
|
||||
errorWidget: errorWidget != null
|
||||
? (context, url, error) => errorWidget!
|
||||
: _buildDefaultErrorWidget,
|
||||
memCacheWidth: enableMemoryCache ? width?.toInt() : null,
|
||||
memCacheHeight: enableMemoryCache ? height?.toInt() : null,
|
||||
maxWidthDiskCache: maxWidthDiskCache,
|
||||
maxHeightDiskCache: maxHeightDiskCache,
|
||||
useOldImageOnUrlChange: true,
|
||||
filterQuality: FilterQuality.medium,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDefaultPlaceholder(BuildContext context, String url) {
|
||||
return ShimmerLoader(
|
||||
width: width ?? double.infinity,
|
||||
height: height ?? 200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDefaultErrorWidget(
|
||||
BuildContext context,
|
||||
String url,
|
||||
dynamic error,
|
||||
) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: context.conduitTheme.shimmerBase,
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: (width != null && height != null)
|
||||
? (width! < height! ? width! * 0.5 : height! * 0.5)
|
||||
: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached circular avatar with progressive loading
|
||||
class CachedAvatar extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final String fallbackText;
|
||||
final double radius;
|
||||
final Color? backgroundColor;
|
||||
final Color? textColor;
|
||||
|
||||
const CachedAvatar({
|
||||
super.key,
|
||||
this.imageUrl,
|
||||
required this.fallbackText,
|
||||
this.radius = 20,
|
||||
this.backgroundColor,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor:
|
||||
backgroundColor ?? context.conduitTheme.surfaceBackground,
|
||||
child: imageUrl != null
|
||||
? ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl!,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: textColor ?? context.conduitTheme.iconSecondary,
|
||||
),
|
||||
errorWidget: (context, url, error) => Text(
|
||||
fallbackText.isNotEmpty ? fallbackText[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: textColor ?? context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: radius * 0.6,
|
||||
),
|
||||
),
|
||||
memCacheWidth: (radius * 2).toInt(),
|
||||
memCacheHeight: (radius * 2).toInt(),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
fallbackText.isNotEmpty ? fallbackText[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: textColor ?? context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: radius * 0.6,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import '../theme/theme_extensions.dart';
|
||||
|
||||
import '../services/brand_service.dart';
|
||||
|
||||
/// Enhanced empty state widgets with illustrations and actions
|
||||
class ConduitEmptyState extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final IconData? icon;
|
||||
final Widget? illustration;
|
||||
final List<EmptyStateAction>? actions;
|
||||
final bool isLoading;
|
||||
|
||||
const ConduitEmptyState({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.illustration,
|
||||
this.actions,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final conduitTheme = context.conduitTheme;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Spacing.xl),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Illustration or icon
|
||||
if (illustration != null)
|
||||
illustration!
|
||||
else if (icon != null)
|
||||
Container(
|
||||
width: IconSize.xxl * 2.5, // 120px equivalent
|
||||
height: IconSize.xxl * 2.5, // 120px equivalent
|
||||
decoration: BoxDecoration(
|
||||
color: conduitTheme.cardBackground,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: conduitTheme.cardBorder, width: 2),
|
||||
),
|
||||
child: Icon(
|
||||
icon!,
|
||||
size: IconSize.xxl,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
),
|
||||
)
|
||||
else
|
||||
// Default to brand icon when no specific icon or illustration provided
|
||||
BrandService.createBrandEmptyStateIcon(
|
||||
size: IconSize.xxl * 2.5, // 120px equivalent
|
||||
showBackground: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: Spacing.xl),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: conduitTheme.headingMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
// Subtitle
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: Spacing.xs),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: conduitTheme.bodyMedium?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
// Actions
|
||||
if (actions != null && actions!.isNotEmpty) ...[
|
||||
const SizedBox(height: Spacing.xl),
|
||||
...actions!.map(
|
||||
(action) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: Spacing.xs),
|
||||
child: _buildActionButton(context, action),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
).animate().fadeIn(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(BuildContext context, EmptyStateAction action) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: action.onPressed,
|
||||
style: action.isPrimary
|
||||
? FilledButton.styleFrom(
|
||||
backgroundColor: context.conduitTheme.buttonPrimary,
|
||||
foregroundColor: context.conduitTheme.buttonPrimaryText,
|
||||
)
|
||||
: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: context.conduitTheme.textSecondary,
|
||||
side: BorderSide(
|
||||
color: context.conduitTheme.dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (action.icon != null) ...[
|
||||
Icon(action.icon, size: IconSize.md),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
],
|
||||
Text(action.label),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Action for empty states
|
||||
class EmptyStateAction {
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final IconData? icon;
|
||||
final bool isPrimary;
|
||||
|
||||
const EmptyStateAction({
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.icon,
|
||||
this.isPrimary = true,
|
||||
});
|
||||
}
|
||||
|
||||
/// Chat-specific empty state
|
||||
class ChatEmptyState extends StatelessWidget {
|
||||
final VoidCallback? onStartChat;
|
||||
|
||||
const ChatEmptyState({super.key, this.onStartChat});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitEmptyState(
|
||||
title: 'Start a conversation',
|
||||
subtitle:
|
||||
'Ask me anything! I\'m here to help with questions, creative tasks, analysis, and more.',
|
||||
// Remove custom illustration to use default brand icon
|
||||
icon: BrandService.primaryIcon,
|
||||
actions: onStartChat != null
|
||||
? [
|
||||
EmptyStateAction(
|
||||
label: 'Start chatting',
|
||||
icon: BrandService.primaryIcon,
|
||||
onPressed: onStartChat!,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Files empty state
|
||||
class FilesEmptyState extends StatelessWidget {
|
||||
final VoidCallback? onUploadFile;
|
||||
|
||||
const FilesEmptyState({super.key, this.onUploadFile});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitEmptyState(
|
||||
title: 'No files yet',
|
||||
subtitle:
|
||||
'Upload documents, images, or other files to get started with your knowledge base.',
|
||||
illustration: Builder(
|
||||
builder: (context) => _buildFilesIllustration(context),
|
||||
),
|
||||
actions: onUploadFile != null
|
||||
? [
|
||||
EmptyStateAction(
|
||||
label: 'Upload files',
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.doc_on_doc
|
||||
: Icons.upload_file,
|
||||
onPressed: onUploadFile!,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilesIllustration(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background circle
|
||||
Container(
|
||||
width: IconSize.xxl * 2.5, // 120px equivalent
|
||||
height: IconSize.xxl * 2.5, // 120px equivalent
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.info.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
// File stack
|
||||
...List.generate(3, (index) {
|
||||
return Positioned(
|
||||
top: 30 + (index * 8.0),
|
||||
left: 30 + (index * 4.0),
|
||||
child:
|
||||
Container(
|
||||
width: TouchTarget.minimum,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: [
|
||||
context.conduitTheme.info,
|
||||
context.conduitTheme.success,
|
||||
context.conduitTheme.warning,
|
||||
][index],
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.xs,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
[Icons.description, Icons.image, Icons.folder][index],
|
||||
color: context.conduitTheme.textInverse,
|
||||
size: IconSize.md,
|
||||
),
|
||||
)
|
||||
.animate(delay: Duration(milliseconds: index * 200))
|
||||
.fadeIn()
|
||||
.slideY(begin: 0.3, end: 0),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tools empty state
|
||||
class ToolsEmptyState extends StatelessWidget {
|
||||
final VoidCallback? onExploreTools;
|
||||
|
||||
const ToolsEmptyState({super.key, this.onExploreTools});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitEmptyState(
|
||||
title: 'Powerful tools await',
|
||||
subtitle: 'Discover tools to enhance your productivity and creativity.',
|
||||
illustration: Builder(
|
||||
builder: (context) => _buildToolsIllustration(context),
|
||||
),
|
||||
actions: onExploreTools != null
|
||||
? [
|
||||
EmptyStateAction(
|
||||
label: 'Explore tools',
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.wand_stars
|
||||
: Icons.auto_awesome,
|
||||
onPressed: onExploreTools!,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolsIllustration(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background circle
|
||||
Container(
|
||||
width: IconSize.xxl * 2.5, // 120px equivalent
|
||||
height: IconSize.xxl * 2.5, // 120px equivalent
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
// Tools arrangement
|
||||
...List.generate(6, (index) {
|
||||
final angle = (index * 60) * (3.14159 / 180);
|
||||
final radius = 35.0;
|
||||
return Positioned(
|
||||
top: 60 + (radius * -cos(angle)) - 15,
|
||||
left: 60 + (radius * sin(angle)) - 15,
|
||||
child:
|
||||
Container(
|
||||
width: Spacing.xl - Spacing.xxs, // 30px equivalent
|
||||
height: Spacing.xl - Spacing.xxs, // 30px equivalent
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
[
|
||||
Icons.palette,
|
||||
Icons.calculate,
|
||||
Icons.code,
|
||||
Icons.translate,
|
||||
Icons.music_note,
|
||||
Icons.analytics,
|
||||
][index],
|
||||
color: context.conduitTheme.textInverse,
|
||||
size: IconSize.sm,
|
||||
),
|
||||
)
|
||||
.animate(delay: Duration(milliseconds: index * 100))
|
||||
.fadeIn()
|
||||
.scale(
|
||||
begin: const Offset(0.5, 0.5),
|
||||
end: const Offset(1.0, 1.0),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Search results empty state
|
||||
class SearchEmptyState extends StatelessWidget {
|
||||
final String query;
|
||||
final VoidCallback? onClearSearch;
|
||||
|
||||
const SearchEmptyState({super.key, required this.query, this.onClearSearch});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitEmptyState(
|
||||
title: 'No results found',
|
||||
subtitle: 'No results for "$query". Try adjusting your search terms.',
|
||||
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search_off,
|
||||
actions: onClearSearch != null
|
||||
? [
|
||||
EmptyStateAction(
|
||||
label: 'Clear search',
|
||||
icon: Platform.isIOS ? CupertinoIcons.clear : Icons.clear,
|
||||
onPressed: onClearSearch!,
|
||||
isPrimary: false,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection error empty state
|
||||
class ConnectionEmptyState extends StatelessWidget {
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const ConnectionEmptyState({super.key, this.onRetry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitEmptyState(
|
||||
title: 'Connection problem',
|
||||
subtitle:
|
||||
'Unable to load content. Please check your connection and try again.',
|
||||
icon: Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off,
|
||||
actions: onRetry != null
|
||||
? [
|
||||
EmptyStateAction(
|
||||
label: 'Try again',
|
||||
icon: Platform.isIOS ? CupertinoIcons.refresh : Icons.refresh,
|
||||
onPressed: onRetry!,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic empty state with custom illustration
|
||||
class CustomEmptyState extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget illustration;
|
||||
final List<EmptyStateAction>? actions;
|
||||
|
||||
const CustomEmptyState({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.illustration,
|
||||
this.actions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitEmptyState(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
illustration: illustration,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get cosine
|
||||
double cos(double radians) {
|
||||
// Simple cosine approximation for illustration positioning
|
||||
if (radians == 0) return 1.0;
|
||||
if (radians == 1.5708) return 0.0; // π/2
|
||||
if (radians == 3.14159) return -1.0; // π
|
||||
if (radians == 4.71239) return 0.0; // 3π/2
|
||||
|
||||
// Taylor series approximation for other values
|
||||
double x2 = radians * radians;
|
||||
return 1 - x2 / 2 + x2 * x2 / 24 - x2 * x2 * x2 / 720;
|
||||
}
|
||||
|
||||
// Helper function to get sine
|
||||
double sin(double radians) {
|
||||
// Simple sine approximation for illustration positioning
|
||||
if (radians == 0) return 0.0;
|
||||
if (radians == 1.5708) return 1.0; // π/2
|
||||
if (radians == 3.14159) return 0.0; // π
|
||||
if (radians == 4.71239) return -1.0; // 3π/2
|
||||
|
||||
// Taylor series approximation for other values
|
||||
double x2 = radians * radians;
|
||||
return radians - radians * x2 / 6 + radians * x2 * x2 / 120;
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/theme_extensions.dart';
|
||||
import 'conduit_components.dart';
|
||||
|
||||
/// Enhanced error widget with production-grade design and better hierarchy
|
||||
class ConduitErrorWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
final IconData? icon;
|
||||
final bool isCompact;
|
||||
|
||||
const ConduitErrorWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
this.icon,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.cardPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.errorBackground.withValues(
|
||||
alpha: Alpha.badgeBackground,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.error.withValues(alpha: Alpha.subtle),
|
||||
width: BorderWidth.standard,
|
||||
),
|
||||
boxShadow: ConduitShadows.card,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.error_outline,
|
||||
size: isCompact ? IconSize.large : IconSize.xl,
|
||||
color: context.conduitTheme.error,
|
||||
),
|
||||
SizedBox(height: isCompact ? Spacing.sm : Spacing.md),
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
|
||||
Text(
|
||||
message,
|
||||
style: AppTypography.standard.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ConduitButton(
|
||||
text: actionLabel!,
|
||||
onPressed: onAction,
|
||||
isDestructive: true,
|
||||
isCompact: isCompact,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced network error widget with better hierarchy
|
||||
class NetworkErrorWidget extends StatelessWidget {
|
||||
final VoidCallback? onRetry;
|
||||
final String? customMessage;
|
||||
final bool isCompact;
|
||||
|
||||
const NetworkErrorWidget({
|
||||
super.key,
|
||||
this.onRetry,
|
||||
this.customMessage,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Connection Error',
|
||||
message:
|
||||
customMessage ??
|
||||
'Unable to connect to the server. Please check your internet connection and try again.',
|
||||
actionLabel: 'Retry',
|
||||
onAction: onRetry,
|
||||
icon: Icons.wifi_off,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced empty state widget with better hierarchy
|
||||
class EmptyStateWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
final IconData? icon;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
final bool isCompact;
|
||||
|
||||
const EmptyStateWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.icon,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.cardPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.cardBackground,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.cardBorder,
|
||||
width: BorderWidth.standard,
|
||||
),
|
||||
boxShadow: ConduitShadows.card,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.inbox_outlined,
|
||||
size: isCompact ? IconSize.large : IconSize.xxl,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
),
|
||||
SizedBox(height: isCompact ? Spacing.sm : Spacing.md),
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
|
||||
Text(
|
||||
message,
|
||||
style: AppTypography.standard.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ConduitButton(
|
||||
text: actionLabel!,
|
||||
onPressed: onAction,
|
||||
isCompact: isCompact,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced loading error widget with better hierarchy
|
||||
class LoadingErrorWidget extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
final bool isCompact;
|
||||
|
||||
const LoadingErrorWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onRetry,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Loading Failed',
|
||||
message: message,
|
||||
actionLabel: onRetry != null ? 'Try Again' : null,
|
||||
onAction: onRetry,
|
||||
icon: Icons.error_outline,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced validation error widget with better hierarchy
|
||||
class ValidationErrorWidget extends StatelessWidget {
|
||||
final String fieldName;
|
||||
final String message;
|
||||
final VoidCallback? onFix;
|
||||
final bool isCompact;
|
||||
|
||||
const ValidationErrorWidget({
|
||||
super.key,
|
||||
required this.fieldName,
|
||||
required this.message,
|
||||
this.onFix,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Invalid $fieldName',
|
||||
message: message,
|
||||
actionLabel: onFix != null ? 'Fix Now' : null,
|
||||
onAction: onFix,
|
||||
icon: Icons.warning_amber_outlined,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced permission error widget with better hierarchy
|
||||
class PermissionErrorWidget extends StatelessWidget {
|
||||
final String permission;
|
||||
final String message;
|
||||
final VoidCallback? onGrant;
|
||||
final bool isCompact;
|
||||
|
||||
const PermissionErrorWidget({
|
||||
super.key,
|
||||
required this.permission,
|
||||
required this.message,
|
||||
this.onGrant,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Permission Required',
|
||||
message: 'This app needs $permission permission to $message.',
|
||||
actionLabel: onGrant != null ? 'Grant Permission' : null,
|
||||
onAction: onGrant,
|
||||
icon: Icons.security,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced server error widget with better hierarchy
|
||||
class ServerErrorWidget extends StatelessWidget {
|
||||
final String error;
|
||||
final VoidCallback? onRetry;
|
||||
final bool isCompact;
|
||||
|
||||
const ServerErrorWidget({
|
||||
super.key,
|
||||
required this.error,
|
||||
this.onRetry,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Server Error',
|
||||
message: error,
|
||||
actionLabel: onRetry != null ? 'Retry' : null,
|
||||
onAction: onRetry,
|
||||
icon: Icons.cloud_off,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced file error widget with better hierarchy
|
||||
class FileErrorWidget extends StatelessWidget {
|
||||
final String fileName;
|
||||
final String error;
|
||||
final VoidCallback? onRetry;
|
||||
final bool isCompact;
|
||||
|
||||
const FileErrorWidget({
|
||||
super.key,
|
||||
required this.fileName,
|
||||
required this.error,
|
||||
this.onRetry,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'File Error',
|
||||
message: 'Failed to process $fileName: $error',
|
||||
actionLabel: onRetry != null ? 'Try Again' : null,
|
||||
onAction: onRetry,
|
||||
icon: Icons.file_present,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced authentication error widget with better hierarchy
|
||||
class AuthErrorWidget extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onLogin;
|
||||
final bool isCompact;
|
||||
|
||||
const AuthErrorWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onLogin,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Authentication Required',
|
||||
message: message,
|
||||
actionLabel: onLogin != null ? 'Sign In' : null,
|
||||
onAction: onLogin,
|
||||
icon: Icons.lock_outline,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced offline error widget with better hierarchy
|
||||
class OfflineErrorWidget extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
final bool isCompact;
|
||||
|
||||
const OfflineErrorWidget({
|
||||
super.key,
|
||||
this.message =
|
||||
'You\'re currently offline. Please check your internet connection.',
|
||||
this.onRetry,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Offline',
|
||||
message: message,
|
||||
actionLabel: onRetry != null ? 'Retry' : null,
|
||||
onAction: onRetry,
|
||||
icon: Icons.wifi_off,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced timeout error widget with better hierarchy
|
||||
class TimeoutErrorWidget extends StatelessWidget {
|
||||
final String operation;
|
||||
final VoidCallback? onRetry;
|
||||
final bool isCompact;
|
||||
|
||||
const TimeoutErrorWidget({
|
||||
super.key,
|
||||
required this.operation,
|
||||
this.onRetry,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConduitErrorWidget(
|
||||
title: 'Request Timeout',
|
||||
message: 'The $operation request timed out. Please try again.',
|
||||
actionLabel: onRetry != null ? 'Retry' : null,
|
||||
onAction: onRetry,
|
||||
icon: Icons.timer_off,
|
||||
isCompact: isCompact,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user