Files
worker/lib/core/utils/extensions.dart
2025-11-07 11:52:06 +07:00

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);
}
}