470 lines
13 KiB
Dart
470 lines
13 KiB
Dart
/// 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<T> on List<T> {
|
|
/// 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<K, List<T>> groupBy<K>(K Function(T) keySelector) {
|
|
final map = <K, List<T>>{};
|
|
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<T> get distinct => toSet().toList();
|
|
|
|
/// Get distinct elements by key
|
|
List<T> distinctBy<K>(K Function(T) keySelector) {
|
|
final seen = <K>{};
|
|
return where((element) => seen.add(keySelector(element))).toList();
|
|
}
|
|
|
|
/// Chunk list into smaller lists of specified size
|
|
List<List<T>> chunk(int size) {
|
|
final chunks = <List<T>>[];
|
|
for (var i = 0; i < length; i += size) {
|
|
chunks.add(sublist(i, (i + size) > length ? length : (i + size)));
|
|
}
|
|
return chunks;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Map Extensions
|
|
// ============================================================================
|
|
|
|
extension MapExtensions<K, V> on Map<K, V> {
|
|
/// 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<T?> push<T>(Widget page) {
|
|
return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
|
|
}
|
|
|
|
/// Navigate and replace current route
|
|
Future<T?> pushReplacement<T>(Widget page) {
|
|
return Navigator.of(
|
|
this,
|
|
).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
|
|
}
|
|
|
|
/// Pop current route
|
|
void pop<T>([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);
|
|
}
|
|
}
|