chore: initial release

This commit is contained in:
cogwheel0
2025-08-10 01:20:45 +05:30
commit 758615813f
218 changed files with 67743 additions and 0 deletions

View File

@@ -0,0 +1,435 @@
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,
);
}
}

View File

@@ -0,0 +1,487 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io' show Platform;
import '../theme/theme_extensions.dart';
/// Platform-specific utilities for enhanced user experience
class PlatformUtils {
PlatformUtils._();
/// Check if device supports haptic feedback
static bool get supportsHaptics => Platform.isIOS || Platform.isAndroid;
/// Trigger light haptic feedback
static void lightHaptic() {
if (supportsHaptics) {
HapticFeedback.lightImpact();
}
}
/// Trigger medium haptic feedback
static void mediumHaptic() {
if (supportsHaptics && Platform.isIOS) {
HapticFeedback.mediumImpact();
} else if (Platform.isAndroid) {
HapticFeedback.lightImpact();
}
}
/// Trigger heavy haptic feedback
static void heavyHaptic() {
if (supportsHaptics && Platform.isIOS) {
HapticFeedback.heavyImpact();
} else if (Platform.isAndroid) {
HapticFeedback.vibrate();
}
}
/// Trigger selection haptic feedback
static void selectionHaptic() {
if (supportsHaptics) {
HapticFeedback.selectionClick();
}
}
/// Get platform-appropriate icon
static IconData getIcon({required IconData ios, required IconData android}) {
return Platform.isIOS ? ios : android;
}
/// Get platform-appropriate text style
static TextStyle getPlatformTextStyle(BuildContext context) {
if (Platform.isIOS) {
return CupertinoTheme.of(context).textTheme.textStyle;
}
return Theme.of(context).textTheme.bodyMedium ?? const TextStyle();
}
/// Create platform-specific button
static Widget createButton({
required String text,
required VoidCallback? onPressed,
bool isPrimary = true,
Color? color,
}) {
if (Platform.isIOS) {
return Builder(
builder: (context) => CupertinoButton(
onPressed: onPressed,
color: isPrimary
? (color ?? context.conduitTheme.buttonPrimary)
: null,
child: Text(text),
),
);
}
return isPrimary
? FilledButton(
onPressed: onPressed,
style: color != null
? FilledButton.styleFrom(backgroundColor: color)
: null,
child: Text(text),
)
: OutlinedButton(onPressed: onPressed, child: Text(text));
}
/// Create platform-specific switch
static Widget createSwitch({
required bool value,
required ValueChanged<bool>? onChanged,
Color? activeColor,
}) {
if (Platform.isIOS) {
return Builder(
builder: (context) => CupertinoSwitch(
value: value,
onChanged: onChanged,
thumbColor: activeColor ?? context.conduitTheme.buttonPrimary,
),
);
}
return Switch(
value: value,
onChanged: onChanged,
activeTrackColor: activeColor,
);
}
/// Create platform-specific slider
static Widget createSlider({
required double value,
required ValueChanged<double>? onChanged,
double min = 0.0,
double max = 1.0,
int? divisions,
Color? activeColor,
}) {
if (Platform.isIOS) {
return Builder(
builder: (context) => CupertinoSlider(
value: value,
onChanged: onChanged,
min: min,
max: max,
divisions: divisions,
activeColor: activeColor ?? context.conduitTheme.buttonPrimary,
),
);
}
return Slider(
value: value,
onChanged: onChanged,
min: min,
max: max,
divisions: divisions,
activeColor: activeColor,
);
}
}
/// iOS-specific enhancements
class IOSEnhancements {
/// Create iOS-style navigation bar
static PreferredSizeWidget createNavigationBar({
required String title,
VoidCallback? onBack,
List<Widget>? actions,
Color? backgroundColor,
}) {
return CupertinoNavigationBar(
middle: Text(title),
leading: onBack != null
? CupertinoNavigationBarBackButton(onPressed: onBack)
: null,
trailing: actions != null && actions.isNotEmpty
? Row(mainAxisSize: MainAxisSize.min, children: actions)
: null,
backgroundColor: backgroundColor,
);
}
/// Create iOS-style context menu
static Widget createContextMenu({
required Widget child,
required List<ContextMenuAction> actions,
}) {
return CupertinoContextMenu(
actions: actions
.map(
(action) => CupertinoContextMenuAction(
onPressed: action.onPressed,
isDefaultAction: action.isDefault,
isDestructiveAction: action.isDestructive,
child: Text(action.title),
),
)
.toList(),
child: child,
);
}
/// Create iOS-style action sheet
static void showActionSheet({
required BuildContext context,
required String title,
String? message,
required List<ActionSheetAction> actions,
}) {
showCupertinoModalPopup(
context: context,
builder: (context) => CupertinoActionSheet(
title: Text(title),
message: message != null ? Text(message) : null,
actions: actions
.map(
(action) => CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context);
action.onPressed();
},
isDefaultAction: action.isDefault,
isDestructiveAction: action.isDestructive,
child: Text(action.title),
),
)
.toList(),
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
),
);
}
}
/// Android-specific enhancements
class AndroidEnhancements {
/// Create Material You themed button
static Widget createMaterial3Button({
required String text,
required VoidCallback? onPressed,
ButtonType type = ButtonType.filled,
IconData? icon,
}) {
Widget button;
switch (type) {
case ButtonType.filled:
button = icon != null
? FilledButton.icon(
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
)
: FilledButton(onPressed: onPressed, child: Text(text));
break;
case ButtonType.outlined:
button = icon != null
? OutlinedButton.icon(
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
)
: OutlinedButton(onPressed: onPressed, child: Text(text));
break;
case ButtonType.text:
button = icon != null
? TextButton.icon(
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
)
: TextButton(onPressed: onPressed, child: Text(text));
break;
}
return button;
}
/// Create Material 3 card
static Widget createCard({
required Widget child,
VoidCallback? onTap,
EdgeInsetsGeometry? padding,
CardType type = CardType.filled,
}) {
Widget card;
switch (type) {
case CardType.filled:
card = Card.filled(
child: padding != null
? Padding(padding: padding, child: child)
: child,
);
break;
case CardType.outlined:
card = Card.outlined(
child: padding != null
? Padding(padding: padding, child: child)
: child,
);
break;
case CardType.elevated:
card = Card(
child: padding != null
? Padding(padding: padding, child: child)
: child,
);
break;
}
if (onTap != null) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
child: card,
);
}
return card;
}
/// Create floating action button with Material 3 styling
static Widget createFAB({
required VoidCallback onPressed,
required Widget child,
bool isExtended = false,
String? label,
}) {
if (isExtended && label != null) {
return FloatingActionButton.extended(
onPressed: onPressed,
icon: child,
label: Text(label),
);
}
return FloatingActionButton(onPressed: onPressed, child: child);
}
}
/// Platform-aware widget that provides different implementations
class PlatformWidget extends StatelessWidget {
final Widget ios;
final Widget android;
final Widget? fallback;
const PlatformWidget({
super.key,
required this.ios,
required this.android,
this.fallback,
});
@override
Widget build(BuildContext context) {
if (Platform.isIOS) {
return ios;
} else if (Platform.isAndroid) {
return android;
} else {
return fallback ?? android;
}
}
}
/// Enhanced button with platform-specific haptics
class HapticButton extends StatelessWidget {
final Widget child;
final VoidCallback? onPressed;
final HapticType hapticType;
final ButtonStyle? style;
const HapticButton({
super.key,
required this.child,
required this.onPressed,
this.hapticType = HapticType.light,
this.style,
});
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: onPressed != null
? () {
_triggerHaptic();
onPressed!();
}
: null,
style: style,
child: child,
);
}
void _triggerHaptic() {
switch (hapticType) {
case HapticType.light:
PlatformUtils.lightHaptic();
break;
case HapticType.medium:
PlatformUtils.mediumHaptic();
break;
case HapticType.heavy:
PlatformUtils.heavyHaptic();
break;
case HapticType.selection:
PlatformUtils.selectionHaptic();
break;
}
}
}
/// Enhanced list tile with platform-specific styling
class PlatformListTile extends StatelessWidget {
final Widget? leading;
final Widget? title;
final Widget? subtitle;
final Widget? trailing;
final VoidCallback? onTap;
final bool enableHaptic;
const PlatformListTile({
super.key,
this.leading,
this.title,
this.subtitle,
this.trailing,
this.onTap,
this.enableHaptic = true,
});
@override
Widget build(BuildContext context) {
final tile = ListTile(
leading: leading,
title: title,
subtitle: subtitle,
trailing: trailing,
onTap: onTap != null && enableHaptic
? () {
PlatformUtils.selectionHaptic();
onTap!();
}
: onTap,
);
if (Platform.isIOS) {
return Builder(
builder: (context) => Container(
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
border: Border(
bottom: BorderSide(
color: context.conduitTheme.dividerColor,
width: 0.5,
),
),
),
child: tile,
),
);
}
return tile;
}
}
// Enums and supporting classes
enum HapticType { light, medium, heavy, selection }
enum ButtonType { filled, outlined, text }
enum CardType { filled, outlined, elevated }
class ContextMenuAction {
final String title;
final VoidCallback onPressed;
final bool isDefault;
final bool isDestructive;
const ContextMenuAction({
required this.title,
required this.onPressed,
this.isDefault = false,
this.isDestructive = false,
});
}
class ActionSheetAction {
final String title;
final VoidCallback onPressed;
final bool isDefault;
final bool isDestructive;
const ActionSheetAction({
required this.title,
required this.onPressed,
this.isDefault = false,
this.isDestructive = false,
});
}

