first commit
This commit is contained in:
166
lib/app/core/utils/app_validators.dart
Normal file
166
lib/app/core/utils/app_validators.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
class AppValidators {
|
||||
AppValidators._();
|
||||
|
||||
static String? required(
|
||||
String value, {
|
||||
String message = 'Field tidak boleh kosong',
|
||||
}) {
|
||||
if (value.trim().isEmpty) {
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? minLength(
|
||||
String value,
|
||||
int min, {
|
||||
String? message,
|
||||
}) {
|
||||
if (value.length < min) {
|
||||
return message ?? 'Minimal $min karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? maxLength(
|
||||
String value,
|
||||
int max, {
|
||||
String? message,
|
||||
}) {
|
||||
if (value.length > max) {
|
||||
return message ?? 'Maksimal $max karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static final _emailRegex = RegExp(r'^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$');
|
||||
|
||||
static String? email(
|
||||
String value, {
|
||||
String emptyMessage = 'Email tidak boleh kosong',
|
||||
String invalidMessage = 'Format email tidak valid',
|
||||
}) {
|
||||
if (value.trim().isEmpty) {
|
||||
return emptyMessage;
|
||||
}
|
||||
|
||||
if (!_emailRegex.hasMatch(value.trim())) {
|
||||
return invalidMessage;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static final _phoneRegex = RegExp(r'^[0-9]{9,15}$');
|
||||
|
||||
static String? phone(
|
||||
String value, {
|
||||
String emptyMessage = 'Nomor telepon tidak boleh kosong',
|
||||
String invalidMessage = 'Nomor telepon tidak valid',
|
||||
}) {
|
||||
final v = value.replaceAll(RegExp('[^0-9]'), '');
|
||||
|
||||
if (v.isEmpty) {
|
||||
return emptyMessage;
|
||||
}
|
||||
|
||||
if (!_phoneRegex.hasMatch(v)) {
|
||||
return invalidMessage;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? password(
|
||||
String value, {
|
||||
int minLength = 8,
|
||||
bool requireUppercase = true,
|
||||
bool requireNumber = true,
|
||||
bool requireSymbol = false,
|
||||
}) {
|
||||
if (value.isEmpty) {
|
||||
return 'Password tidak boleh kosong';
|
||||
}
|
||||
|
||||
if (value.length < minLength) {
|
||||
return 'Password minimal $minLength karakter';
|
||||
}
|
||||
|
||||
if (requireUppercase && !value.contains(RegExp('[A-Z]'))) {
|
||||
return 'Password harus mengandung huruf besar';
|
||||
}
|
||||
|
||||
if (requireNumber && !value.contains(RegExp('[0-9]'))) {
|
||||
return 'Password harus mengandung angka';
|
||||
}
|
||||
|
||||
if (requireSymbol && !value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
|
||||
return 'Password harus mengandung simbol';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? number(
|
||||
String value, {
|
||||
String message = 'Harus berupa angka',
|
||||
}) {
|
||||
if (value.isEmpty) return null;
|
||||
|
||||
if (num.tryParse(value) == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? minNumber(
|
||||
String value,
|
||||
num min, {
|
||||
String? message,
|
||||
}) {
|
||||
final n = num.tryParse(value);
|
||||
if (n == null) return 'Harus berupa angka';
|
||||
|
||||
if (n < min) {
|
||||
return message ?? 'Minimal $min';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? maxNumber(
|
||||
String value,
|
||||
num max, {
|
||||
String? message,
|
||||
}) {
|
||||
final n = num.tryParse(value);
|
||||
if (n == null) return 'Harus berupa angka';
|
||||
|
||||
if (n > max) {
|
||||
return message ?? 'Maksimal $max';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? equals(
|
||||
String value,
|
||||
String otherValue, {
|
||||
String message = 'Nilai tidak sama',
|
||||
}) {
|
||||
if (value != otherValue) {
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? compose(
|
||||
String value,
|
||||
List<String? Function(String)> validators,
|
||||
) {
|
||||
for (final validator in validators) {
|
||||
final result = validator(value);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
80
lib/app/core/utils/date_time_utils.dart
Normal file
80
lib/app/core/utils/date_time_utils.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateTimeUtils {
|
||||
DateTimeUtils._();
|
||||
|
||||
static const _locale = 'id_ID';
|
||||
|
||||
static String yyyyMMdd(DateTime date) {
|
||||
return DateFormat('yyyy-MM-dd', _locale).format(date);
|
||||
}
|
||||
|
||||
static String ddMMMyyyy(DateTime date) {
|
||||
return DateFormat('dd MMM yyyy', _locale).format(date);
|
||||
}
|
||||
|
||||
static String ddMMMMyyyy(DateTime date) {
|
||||
return DateFormat('dd MMMM yyyy', _locale).format(date);
|
||||
}
|
||||
|
||||
static String ddMMMyyyyHHmm(DateTime date) {
|
||||
return DateFormat('dd MMM yyyy HH:mm', _locale).format(date);
|
||||
}
|
||||
|
||||
static String hhMM(DateTime date) {
|
||||
return DateFormat('HH:mm', _locale).format(date);
|
||||
}
|
||||
|
||||
static String hhMMss(DateTime date) {
|
||||
return DateFormat('HH:mm:ss', _locale).format(date);
|
||||
}
|
||||
|
||||
static String dayDDMMMMyyyy(DateTime date) {
|
||||
return DateFormat('EEEE, dd MMMM yyyy', _locale).format(date);
|
||||
}
|
||||
|
||||
static String dayDDMMMMyyyyHHmm(DateTime date) {
|
||||
return DateFormat('EEEE, dd MMMM yyyy HH:mm:ss', _locale).format(date);
|
||||
}
|
||||
|
||||
static DateTime? parse(String? value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return DateTime.tryParse(value);
|
||||
}
|
||||
|
||||
static String toApi(DateTime date) {
|
||||
return DateFormat("yyyy-MM-dd'T'HH:mm:ss", _locale).format(date);
|
||||
}
|
||||
|
||||
static String timeAgo(DateTime date) {
|
||||
final diff = DateTime.now().difference(date);
|
||||
|
||||
if (diff.inSeconds < 60) {
|
||||
return '${diff.inSeconds} detik lalu';
|
||||
} else if (diff.inMinutes < 60) {
|
||||
return '${diff.inMinutes} menit lalu';
|
||||
} else if (diff.inHours < 24) {
|
||||
return '${diff.inHours} jam lalu';
|
||||
} else if (diff.inDays < 7) {
|
||||
return '${diff.inDays} hari lalu';
|
||||
}
|
||||
|
||||
return dayDDMMMMyyyy(date);
|
||||
}
|
||||
|
||||
static String formatDateTimeWithGMT(DateTime date) {
|
||||
final formatted = DateFormat(
|
||||
'MMM dd, yyyy hh:mm:ss a',
|
||||
_locale,
|
||||
).format(date);
|
||||
|
||||
final offset = date.timeZoneOffset;
|
||||
|
||||
final sign = offset.isNegative ? '-' : '+';
|
||||
|
||||
final hours = offset.inHours.abs().toString().padLeft(2, '0');
|
||||
final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0');
|
||||
|
||||
return '$formatted GMT$sign$hours:$minutes';
|
||||
}
|
||||
}
|
||||
5
lib/app/core/utils/file_size.dart
Normal file
5
lib/app/core/utils/file_size.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
String formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
58
lib/app/core/utils/session_manager.dart
Normal file
58
lib/app/core/utils/session_manager.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SessionManager {
|
||||
static const _refreshTokenKey = 'refreshToken';
|
||||
static const _tokenKey = 'token';
|
||||
static const _userKey = 'user';
|
||||
|
||||
static Future<void> saveToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_tokenKey, token);
|
||||
}
|
||||
|
||||
static Future<void> saveRefreshToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_refreshTokenKey, token);
|
||||
}
|
||||
|
||||
static Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_tokenKey);
|
||||
}
|
||||
|
||||
static Future<String?> getRefreshToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_refreshTokenKey);
|
||||
}
|
||||
|
||||
static Future<void> saveUser(Map<String, dynamic> user) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_userKey, jsonEncode(user));
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> getUser() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final data = prefs.getString(_userKey);
|
||||
|
||||
if (data == null) return null;
|
||||
|
||||
return jsonDecode(data) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<String?> getUserName() async {
|
||||
final user = await getUser();
|
||||
return user?['name']?.toString() ?? '';
|
||||
}
|
||||
|
||||
static Future<String?> getNrp() async {
|
||||
final user = await getUser();
|
||||
return user?['nrp']?.toString() ?? '';
|
||||
}
|
||||
|
||||
static Future<void> clearSession() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove(_userKey);
|
||||
}
|
||||
}
|
||||
181
lib/app/core/utils/show_message.dart
Normal file
181
lib/app/core/utils/show_message.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/app_colors.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/enum/snackbar_type_enum.dart';
|
||||
|
||||
void showMessage(
|
||||
BuildContext context,
|
||||
String message,
|
||||
SnackBarType snackBarType, {
|
||||
int duration = 2000,
|
||||
}) {
|
||||
final overlay = Overlay.of(context);
|
||||
|
||||
final indicatorColor = switch (snackBarType) {
|
||||
SnackBarType.error => AppColors.primary900,
|
||||
SnackBarType.success => AppColors.statusSuccess,
|
||||
SnackBarType.warning => AppColors.statusWarning,
|
||||
_ => const Color(0xFF7FAAF6),
|
||||
};
|
||||
|
||||
late OverlayEntry entry;
|
||||
|
||||
entry = OverlayEntry(
|
||||
builder: (_) => _TopSnackBar(
|
||||
message: message,
|
||||
duration: duration,
|
||||
indicatorColor: indicatorColor,
|
||||
onDismiss: () {
|
||||
entry.remove();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(entry);
|
||||
}
|
||||
|
||||
class _TopSnackBar extends StatefulWidget {
|
||||
const _TopSnackBar({
|
||||
required this.message,
|
||||
required this.duration,
|
||||
required this.indicatorColor,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final int duration;
|
||||
final Color indicatorColor;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
@override
|
||||
State<_TopSnackBar> createState() => _TopSnackBarState();
|
||||
}
|
||||
|
||||
class _TopSnackBarState extends State<_TopSnackBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController controller;
|
||||
late Animation<Offset> slideAnimation;
|
||||
late Animation<double> opacityAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 620),
|
||||
reverseDuration: const Duration(milliseconds: 520),
|
||||
);
|
||||
|
||||
slideAnimation =
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0, -2),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic,
|
||||
),
|
||||
);
|
||||
|
||||
opacityAnimation =
|
||||
Tween<double>(
|
||||
begin: 0,
|
||||
end: 1,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
|
||||
unawaited(controller.forward());
|
||||
|
||||
unawaited(_startTimer());
|
||||
}
|
||||
|
||||
Future<void> _startTimer() async {
|
||||
await Future<void>.delayed(Duration(milliseconds: widget.duration));
|
||||
await dismiss();
|
||||
}
|
||||
|
||||
Future<void> dismiss() async {
|
||||
if (!mounted) return;
|
||||
await controller.reverse();
|
||||
widget.onDismiss();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 8,
|
||||
left: 16.w,
|
||||
right: 16.w,
|
||||
child: SlideTransition(
|
||||
position: slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: opacityAnimation,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: dismiss,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 600.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 10.r,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 0),
|
||||
child: Text(
|
||||
widget.message,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: Duration(milliseconds: widget.duration),
|
||||
tween: Tween(begin: 1, end: 0),
|
||||
builder: (_, value, _) {
|
||||
return LinearProgressIndicator(
|
||||
value: value,
|
||||
color: widget.indicatorColor,
|
||||
backgroundColor: Colors.transparent,
|
||||
minHeight: 4.h,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/app/core/utils/token_manager.dart
Normal file
50
lib/app/core/utils/token_manager.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dart:developer';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class TokenManager {
|
||||
static const _tokenKey = 'token';
|
||||
static const _refreshTokenKey = 'refreshToken';
|
||||
static const _oneTimeFlagPrefix = 'one_time_flag__';
|
||||
|
||||
static Future<void> setOneTimeFlag(
|
||||
String name, {
|
||||
required bool value,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('$_oneTimeFlagPrefix$name', value);
|
||||
}
|
||||
|
||||
static Future<bool> getOneTimeFlag(String name) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool('$_oneTimeFlagPrefix$name') ?? false;
|
||||
}
|
||||
|
||||
static Future<void> clearOneTimeFlag(
|
||||
String name,
|
||||
) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('$_oneTimeFlagPrefix$name');
|
||||
}
|
||||
|
||||
static Future<void> saveRefreshToken(String token) async {
|
||||
log('SAVE REFRESH TOKEN: $token');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_refreshTokenKey, token);
|
||||
}
|
||||
|
||||
static Future<void> clearToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_refreshTokenKey, '');
|
||||
await prefs.setString(_tokenKey, '');
|
||||
}
|
||||
|
||||
static Future<String?> getRefreshToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_refreshTokenKey);
|
||||
}
|
||||
|
||||
static Future<void> removeToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
}
|
||||
}
|
||||
20
lib/app/core/utils/version_utils.dart
Normal file
20
lib/app/core/utils/version_utils.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
class VersionUtils {
|
||||
static bool isNewer(String remote, String local) {
|
||||
final r = _parse(remote);
|
||||
final l = _parse(local);
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (r[i] > l[i]) return true;
|
||||
if (r[i] < l[i]) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static List<int> _parse(String v) {
|
||||
final parts = v.split('.');
|
||||
return List.generate(
|
||||
3,
|
||||
(i) => i < parts.length ? int.tryParse(parts[i]) ?? 0 : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user