/// Dart Extension Methods /// /// Provides useful extension methods for common data types /// used throughout the app. library; import 'dart:math' as math; import 'package:flutter/material.dart'; // ============================================================================ // String Extensions // ============================================================================ extension StringExtensions on String { /// Check if string is null or empty bool get isNullOrEmpty => trim().isEmpty; /// Check if string is not null and not empty bool get isNotNullOrEmpty => trim().isNotEmpty; /// Capitalize first letter String get capitalize { if (isEmpty) return this; return '${this[0].toUpperCase()}${substring(1)}'; } /// Capitalize each word String get capitalizeWords { if (isEmpty) return this; return split(' ').map((word) => word.capitalize).join(' '); } /// Convert to title case String get titleCase => capitalizeWords; /// Remove all whitespace String get removeWhitespace => replaceAll(RegExp(r'\s+'), ''); /// Check if string is a valid email bool get isEmail { final emailRegex = RegExp( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', ); return emailRegex.hasMatch(this); } /// Check if string is a valid Vietnamese phone number bool get isPhoneNumber { final phoneRegex = RegExp(r'^(0|\+?84)(3|5|7|8|9)[0-9]{8}$'); return phoneRegex.hasMatch(replaceAll(RegExp(r'[^\d+]'), '')); } /// Check if string is numeric bool get isNumeric { return double.tryParse(this) != null; } /// Convert string to int (returns null if invalid) int? get toIntOrNull => int.tryParse(this); /// Convert string to double (returns null if invalid) double? get toDoubleOrNull => double.tryParse(this); /// Truncate string with ellipsis String truncate(int maxLength, {String ellipsis = '...'}) { if (length <= maxLength) return this; return '${substring(0, maxLength - ellipsis.length)}$ellipsis'; } /// Remove Vietnamese diacritics String get removeDiacritics { const withDiacritics = 'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ'; const withoutDiacritics = 'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiioooooooooooooooooouuuuuuuuuuuyyyyyd'; var result = toLowerCase(); for (var i = 0; i < withDiacritics.length; i++) { result = result.replaceAll(withDiacritics[i], withoutDiacritics[i]); } return result; } /// Convert to URL-friendly slug String get slugify { var slug = removeDiacritics; slug = slug.toLowerCase(); slug = slug.replaceAll(RegExp(r'[^\w\s-]'), ''); slug = slug.replaceAll(RegExp(r'[-\s]+'), '-'); return slug; } /// Mask email (e.g., "j***@example.com") String get maskEmail { if (!isEmail) return this; final parts = split('@'); final name = parts[0]; final maskedName = name.length > 2 ? '${name[0]}${'*' * (name.length - 1)}' : name; return '$maskedName@${parts[1]}'; } /// Mask phone number (e.g., "0xxx xxx ***") String get maskPhone { final cleaned = replaceAll(RegExp(r'\D'), ''); if (cleaned.length < 10) return this; return '${cleaned.substring(0, 4)} ${cleaned.substring(4, 7)} ***'; } } // ============================================================================ // DateTime Extensions // ============================================================================ extension DateTimeExtensions on DateTime { /// Check if date is today bool get isToday { final now = DateTime.now(); return year == now.year && month == now.month && day == now.day; } /// Check if date is yesterday bool get isYesterday { final yesterday = DateTime.now().subtract(const Duration(days: 1)); return year == yesterday.year && month == yesterday.month && day == yesterday.day; } /// Check if date is tomorrow bool get isTomorrow { final tomorrow = DateTime.now().add(const Duration(days: 1)); return year == tomorrow.year && month == tomorrow.month && day == tomorrow.day; } /// Check if date is in the past bool get isPast => isBefore(DateTime.now()); /// Check if date is in the future bool get isFuture => isAfter(DateTime.now()); /// Get start of day (00:00:00) DateTime get startOfDay => DateTime(year, month, day); /// Get end of day (23:59:59) DateTime get endOfDay => DateTime(year, month, day, 23, 59, 59, 999); /// Get start of month DateTime get startOfMonth => DateTime(year, month, 1); /// Get end of month DateTime get endOfMonth => DateTime(year, month + 1, 0, 23, 59, 59, 999); /// Get start of year DateTime get startOfYear => DateTime(year, 1, 1); /// Get end of year DateTime get endOfYear => DateTime(year, 12, 31, 23, 59, 59, 999); /// Add days DateTime addDays(int days) => add(Duration(days: days)); /// Subtract days DateTime subtractDays(int days) => subtract(Duration(days: days)); /// Add months DateTime addMonths(int months) => DateTime(year, month + months, day); /// Subtract months DateTime subtractMonths(int months) => DateTime(year, month - months, day); /// Add years DateTime addYears(int years) => DateTime(year + years, month, day); /// Subtract years DateTime subtractYears(int years) => DateTime(year - years, month, day); /// Get age in years from this date int get ageInYears { final today = DateTime.now(); var age = today.year - year; if (today.month < month || (today.month == month && today.day < day)) { age--; } return age; } /// Get difference in days from now int get daysFromNow => DateTime.now().difference(this).inDays; /// Get difference in hours from now int get hoursFromNow => DateTime.now().difference(this).inHours; /// Get difference in minutes from now int get minutesFromNow => DateTime.now().difference(this).inMinutes; /// Copy with new values DateTime copyWith({ int? year, int? month, int? day, int? hour, int? minute, int? second, int? millisecond, int? microsecond, }) { return DateTime( year ?? this.year, month ?? this.month, day ?? this.day, hour ?? this.hour, minute ?? this.minute, second ?? this.second, millisecond ?? this.millisecond, microsecond ?? this.microsecond, ); } } // ============================================================================ // Duration Extensions // ============================================================================ extension DurationExtensions on Duration { /// Format duration as readable string (e.g., "2 giờ 30 phút") String get formatted { final hours = inHours; final minutes = inMinutes.remainder(60); final seconds = inSeconds.remainder(60); if (hours > 0) { if (minutes > 0) { return '$hours giờ $minutes phút'; } return '$hours giờ'; } else if (minutes > 0) { if (seconds > 0) { return '$minutes phút $seconds giây'; } return '$minutes phút'; } else { return '$seconds giây'; } } /// Format as HH:MM:SS String get hhmmss { final hours = inHours; final minutes = inMinutes.remainder(60); final seconds = inSeconds.remainder(60); return '${hours.toString().padLeft(2, '0')}:' '${minutes.toString().padLeft(2, '0')}:' '${seconds.toString().padLeft(2, '0')}'; } /// Format as MM:SS String get mmss { final minutes = inMinutes.remainder(60); final seconds = inSeconds.remainder(60); return '${minutes.toString().padLeft(2, '0')}:' '${seconds.toString().padLeft(2, '0')}'; } } // ============================================================================ // List Extensions // ============================================================================ extension ListExtensions on List { /// Get first element or null if list is empty T? get firstOrNull => isEmpty ? null : first; /// Get last element or null if list is empty T? get lastOrNull => isEmpty ? null : last; /// Get element at index or null if out of bounds T? elementAtOrNull(int index) { if (index < 0 || index >= length) return null; return this[index]; } /// Group list by key Map> groupBy(K Function(T) keySelector) { final map = >{}; for (final element in this) { final key = keySelector(element); if (!map.containsKey(key)) { map[key] = []; } map[key]!.add(element); } return map; } /// Get distinct elements List get distinct => toSet().toList(); /// Get distinct elements by key List distinctBy(K Function(T) keySelector) { final seen = {}; return where((element) => seen.add(keySelector(element))).toList(); } /// Chunk list into smaller lists of specified size List> chunk(int size) { final chunks = >[]; for (var i = 0; i < length; i += size) { chunks.add(sublist(i, (i + size) > length ? length : (i + size))); } return chunks; } } // ============================================================================ // Map Extensions // ============================================================================ extension MapExtensions on Map { /// Get value or default if key doesn't exist V getOrDefault(K key, V defaultValue) { return containsKey(key) ? this[key] as V : defaultValue; } /// Get value or null if key doesn't exist V? getOrNull(K key) { return containsKey(key) ? this[key] : null; } } // ============================================================================ // BuildContext Extensions // ============================================================================ extension BuildContextExtensions on BuildContext { /// Get screen size Size get screenSize => MediaQuery.of(this).size; /// Get screen width double get screenWidth => MediaQuery.of(this).size.width; /// Get screen height double get screenHeight => MediaQuery.of(this).size.height; /// Check if screen is small (<600dp) bool get isSmallScreen => MediaQuery.of(this).size.width < 600; /// Check if screen is medium (600-960dp) bool get isMediumScreen { final width = MediaQuery.of(this).size.width; return width >= 600 && width < 960; } /// Check if screen is large (>=960dp) bool get isLargeScreen => MediaQuery.of(this).size.width >= 960; /// Get theme ThemeData get theme => Theme.of(this); /// Get text theme TextTheme get textTheme => Theme.of(this).textTheme; /// Get color scheme ColorScheme get colorScheme => Theme.of(this).colorScheme; /// Get primary color Color get primaryColor => Theme.of(this).primaryColor; /// Check if dark mode is enabled bool get isDarkMode => Theme.of(this).brightness == Brightness.dark; /// Get safe area padding EdgeInsets get safeAreaPadding => MediaQuery.of(this).padding; /// Get bottom safe area padding (for devices with notch) double get bottomSafeArea => MediaQuery.of(this).padding.bottom; /// Get top safe area padding (for status bar) double get topSafeArea => MediaQuery.of(this).padding.top; /// Show snackbar void showSnackBar(String message, {Duration? duration}) { ScaffoldMessenger.of(this).showSnackBar( SnackBar( content: Text(message), duration: duration ?? const Duration(seconds: 3), ), ); } /// Show error snackbar void showErrorSnackBar(String message, {Duration? duration}) { ScaffoldMessenger.of(this).showSnackBar( SnackBar( content: Text(message), backgroundColor: colorScheme.error, duration: duration ?? const Duration(seconds: 4), ), ); } /// Show success snackbar void showSuccessSnackBar(String message, {Duration? duration}) { ScaffoldMessenger.of(this).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.green, duration: duration ?? const Duration(seconds: 3), ), ); } /// Hide keyboard void hideKeyboard() { FocusScope.of(this).unfocus(); } /// Navigate to route Future push(Widget page) { return Navigator.of(this).push(MaterialPageRoute(builder: (_) => page)); } /// Navigate and replace current route Future pushReplacement(Widget page) { return Navigator.of( this, ).pushReplacement(MaterialPageRoute(builder: (_) => page)); } /// Pop current route void pop([T? result]) { Navigator.of(this).pop(result); } /// Pop until first route void popUntilFirst() { Navigator.of(this).popUntil((route) => route.isFirst); } } // ============================================================================ // Num Extensions // ============================================================================ extension NumExtensions on num { /// Check if number is positive bool get isPositive => this > 0; /// Check if number is negative bool get isNegative => this < 0; /// Check if number is zero bool get isZero => this == 0; /// Clamp number between min and max num clampTo(num min, num max) => clamp(min, max); /// Round to specified decimal places double roundToDecimal(int places) { final mod = math.pow(10.0, places); return ((this * mod).round().toDouble() / mod); } }