View File

@@ -0,0 +1,221 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io' show Platform;
import '../theme/theme_extensions.dart';
/// Utility functions for common UI patterns and helpers
/// Following Conduit design principles
class UiUtils {
static bool get isIOS => Platform.isIOS;
/// Returns platform-appropriate icon
static IconData platformIcon({
required IconData ios,
required IconData android,
}) {
return isIOS ? ios : android;
}
/// Common platform icons used throughout the app
static IconData get chatIcon =>
platformIcon(ios: CupertinoIcons.chat_bubble_2, android: Icons.chat);
static IconData get searchIcon =>
platformIcon(ios: CupertinoIcons.search, android: Icons.search);
static IconData get deleteIcon =>
platformIcon(ios: CupertinoIcons.delete, android: Icons.delete);
static IconData get archiveIcon =>
platformIcon(ios: CupertinoIcons.archivebox, android: Icons.archive);
static IconData get shareIcon =>
platformIcon(ios: CupertinoIcons.share, android: Icons.share);
static IconData get settingsIcon =>
platformIcon(ios: CupertinoIcons.gear, android: Icons.settings);
static IconData get editIcon =>
platformIcon(ios: CupertinoIcons.pencil, android: Icons.edit_outlined);
static IconData get menuIcon =>
platformIcon(ios: CupertinoIcons.line_horizontal_3, android: Icons.menu);
static IconData get addIcon =>
platformIcon(ios: CupertinoIcons.plus_circle, android: Icons.add);
static IconData get attachIcon =>
platformIcon(ios: CupertinoIcons.paperclip, android: Icons.attach_file);
static IconData get micIcon =>
platformIcon(ios: CupertinoIcons.mic, android: Icons.mic);
static IconData get sendIcon => platformIcon(
ios: CupertinoIcons.arrow_up_circle_fill,
android: Icons.send,
);
static IconData get moreIcon => platformIcon(
ios: CupertinoIcons.ellipsis_vertical,
android: Icons.more_vert,
);
static IconData get closeIcon =>
platformIcon(ios: CupertinoIcons.xmark, android: Icons.close);
static IconData get checkIcon =>
platformIcon(ios: CupertinoIcons.check_mark, android: Icons.check);
static IconData get globeIcon =>
platformIcon(ios: CupertinoIcons.globe, android: Icons.public);
static IconData get folderIcon =>
platformIcon(ios: CupertinoIcons.folder, android: Icons.folder);
static IconData get tagIcon =>
platformIcon(ios: CupertinoIcons.tag, android: Icons.label);
static IconData get copyIcon =>
platformIcon(ios: CupertinoIcons.doc_on_doc, android: Icons.copy);
static IconData get pinIcon =>
platformIcon(ios: CupertinoIcons.pin_fill, android: Icons.push_pin);
static IconData get unpinIcon => platformIcon(
ios: CupertinoIcons.pin_slash,
android: Icons.push_pin_outlined,
);
/// Shows a Conduit-styled snackbar with conversational messaging
static void showMessage(
BuildContext context,
String message, {
bool isError = false,
VoidCallback? onRetry,
Duration? duration,
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError
? context.conduitTheme.error
: context.conduitTheme.buttonPrimary,
behavior: SnackBarBehavior.floating,
action: onRetry != null
? SnackBarAction(
label: 'Try again',
textColor: context.conduitTheme.textInverse,
onPressed: onRetry,
)
: null,
duration: duration ?? const Duration(seconds: 3),
),
);
}
/// Shows a Conduit-styled confirmation dialog
static Future<bool> showConfirmationDialog(
BuildContext context, {
required String title,
required String message,
String confirmText = 'Confirm',
String cancelText = 'Cancel',
bool isDestructive = false,
}) async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: context.conduitTheme.surfaceBackground,
title: Text(
title,
style: TextStyle(color: context.conduitTheme.textPrimary),
),
content: Text(
message,
style: TextStyle(color: context.conduitTheme.textSecondary),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(cancelText),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: isDestructive
? TextButton.styleFrom(
foregroundColor: context.conduitTheme.error,
)
: null,
child: Text(confirmText),
),
],
),
) ??
false;
}
/// Formats dates in a conversational way following Conduit patterns
static String formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
if (difference.inHours == 0) {
if (difference.inMinutes == 0) {
return 'Just now';
}
return '${difference.inMinutes}m ago';
}
return '${difference.inHours}h ago';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else if (difference.inDays < 30) {
final weeks = (difference.inDays / 7).floor();
return weeks == 1 ? '1 week ago' : '$weeks weeks ago';
} else if (difference.inDays < 365) {
final months = (difference.inDays / 30).floor();
return months == 1 ? '1 month ago' : '$months months ago';
} else {
return '${date.month}/${date.day}/${date.year}';
}
}
/// Creates a smooth haptic feedback on iOS
static void hapticFeedback() {
if (isIOS) {
// iOS haptic feedback would be implemented here
// For now, we'll leave this as a placeholder
}
}
/// Safe area padding helper
static EdgeInsets safeAreaPadding(BuildContext context) {
return MediaQuery.of(context).padding;
}
/// Screen size helpers
static Size screenSize(BuildContext context) {
return MediaQuery.of(context).size;
}
static bool isSmallScreen(BuildContext context) {
return screenSize(context).width < 375;
}
static bool isLargeScreen(BuildContext context) {
return screenSize(context).width > 414;
}
/// Keyboard handling
static bool isKeyboardOpen(BuildContext context) {
return MediaQuery.of(context).viewInsets.bottom > 0;
}
/// Focus management
static void unfocus(BuildContext context) {
FocusScope.of(context).unfocus();
}
}