feat&refact&imp: code modal internet check and prepare UI statement

This commit is contained in:
pahmiudahgede 2025-05-28 18:06:31 +07:00
parent 42cd53752b
commit 4db92f3d22
14 changed files with 2278 additions and 724 deletions

View File

@ -1,11 +1,13 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http_parser/http_parser.dart';
import 'package:http/http.dart' as http;
import 'package:rijig_mobile/core/api/api_exception.dart';
import 'package:rijig_mobile/core/network/network_service.dart';
import 'package:rijig_mobile/core/storage/expired_token.dart';
import 'package:rijig_mobile/core/storage/secure_storage.dart';
@ -17,6 +19,11 @@ class Https {
final String? _baseUrl = dotenv.env["BASE_URL"];
final String? _apiKey = dotenv.env["API_KEY"];
final SecureStorage _secureStorage = SecureStorage();
final NetworkService _networkService = NetworkService();
final int _maxRetries = 3;
final List<int> _retryStatusCodes = [408, 429, 500, 502, 503, 504];
final Duration _baseRetryDelay = const Duration(seconds: 1);
Future<Map<String, String>> _getHeaders() async {
String? token = await _secureStorage.readSecureData('token');
@ -51,52 +58,78 @@ class Https {
Encoding? encoding,
String? baseUrl,
http.MultipartRequest? multipartRequest,
bool checkNetwork = true,
int retryCount = 0,
}) async {
if (checkNetwork) {
final bool isConnected = await _networkService.checkConnection();
if (!isConnected) {
throw NetworkException(
'No internet connection available',
NetworkErrorType.noConnection,
);
}
}
final requestHeaders = await _getHeaders();
String url = "${baseUrl ?? _baseUrl}$desturl";
debugPrint("url $url");
debugPrint("Request URL: $url");
debugPrint("Network Quality: ${_networkService.currentQuality}");
final timeout = _networkService.getTimeoutDuration();
http.Response response;
try {
if (multipartRequest != null) {
response = await multipartRequest.send().then(
(response) => http.Response.fromStream(response),
);
response = await multipartRequest
.send()
.timeout(timeout)
.then((response) => http.Response.fromStream(response));
} else {
switch (method.toLowerCase()) {
case 'get':
response = await http.get(Uri.parse(url), headers: requestHeaders);
response = await http
.get(Uri.parse(url), headers: requestHeaders)
.timeout(timeout);
break;
case 'post':
response = await http.post(
response = await http
.post(
Uri.parse(url),
body: jsonEncode(body),
headers: requestHeaders,
encoding: encoding,
);
)
.timeout(timeout);
break;
case 'put':
response = await http.put(
response = await http
.put(
Uri.parse(url),
body: jsonEncode(body),
headers: requestHeaders,
encoding: encoding,
);
)
.timeout(timeout);
break;
case 'delete':
response = await http.delete(
response = await http
.delete(
Uri.parse(url),
body: jsonEncode(body),
headers: requestHeaders,
);
)
.timeout(timeout);
break;
case 'patch':
response = await http.patch(
response = await http
.patch(
Uri.parse(url),
body: jsonEncode(body),
headers: requestHeaders,
encoding: encoding,
);
)
.timeout(timeout);
break;
default:
throw ApiException('Unsupported HTTP method: $method', 405);
@ -104,15 +137,75 @@ class Https {
}
final int statusCode = response.statusCode;
if (_shouldRetry(statusCode, retryCount)) {
return await _retryRequest(
method,
desturl: desturl,
headers: headers,
body: body,
encoding: encoding,
baseUrl: baseUrl,
multipartRequest: multipartRequest,
retryCount: retryCount + 1,
);
}
if (statusCode < 200 || statusCode >= 400) {
throw ApiException(
'Error during HTTP $method request: ${response.body}',
statusCode,
);
}
return jsonDecode(response.body);
} on TimeoutException catch (e) {
// Pindahkan TimeoutException ke atas sebelum SocketException
debugPrint('Timeout Exception: $e');
if (_shouldRetryOnNetworkError(retryCount)) {
return await _retryRequest(
method,
desturl: desturl,
headers: headers,
body: body,
encoding: encoding,
baseUrl: baseUrl,
multipartRequest: multipartRequest,
retryCount: retryCount + 1,
isNetworkError: true,
);
}
throw NetworkException(
'Request timeout: ${e.toString()}',
NetworkErrorType.timeout,
);
} on SocketException catch (e) {
debugPrint('Socket Exception: $e');
if (_shouldRetryOnNetworkError(retryCount)) {
return await _retryRequest(
method,
desturl: desturl,
headers: headers,
body: body,
encoding: encoding,
baseUrl: baseUrl,
multipartRequest: multipartRequest,
retryCount: retryCount + 1,
isNetworkError: true,
);
}
throw NetworkException(
'Connection failed: ${e.message}',
NetworkErrorType.connectionFailed,
);
} catch (error) {
if (error is ApiException) {
debugPrint('Request error: $error');
if (error is ApiException || error is NetworkException) {
rethrow;
} else {
throw ApiException('Network error: $error', 500);
@ -120,6 +213,58 @@ class Https {
}
}
bool _shouldRetry(int statusCode, int retryCount) {
return retryCount < _maxRetries && _retryStatusCodes.contains(statusCode);
}
bool _shouldRetryOnNetworkError(int retryCount) {
return retryCount < _maxRetries;
}
Future<dynamic> _retryRequest(
String method, {
required String desturl,
Map<String, String> headers = const {},
dynamic body,
Encoding? encoding,
String? baseUrl,
http.MultipartRequest? multipartRequest,
required int retryCount,
bool isNetworkError = false,
}) async {
final delay = Duration(
milliseconds: _baseRetryDelay.inMilliseconds * (retryCount * retryCount),
);
debugPrint(
'Retrying request (attempt ${retryCount + 1}/$_maxRetries) after ${delay.inMilliseconds}ms',
);
await Future.delayed(delay);
if (isNetworkError) {
final bool isConnected = await _networkService.checkConnection();
if (!isConnected) {
throw NetworkException(
'No internet connection available after retry',
NetworkErrorType.noConnection,
);
}
}
return await _request(
method,
desturl: desturl,
headers: headers,
body: body,
encoding: encoding,
baseUrl: baseUrl,
multipartRequest: multipartRequest,
checkNetwork: false,
retryCount: retryCount,
);
}
Future<dynamic> get(
String desturl, {
Map<String, String> headers = const {},
@ -243,3 +388,15 @@ class Https {
);
}
}
enum NetworkErrorType { noConnection, connectionFailed, timeout, poor }
class NetworkException implements Exception {
final String message;
final NetworkErrorType type;
NetworkException(this.message, this.type);
@override
String toString() => 'NetworkException: $message';
}

View File

@ -0,0 +1,545 @@
// ignore_for_file: avoid_shadowing_type_parameters
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rijig_mobile/core/api/api_services.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/widget/showmodal.dart';
import 'network_service.dart';
abstract class NetworkAwareWidget extends StatefulWidget {
const NetworkAwareWidget({super.key});
}
abstract class NetworkAwareState<T extends NetworkAwareWidget>
extends State<T> {
final NetworkService _networkService = NetworkService();
StreamSubscription<NetworkStatus>? _networkSubscription;
bool _hasShownNetworkDialog = false;
@override
void initState() {
super.initState();
_initNetworkListener();
}
void _initNetworkListener() {
_networkSubscription = _networkService.networkStatusStream.listen((status) {
if (mounted) {
_handleNetworkStatusChange(status);
}
});
}
void _handleNetworkStatusChange(NetworkStatus status) {
switch (status) {
case NetworkStatus.disconnected:
if (!_hasShownNetworkDialog) {
_showNetworkErrorDialog();
}
onNetworkDisconnected();
break;
case NetworkStatus.connected:
if (_hasShownNetworkDialog) {
_dismissNetworkDialog();
}
onNetworkConnected();
break;
case NetworkStatus.checking:
onNetworkChecking();
break;
case NetworkStatus.poor:
onNetworkPoor();
break;
}
}
void onNetworkConnected() {}
void onNetworkDisconnected() {}
void onNetworkChecking() {}
void onNetworkPoor() {}
Future<T> performNetworkOperation<T>(
Future<T> Function() operation, {
bool showLoadingDialog = true,
String? loadingMessage,
}) async {
if (showLoadingDialog) {
_showLoadingDialog(loadingMessage ?? 'Loading...');
}
try {
final result = await operation();
if (showLoadingDialog && mounted) {
router.pop(context);
}
return result;
} catch (e) {
if (showLoadingDialog && mounted) {
router.pop(context);
}
_handleNetworkException(e);
rethrow;
} finally {
if (showLoadingDialog && mounted) {
router.pop(context);
}
}
}
void _handleNetworkException(e) {
switch (e.type) {
case NetworkErrorType.noConnection:
_showNetworkErrorDialog();
break;
case NetworkErrorType.timeout:
_showTimeoutDialog();
break;
case NetworkErrorType.connectionFailed:
_showConnectionFailedDialog();
break;
case NetworkErrorType.poor:
_showPoorConnectionDialog();
break;
}
}
void _showLoadingDialog(String message) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => LoadingDialog(message: message),
);
}
void _showNetworkErrorDialog() {
_hasShownNetworkDialog = true;
NetworkDialogManager.showNoInternetDialog(
context: context,
onRetry: () async {
router.pop(context);
_hasShownNetworkDialog = false;
await _networkService.checkConnection();
},
onExit: () {
SystemNavigator.pop();
},
);
}
void _showTimeoutDialog() {
NetworkDialogManager.showTimeoutDialog(
context: context,
onRetry: () {
router.pop(context);
},
);
}
void _showConnectionFailedDialog() {
NetworkDialogManager.showConnectionFailedDialog(
context: context,
onRetry: () {
router.pop(context);
},
);
}
void _showPoorConnectionDialog() {
NetworkDialogManager.showPoorConnectionDialog(
context: context,
onContinue: () {
router.pop(context);
},
);
}
void _dismissNetworkDialog() {
if (_hasShownNetworkDialog && mounted) {
router.pop(context);
_hasShownNetworkDialog = false;
}
}
@override
void dispose() {
_networkSubscription?.cancel();
super.dispose();
}
}
class NetworkDialogManager {
static void showNoInternetDialog({
required BuildContext context,
required VoidCallback onRetry,
required VoidCallback onExit,
}) {
CustomModalDialog.show(
context: context,
showCloseIcon: false,
variant: ModalVariant.imageVersion,
title: 'Tidak Ada Koneksi Internet',
content:
'Sepertinya koneksi internet Anda bermasalah. Periksa koneksi WiFi atau data seluler Anda, lalu coba lagi.',
imageAsset: 'assets/image/bad_connection.png',
buttonCount: 2,
button1: ElevatedButton(
onPressed: onRetry,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Coba Lagi'),
),
button2: OutlinedButton(
onPressed: onExit,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Keluar'),
),
);
}
static void showTimeoutDialog({
required BuildContext context,
required VoidCallback onRetry,
}) {
CustomModalDialog.show(
context: context,
showCloseIcon: false,
variant: ModalVariant.imageVersion,
title: 'Koneksi Timeout',
content:
'Permintaan memakan waktu terlalu lama. Periksa koneksi internet Anda dan coba lagi.',
imageAsset: 'assets/images/timeout.png', // Ganti dengan path gambar Anda
buttonCount: 2,
button1: ElevatedButton(
onPressed: onRetry,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Coba Lagi'),
),
button2: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Tutup'),
),
);
}
static void showConnectionFailedDialog({
required BuildContext context,
required VoidCallback onRetry,
}) {
CustomModalDialog.show(
context: context,
showCloseIcon: false,
variant: ModalVariant.imageVersion,
title: 'Koneksi Gagal',
content:
'Tidak dapat terhubung ke server. Pastikan koneksi internet Anda stabil.',
imageAsset:
'assets/images/connection_failed.png', // Ganti dengan path gambar Anda
buttonCount: 2,
button1: ElevatedButton(
onPressed: onRetry,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Coba Lagi'),
),
button2: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Tutup'),
),
);
}
static void showPoorConnectionDialog({
required BuildContext context,
required VoidCallback onContinue,
}) {
CustomModalDialog.show(
context: context,
showCloseIcon: false,
variant: ModalVariant.imageVersion,
title: 'Koneksi Lambat',
content:
'Koneksi internet Anda lambat. Beberapa fitur mungkin tidak berfungsi optimal.',
imageAsset:
'assets/images/poor_connection.png',
buttonCount: 2,
button1: ElevatedButton(
onPressed: onContinue,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Lanjutkan'),
),
button2: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Tutup'),
),
);
}
}
// void _showLoadingDialog(String message) {
// CustomModalDialog.show(
// context: context,
// variant: ModalVariant.imageVersion,
// title: 'Memuat',
// content: message,
// imageAsset: 'assets/images/loading.png', // Ganti dengan path gambar loading Anda
// buttonCount: 0, // Tidak ada button untuk loading dialog
// );
// }
class NetworkErrorDialog extends StatelessWidget {
final String title;
final String message;
final IconData iconData;
final String primaryButtonText;
final VoidCallback onPrimaryPressed;
final String secondaryButtonText;
final VoidCallback onSecondaryPressed;
const NetworkErrorDialog({
super.key,
required this.title,
required this.message,
required this.iconData,
required this.primaryButtonText,
required this.onPrimaryPressed,
required this.secondaryButtonText,
required this.onSecondaryPressed,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: const EdgeInsets.all(24),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
shape: BoxShape.circle,
),
child: Icon(iconData, size: 48, color: Colors.red.shade400),
),
const SizedBox(height: 24),
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
message,
style: const TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: onSecondaryPressed,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(secondaryButtonText),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: onPrimaryPressed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(primaryButtonText),
),
),
],
),
],
),
);
}
}
class LoadingDialog extends StatelessWidget {
final String message;
const LoadingDialog({super.key, required this.message});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: whiteColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class NetworkStatusIndicator extends StatefulWidget {
final Widget child;
final bool showIndicator;
const NetworkStatusIndicator({
super.key,
required this.child,
this.showIndicator = true,
});
@override
State<NetworkStatusIndicator> createState() => _NetworkStatusIndicatorState();
}
class _NetworkStatusIndicatorState extends State<NetworkStatusIndicator> {
final NetworkService _networkService = NetworkService();
StreamSubscription<NetworkStatus>? _subscription;
NetworkStatus _currentStatus = NetworkStatus.connected;
@override
void initState() {
super.initState();
_currentStatus = _networkService.currentStatus;
_subscription = _networkService.networkStatusStream.listen((status) {
if (mounted) {
setState(() {
_currentStatus = status;
});
}
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (widget.showIndicator && _currentStatus != NetworkStatus.connected)
Positioned(
top: MediaQuery.of(context).padding.top,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: _getStatusColor(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_getStatusIcon(), size: 16, color: whiteColor),
const SizedBox(width: 8),
Text(
_getStatusText(),
style: TextStyle(
color: whiteColor,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
);
}
Color _getStatusColor() {
switch (_currentStatus) {
case NetworkStatus.disconnected:
return Colors.red;
case NetworkStatus.checking:
return Colors.orange;
case NetworkStatus.poor:
return Colors.amber;
case NetworkStatus.connected:
return Colors.green;
}
}
IconData _getStatusIcon() {
switch (_currentStatus) {
case NetworkStatus.disconnected:
return Icons.wifi_off;
case NetworkStatus.checking:
return Icons.wifi_find;
case NetworkStatus.poor:
return Icons.signal_wifi_bad;
case NetworkStatus.connected:
return Icons.wifi;
}
}
String _getStatusText() {
switch (_currentStatus) {
case NetworkStatus.disconnected:
return 'Tidak ada koneksi internet';
case NetworkStatus.checking:
return 'Memeriksa koneksi...';
case NetworkStatus.poor:
return 'Koneksi lambat';
case NetworkStatus.connected:
return 'Terhubung';
}
}
}

View File

@ -1,23 +1,58 @@
// ignore_for_file: unrelated_type_equality_checks
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:rijig_mobile/core/network/network_service.dart';
import 'package:rijig_mobile/core/utils/exportimportview.dart';
class NetworkInfo {
final Connectivity _connectivity = Connectivity();
Future<bool> checkConnection() async {
var connectivityResult = await _connectivity.checkConnectivity();
try {
var connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.none) {
return false;
}
try {
final lookupResult = await InternetAddress.lookup('google.com');
return lookupResult.isNotEmpty && lookupResult[0].rawAddress.isNotEmpty;
} catch (_) {
final result = await InternetAddress.lookup(
'google.com',
).timeout(const Duration(seconds: 5));
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
} on SocketException catch (e) {
debugPrint('Network check - Socket exception: $e');
return false;
} on TimeoutException catch (e) {
debugPrint('Network check - Timeout: $e');
return false;
} catch (e) {
debugPrint('Network check - General error: $e');
return false;
}
}
Future<NetworkQuality> checkNetworkQuality() async {
final stopwatch = Stopwatch()..start();
try {
await InternetAddress.lookup(
'google.com',
).timeout(const Duration(seconds: 3));
stopwatch.stop();
final responseTime = stopwatch.elapsedMilliseconds;
if (responseTime < 1000) {
return NetworkQuality.excellent;
} else if (responseTime < 2000) {
return NetworkQuality.good;
} else if (responseTime < 3000) {
return NetworkQuality.fair;
} else {
return NetworkQuality.poor;
}
} catch (e) {
return NetworkQuality.none;
}
}
}

View File

@ -0,0 +1,163 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
enum NetworkStatus { connected, disconnected, checking, poor }
enum NetworkQuality { excellent, good, fair, poor, none }
class NetworkService {
static final NetworkService _instance = NetworkService._internal();
NetworkService._internal();
factory NetworkService() => _instance;
final Connectivity _connectivity = Connectivity();
final StreamController<NetworkStatus> _networkStatusController =
StreamController<NetworkStatus>.broadcast();
NetworkStatus _currentStatus = NetworkStatus.checking;
NetworkQuality _currentQuality = NetworkQuality.none;
Timer? _connectionTimer;
Timer? _qualityTimer;
bool _isInitialized = false;
Stream<NetworkStatus> get networkStatusStream =>
_networkStatusController.stream;
NetworkStatus get currentStatus => _currentStatus;
NetworkQuality get currentQuality => _currentQuality;
bool get isConnected => _currentStatus == NetworkStatus.connected;
Future<void> initialize() async {
if (_isInitialized) return;
_isInitialized = true;
await _checkConnection();
_connectivity.onConnectivityChanged.listen(_handleConnectivityChange);
_startQualityMonitoring();
}
Future<bool> checkConnection({int timeoutSeconds = 10}) async {
_updateStatus(NetworkStatus.checking);
try {
final connectivityResult = await _connectivity.checkConnectivity();
if (connectivityResult.contains(ConnectivityResult.none) ||
connectivityResult.isEmpty) {
_updateStatus(NetworkStatus.disconnected);
return false;
}
final bool canReachInternet = await _canReachInternet(timeoutSeconds);
if (canReachInternet) {
_updateStatus(NetworkStatus.connected);
await _checkNetworkQuality();
return true;
} else {
_updateStatus(NetworkStatus.disconnected);
return false;
}
} catch (e) {
debugPrint('Network check error: $e');
_updateStatus(NetworkStatus.disconnected);
return false;
}
}
Future<void> _checkConnection() async {
await checkConnection();
}
void _handleConnectivityChange(List<ConnectivityResult> results) {
if (results.contains(ConnectivityResult.none) || results.isEmpty) {
_updateStatus(NetworkStatus.disconnected);
_currentQuality = NetworkQuality.none;
} else {
Timer(const Duration(seconds: 1), () async {
await _checkConnection();
});
}
}
Future<bool> _canReachInternet(int timeoutSeconds) async {
final List<String> testHosts = ['google.com', 'cloudflare.com', '8.8.8.8'];
for (String host in testHosts) {
try {
final result = await InternetAddress.lookup(
host,
).timeout(Duration(seconds: timeoutSeconds));
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
return true;
}
} catch (e) {
debugPrint('Failed to reach $host: $e');
continue;
}
}
return false;
}
Future<void> _checkNetworkQuality() async {
try {
final stopwatch = Stopwatch()..start();
await InternetAddress.lookup(
'google.com',
).timeout(const Duration(seconds: 5));
stopwatch.stop();
final responseTime = stopwatch.elapsedMilliseconds;
if (responseTime < 200) {
_currentQuality = NetworkQuality.excellent;
} else if (responseTime < 500) {
_currentQuality = NetworkQuality.good;
} else if (responseTime < 1000) {
_currentQuality = NetworkQuality.fair;
} else {
_currentQuality = NetworkQuality.poor;
}
} catch (e) {
_currentQuality = NetworkQuality.none;
}
}
void _startQualityMonitoring() {
_qualityTimer = Timer.periodic(const Duration(minutes: 2), (_) async {
if (_currentStatus == NetworkStatus.connected) {
await _checkNetworkQuality();
}
});
}
void _updateStatus(NetworkStatus status) {
if (_currentStatus != status) {
_currentStatus = status;
_networkStatusController.add(status);
debugPrint('Network status changed to: $status');
}
}
Duration getTimeoutDuration() {
switch (_currentQuality) {
case NetworkQuality.excellent:
return const Duration(seconds: 10);
case NetworkQuality.good:
return const Duration(seconds: 15);
case NetworkQuality.fair:
return const Duration(seconds: 20);
case NetworkQuality.poor:
return const Duration(seconds: 30);
case NetworkQuality.none:
return const Duration(seconds: 10);
}
}
void dispose() {
_connectionTimer?.cancel();
_qualityTimer?.cancel();
_networkStatusController.close();
}
}

View File

@ -1,10 +1,12 @@
import 'package:rijig_mobile/core/utils/exportimportview.dart';
import 'package:rijig_mobile/features/profil/components/secure_pin_input.dart';
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => SplashScreen()),
GoRoute(path: '/trashview', builder: (context, state) => TestRequestPickScreen()),
GoRoute(path: '/ordersumary', builder: (context, state) => OrderSummaryScreen()),
GoRoute(path: '/pinsecureinput', builder: (context, state) => SecurityCodeScreen()),
GoRoute(
path: '/cmapview',
builder: (context, state) => CollectorRouteMapScreen(),

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:gap/gap.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
class OrderSummaryScreen extends StatefulWidget {
const OrderSummaryScreen({super.key});
@ -9,7 +12,6 @@ class OrderSummaryScreen extends StatefulWidget {
}
class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
// List untuk menyimpan item yang dipilih
List<Map<String, dynamic>> selectedItems = [
{
'name': 'Plastik',
@ -30,35 +32,79 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
];
void _removeItem(int index) {
if (index < 0 || index >= selectedItems.length) return;
final removedItem = selectedItems[index];
setState(() {
selectedItems.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Item berhasil dihapus'),
duration: Duration(seconds: 2),
_showSnackbar('${removedItem['name']} berhasil dihapus');
}
void _clearAllItems() {
if (selectedItems.isEmpty) return;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Hapus Semua Item'),
content: Text(
'Apakah Anda yakin ingin menghapus semua item dari daftar?',
),
actions: [
TextButton(
onPressed: () => router.pop(context),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () {
setState(() {
selectedItems.clear();
});
router.pop(context);
_showSnackbar('Semua item berhasil dihapus');
},
style: ElevatedButton.styleFrom(
backgroundColor: redColor,
foregroundColor: whiteColor,
),
child: Text('Hapus Semua'),
),
],
);
},
);
}
void _incrementQuantity(int index) {
if (index < 0 || index >= selectedItems.length) return;
setState(() {
selectedItems[index]['quantity'] += 2.5;
selectedItems[index]['quantity'] += 0.5;
});
}
void _decrementQuantity(int index) {
if (index < 0 || index >= selectedItems.length) return;
setState(() {
if (selectedItems[index]['quantity'] > 0) {
selectedItems[index]['quantity'] =
(selectedItems[index]['quantity'] - 2.5).clamp(0.0, double.infinity);
final currentQuantity = selectedItems[index]['quantity'] as double;
if (currentQuantity > 0.5) {
selectedItems[index]['quantity'] = (currentQuantity - 0.5).clamp(
0.0,
double.infinity,
);
}
});
}
void _showQuantityDialog(int index) {
TextEditingController controller = TextEditingController(
text: selectedItems[index]['quantity'].toString()
if (index < 0 || index >= selectedItems.length) return;
final TextEditingController controller = TextEditingController(
text: selectedItems[index]['quantity'].toString(),
);
showDialog(
@ -77,21 +123,19 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => router.pop(context),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () {
double? newQuantity = double.tryParse(controller.text);
if (newQuantity != null && newQuantity >= 0) {
final newQuantity = double.tryParse(controller.text);
if (newQuantity != null && newQuantity > 0) {
setState(() {
selectedItems[index]['quantity'] = newQuantity;
});
Navigator.pop(context);
router.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Masukkan angka yang valid')),
);
_showSnackbar('Masukkan angka yang valid (lebih dari 0)');
}
},
child: Text('Simpan'),
@ -102,33 +146,43 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
);
}
void _showSnackbar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: Duration(seconds: 2)),
);
}
double get totalWeight {
return selectedItems.fold(0.0, (sum, item) => sum + item['quantity']);
return selectedItems.fold(
0.0,
(sum, item) => sum + (item['quantity'] as double),
);
}
int get estimatedEarnings {
return selectedItems.fold<int>(0, (sum, item) =>
sum + ((item['price'] as int) * (item['quantity'] as double)).round().toInt());
return selectedItems.fold<int>(
0,
(sum, item) =>
sum + ((item['price'] as int) * (item['quantity'] as double)).round(),
);
}
int get applicationFee {
return 550;
}
int get applicationFee => 550;
int get estimatedIncome {
return estimatedEarnings - applicationFee;
}
int get estimatedIncome => estimatedEarnings - applicationFee;
bool get hasItems => selectedItems.isNotEmpty;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: whiteColor,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
onPressed: () => router.pop(context),
),
title: Text(
'Detail Pesanan',
@ -139,21 +193,111 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
),
centerTitle: true,
actions: [
if (hasItems)
PopupMenuButton<String>(
icon: Icon(Icons.more_vert, color: Colors.black),
onSelected: (value) {
if (value == 'clear_all') {
_clearAllItems();
}
},
itemBuilder:
(BuildContext context) => [
PopupMenuItem<String>(
value: 'clear_all',
child: Row(
children: [
Icon(Icons.clear_all, color: redColor, size: 20),
Gap(8),
Text(
'Hapus Semua',
style: TextStyle(color: redColor),
),
],
),
),
],
),
],
),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
child:
hasItems
? SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Jenis Sampah
_buildItemsSection(),
Gap(20),
_buildEarningsSection(),
],
),
)
: _buildEmptyState(),
),
_buildBottomButton(),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
Icons.delete_outline,
size: 48,
color: Colors.grey.shade400,
),
),
Gap(16),
Text(
'Belum ada item',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
Gap(8),
Text(
'Tambahkan item sampah untuk melanjutkan',
style: TextStyle(fontSize: 14, color: Colors.grey.shade500),
),
Gap(24),
ElevatedButton.icon(
onPressed: () => router.pop(context),
icon: Icon(Icons.add),
label: Text('Tambah Item'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: whiteColor,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
);
}
Widget _buildItemsSection() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: whiteColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@ -166,7 +310,20 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
child: Column(
children: [
Row(
_buildSectionHeader(),
Gap(16),
...selectedItems.asMap().entries.map(
(entry) => _buildItemCard(entry.key, entry.value),
),
Gap(16),
_buildTotalWeight(),
],
),
);
}
Widget _buildSectionHeader() {
return Row(
children: [
Container(
padding: EdgeInsets.all(8),
@ -174,13 +331,9 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.delete_outline,
color: Colors.orange,
size: 20,
child: Icon(Icons.delete_outline, color: Colors.orange, size: 20),
),
),
SizedBox(width: 12),
Gap(12),
Text(
'Jenis Sampah',
style: TextStyle(
@ -191,15 +344,12 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
Spacer(),
TextButton(
onPressed: () {
// Navigate back to selection screen
Navigator.pop(context);
},
onPressed: () => router.pop(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, color: Colors.blue, size: 16),
SizedBox(width: 4),
Gap(4),
Text(
'Tambah',
style: TextStyle(
@ -212,25 +362,21 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
),
],
),
SizedBox(height: 16),
// List Items dengan Slidable
...selectedItems.asMap().entries.map((entry) {
int index = entry.key;
Map<String, dynamic> item = entry.value;
);
}
Widget _buildItemCard(int index, Map<String, dynamic> item) {
return Padding(
padding: EdgeInsets.only(bottom: 12),
child: Slidable(
key: ValueKey(item['name']),
key: ValueKey('${item['name']}_$index'),
endActionPane: ActionPane(
motion: ScrollMotion(),
children: [
SlidableAction(
onPressed: (context) => _removeItem(index),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
backgroundColor: redColor,
foregroundColor: whiteColor,
icon: Icons.delete,
label: 'Hapus',
borderRadius: BorderRadius.circular(12),
@ -246,24 +392,31 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
child: Row(
children: [
// Icon
Container(
_buildItemIcon(item),
Gap(12),
_buildItemInfo(item),
_buildQuantityControls(index, item),
],
),
),
),
);
}
Widget _buildItemIcon(Map<String, dynamic> item) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: item['backgroundColor'],
borderRadius: BorderRadius.circular(20),
),
child: Icon(
item['icon'],
color: item['iconColor'],
size: 20,
),
),
SizedBox(width: 12),
child: Icon(item['icon'], color: item['iconColor'], size: 20),
);
}
// Item info
Expanded(
Widget _buildItemInfo(Map<String, dynamic> item) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -275,12 +428,9 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
color: Colors.black,
),
),
SizedBox(height: 4),
Gap(4),
Container(
padding: EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(8),
@ -288,103 +438,79 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
child: Text(
'Rp ${item['price']}/kg',
style: TextStyle(
color: Colors.white,
color: whiteColor,
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 4),
// Delete icon placeholder (always there for consistent height)
SizedBox(
height: 16,
child: Icon(
Icons.delete_outline,
color: Colors.red,
size: 12,
),
),
],
),
),
);
}
// Quantity controls
Row(
Widget _buildQuantityControls(int index, Map<String, dynamic> item) {
return Row(
children: [
// Decrease button
GestureDetector(
_buildQuantityButton(
icon: Icons.remove,
onTap: () => _decrementQuantity(index),
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey.shade300),
backgroundColor: whiteColor,
iconColor: Colors.grey.shade600,
),
child: Icon(
Icons.remove,
color: Colors.grey.shade600,
size: 16,
),
),
),
SizedBox(width: 8),
// Quantity display (clickable)
Gap(8),
GestureDetector(
onTap: () => _showQuantityDialog(index),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
color: whiteColor,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
'${item['quantity'].toString().replaceAll('.0', '')} kg',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
'${_formatQuantity(item['quantity'])} kg',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
),
),
),
SizedBox(width: 8),
// Increase button
GestureDetector(
Gap(8),
_buildQuantityButton(
icon: Icons.add,
onTap: () => _incrementQuantity(index),
backgroundColor: Colors.blue,
iconColor: whiteColor,
),
],
);
}
Widget _buildQuantityButton({
required IconData icon,
required VoidCallback onTap,
required Color backgroundColor,
required Color iconColor,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 28,
height: 28,
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.blue,
color: backgroundColor,
borderRadius: BorderRadius.circular(6),
border:
backgroundColor == whiteColor
? Border.all(color: Colors.grey.shade300)
: null,
),
child: Icon(
Icons.add,
color: Colors.white,
size: 16,
),
),
),
],
),
],
),
),
child: Icon(icon, color: iconColor, size: 16),
),
);
}),
}
SizedBox(height: 16),
// Total Weight
Container(
Widget _buildTotalWeight() {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
@ -398,13 +524,9 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.scale,
color: Colors.grey.shade700,
size: 16,
child: Icon(Icons.scale, color: Colors.grey.shade700, size: 16),
),
),
SizedBox(width: 12),
Gap(12),
Text(
'Berat total',
style: TextStyle(
@ -415,7 +537,7 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
Spacer(),
Text(
'${totalWeight.toString().replaceAll('.0', '')} kg',
'${_formatQuantity(totalWeight)} kg',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@ -424,19 +546,15 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
],
),
),
],
),
),
);
}
SizedBox(height: 20),
// Perkiraan Pendapatan
Container(
Widget _buildEarningsSection() {
return Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: whiteColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@ -458,13 +576,9 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.folder,
color: Colors.orange,
size: 20,
child: Icon(Icons.folder, color: Colors.orange, size: 20),
),
),
SizedBox(width: 12),
Gap(12),
Text(
'Perkiraan Pendapatan',
style: TextStyle(
@ -475,26 +589,20 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
],
),
SizedBox(height: 16),
// Estimasi pembayaran
Gap(16),
_buildIncomeRow(
'Estimasi pembayaran',
estimatedEarnings,
Colors.orange,
),
SizedBox(height: 8),
// Biaya jasa aplikasi
Gap(8),
_buildIncomeRow(
'Biaya jasa aplikasi',
applicationFee,
Colors.orange,
showInfo: true,
),
SizedBox(height: 8),
// Estimasi pendapatan
Gap(8),
_buildIncomeRow(
'Estimasi pendapatan',
estimatedIncome,
@ -503,61 +611,16 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
],
),
),
],
),
),
),
// Bottom Continue Button
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha:0.2),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: selectedItems.isNotEmpty ? () {
// Handle continue action
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lanjut ke proses selanjutnya'),
),
);
} : null,
style: ElevatedButton.styleFrom(
backgroundColor: selectedItems.isNotEmpty ? Colors.blue : Colors.grey,
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Lanjut',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
);
}
Widget _buildIncomeRow(String title, int amount, Color color, {bool showInfo = false, bool isBold = false}) {
Widget _buildIncomeRow(
String title,
int amount,
Color color, {
bool showInfo = false,
bool isBold = false,
}) {
return Row(
children: [
Container(
@ -566,13 +629,9 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
color: color,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.currency_exchange,
color: Colors.white,
size: 12,
child: Icon(Icons.currency_exchange, color: whiteColor, size: 12),
),
),
SizedBox(width: 8),
Gap(8),
Expanded(
child: Row(
children: [
@ -585,18 +644,14 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
),
),
if (showInfo) ...[
SizedBox(width: 4),
Icon(
Icons.info_outline,
color: Colors.blue,
size: 16,
),
Gap(4),
Icon(Icons.info_outline, color: Colors.blue, size: 16),
],
],
),
),
Text(
amount.toString(),
'Rp ${_formatCurrency(amount)}',
style: TextStyle(
fontSize: 14,
fontWeight: isBold ? FontWeight.w600 : FontWeight.w500,
@ -606,4 +661,61 @@ class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
],
);
}
Widget _buildBottomButton() {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: whiteColor,
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.2),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
hasItems
? () {
_showSnackbar('Lanjut ke proses selanjutnya');
// Handle continue action
}
: () => router.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: hasItems ? Colors.blue : Colors.grey.shade400,
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
hasItems ? 'Lanjut' : 'Tambah Item',
style: TextStyle(
color: whiteColor,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
String _formatQuantity(double quantity) {
return quantity % 1 == 0
? quantity.toInt().toString()
: quantity.toString();
}
String _formatCurrency(int amount) {
return amount.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
}

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/widget/appbar.dart';
class TestRequestPickScreen extends StatefulWidget {
const TestRequestPickScreen({super.key});
@ -143,40 +145,41 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
backgroundColor: whiteColor,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
title: Row(
children: [
Icon(Icons.location_on, color: Colors.grey),
SizedBox(width: 8),
Text(
'Purbalingga',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Spacer(),
TextButton(
onPressed: () {},
child: Text('Ganti', style: TextStyle(color: Colors.blue)),
),
],
),
),
appBar: CustomAppBar(judul: "Pilih Sampah"),
// appBar: AppBar(
// backgroundColor: whiteColor,
// elevation: 0,
// leading: IconButton(
// icon: Icon(Icons.arrow_back, color: Colors.black),
// onPressed: () => Navigator.pop(context),
// ),
// title: Row(
// children: [
// Icon(Icons.location_on, color: Colors.grey),
// Gap(8),
// Text(
// 'Purbalingga',
// style: TextStyle(
// color: Colors.black,
// fontSize: 16,
// fontWeight: FontWeight.w500,
// ),
// ),
// Spacer(),
// TextButton(
// onPressed: () {},
// child: Text('Ganti', style: TextStyle(color: Colors.blue)),
// ),
// ],
// ),
// ),
body: Column(
children: [
// Header
Container(
width: double.infinity,
padding: EdgeInsets.all(16),
padding: PaddingCustom().paddingAll(16),
color: whiteColor,
child: Text(
'Pilih Sampah',
@ -191,7 +194,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
// List Items
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16),
padding: PaddingCustom().paddingAll(16),
itemCount: quantities.keys.length,
itemBuilder: (context, index) {
String itemName = quantities.keys.elementAt(index);
@ -200,7 +203,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
return Container(
margin: EdgeInsets.only(bottom: 12),
padding: EdgeInsets.all(16),
padding: PaddingCustom().paddingAll(16),
decoration: BoxDecoration(
color: whiteColor,
borderRadius: BorderRadius.circular(12),
@ -229,7 +232,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
size: 24,
),
),
SizedBox(width: 16),
Gap(16),
// Item info
Expanded(
@ -245,7 +248,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
color: Colors.black,
),
),
SizedBox(height: 4),
Gap(4),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
@ -265,7 +268,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
),
),
// Show delete icon when quantity > 0 (below price)
SizedBox(height: 4),
Gap(4),
SizedBox(
height: 24, // Fixed height untuk consistency
child:
@ -273,7 +276,9 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
? GestureDetector(
onTap: () => _resetQuantity(itemName),
child: Container(
padding: EdgeInsets.all(4),
padding: PaddingCustom().paddingAll(
4,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(
@ -314,7 +319,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
),
),
),
SizedBox(width: 12),
Gap(12),
// Quantity display (clickable)
GestureDetector(
@ -339,8 +344,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
),
),
),
SizedBox(width: 12),
Gap(12),
// Increase button
GestureDetector(
onTap: () => _incrementQuantity(itemName),
@ -395,7 +399,8 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
// Bottom summary
Container(
padding: EdgeInsets.all(16),
// padding: PaddingCustom().paddingAll(16),
padding: PaddingCustom().paddingAll(16),
decoration: BoxDecoration(
color: whiteColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
@ -422,7 +427,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
color: Colors.grey.shade700,
),
),
SizedBox(height: 4),
Gap(4),
Row(
children: [
Text(
@ -452,7 +457,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
),
],
),
SizedBox(height: 4),
Gap(4),
Text(
'Minimum total berat 3kg',
style: TextStyle(fontSize: 12, color: Colors.red),
@ -460,18 +465,12 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
],
),
),
SizedBox(width: 16),
Gap(16),
ElevatedButton(
onPressed:
totalWeight >= 3
? () {
// Handle continue action
router.push('/ordersumary');
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text('Lanjut ke proses selanjutnya'),
// ),
// );
}
: null,
style: ElevatedButton.styleFrom(

View File

@ -1,5 +1,4 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:math' as math;
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
@ -71,16 +70,25 @@ class _HomeScreenState extends State<HomeScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(onPressed: ()=>router.push('/trashview'), icon: Icon(
IconButton(
onPressed: () => router.push('/trashview'),
icon: Icon(
Iconsax.notification_copy,
color: primaryColor,
),),
// Icon(
// Iconsax.notification_copy,
// color: primaryColor,
// ),
),
),
Gap(10),
Icon(Iconsax.message_copy, color: primaryColor),
IconButton(
onPressed: () {
debugPrint('message tapped');
},
icon: Icon(
Iconsax.message_copy,
color: primaryColor,
),
),
],
),
],
@ -108,9 +116,8 @@ class _HomeScreenState extends State<HomeScreen> {
CustomModalDialog.show(
context: context,
variant: ModalVariant.textVersion,
title: 'Hapus Akun',
content:
'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
title: 'Belum Tersedia',
content: 'Maaf, fitur ini belum tersedia',
buttonCount: 2,
button1: CardButtonOne(
textButton: "Ya, Hapus",
@ -181,7 +188,6 @@ class _HomeScreenState extends State<HomeScreen> {
ArticleScreen(),
],
),
// Gap(20),
],
),
),

View File

@ -1,6 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:rijig_mobile/core/network/network_info.dart';
import 'package:rijig_mobile/core/network/network_aware_widgets.dart';
import 'package:rijig_mobile/core/network/network_service.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/storage/expired_token.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
@ -9,21 +13,48 @@ class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
SplashScreenState createState() => SplashScreenState();
UpdatedSplashScreenState createState() => UpdatedSplashScreenState();
}
class SplashScreenState extends State<SplashScreen> {
class UpdatedSplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
bool _isCheckingConnection = true;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
final NetworkService _networkService = NetworkService();
@override
void initState() {
super.initState();
_checkNetworkConnection();
_checkLoginStatus();
_initializeAnimations();
_startAppInitialization();
}
Future<void> _checkNetworkConnection() async {
bool isConnected = await NetworkInfo().checkConnection();
void _initializeAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_animationController.forward();
}
Future<void> _startAppInitialization() async {
// Initialize network service
await _networkService.initialize();
await Future.delayed(const Duration(milliseconds: 3000));
await _checkNetworkAndProceed();
}
Future<void> _checkNetworkAndProceed() async {
setState(() {
_isCheckingConnection = true;
});
bool isConnected = await _networkService.checkConnection();
setState(() {
_isCheckingConnection = false;
@ -33,57 +64,85 @@ class SplashScreenState extends State<SplashScreen> {
_showNoInternetDialog();
return;
}
await _checkLoginStatus();
}
Future<void> _checkLoginStatus() async {
try {
bool expired = await isTokenExpired();
if (expired) {
debugPrint("tets expired");
debugPrint("Token expired - redirecting to onboarding");
router.go("/onboarding");
} else {
debugPrint("test not expired");
debugPrint("Token valid - redirecting to navigation");
router.go("/navigasi");
}
} catch (e) {
debugPrint("Login status check error: $e");
router.go("/onboarding");
}
}
void _showNoInternetDialog() {
showDialog(
NetworkDialogManager.showNoInternetDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('No Internet'),
content: Text('Mohon periksa koneksi internet anda, dan coba lagi'),
actions: [
TextButton(
onPressed: () {
onRetry: () {
Navigator.of(context).pop();
_checkNetworkAndProceed();
},
onExit: () {
SystemNavigator.pop();
},
child: Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
const String assetName = 'assets/icon/logorijig.svg';
return Scaffold(
backgroundColor: primaryColor,
body: Center(
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: SvgPicture.asset(assetName, height: 120),
),
void dispose() {
_animationController.dispose();
super.dispose();
}
if (_isCheckingConnection)
Align(
alignment: Alignment.bottomCenter,
child: CircularProgressIndicator(color: whiteColor,),
@override
Widget build(BuildContext context) {
return NetworkStatusIndicator(
child: Scaffold(
backgroundColor: primaryColor,
body: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset('assets/icon/logorijig.svg', height: 120),
const SizedBox(height: 60),
if (_isCheckingConnection) ...[
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: whiteColor,
strokeWidth: 2.5,
),
),
const SizedBox(height: 16),
Text(
'Memeriksa koneksi...',
style: TextStyle(
color: whiteColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
],
],
),
),
);
},
),
),
);

View File

@ -1,12 +1,10 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/auth/presentation/viewmodel/logout_vmod.dart';
import 'package:rijig_mobile/widget/buttoncard.dart';
import 'package:rijig_mobile/widget/custom_bottom_sheet.dart';
import 'package:toastification/toastification.dart';
class ButtonLogout extends StatefulWidget {
const ButtonLogout({super.key});
@ -26,58 +24,25 @@ class _ButtonLogoutState extends State<ButtonLogout> {
textButton: viewModel.isLoading ? 'Logging out...' : 'Logout',
fontSized: 16,
colorText: whiteColor,
color: redColor,
color: primaryColor,
borderRadius: 10,
horizontal: double.infinity,
vertical: 50,
onTap:
() => CustomBottomSheet.show(
context: context,
title: "Logout Sekarang?",
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Yakin ingin logout dari akun ini?"),
// tambahan konten
],
),
button1: CardButtonOne(
textButton: "Logout",
onTap: () {},
fontSized: 14,
colorText: Colors.white,
color: Colors.red,
borderRadius: 10,
horizontal: double.infinity,
vertical: 50,
loadingTrue: false,
usingRow: false,
),
button2: CardButtonOne(
textButton: "Batal",
onTap: () => router.pop(),
fontSized: 14,
colorText: Colors.red,
color: Colors.white,
borderRadius: 10,
horizontal: double.infinity,
vertical: 50,
loadingTrue: false,
usingRow: false,
),
),
// onTap: () async {
// await viewModel.logout();
onTap: () async {
await viewModel.logout();
// if (viewModel.errorMessage == null) {
// router.go("/login");
// } else {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text(viewModel.errorMessage!)),
// );
// }
// },
if (viewModel.errorMessage == null) {
router.go("/login");
} else {
toastification.show(
type: ToastificationType.error,
title: Text("Belum berhsail logout"),
autoCloseDuration: const Duration(seconds: 3),
showProgressBar: true,
);
}
},
loadingTrue: viewModel.isLoading,
usingRow: false,
),

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:iconsax_flutter/iconsax_flutter.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/profil/components/profile_list_tile.dart';
import 'package:rijig_mobile/widget/buttoncard.dart';
import 'package:rijig_mobile/widget/custom_bottom_sheet.dart';
class ProfileMenuOptions extends StatelessWidget {
const ProfileMenuOptions({super.key});
@ -9,11 +12,11 @@ class ProfileMenuOptions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: PaddingCustom().paddingAll(7),
padding: PaddingCustom().paddingAll(10),
decoration: BoxDecoration(
color: whiteColor,
border: Border.all(color: greyColor),
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
@ -21,7 +24,9 @@ class ProfileMenuOptions extends StatelessWidget {
title: 'Ubah Pin',
iconColor: primaryColor,
icon: Iconsax.wallet,
onTap: () {},
onTap: () {
router.push('/pinsecureinput');
},
),
Divider(thickness: 0.7, color: greyColor),
ProfileListTile(
@ -44,6 +49,48 @@ class ProfileMenuOptions extends StatelessWidget {
iconColor: primaryColor,
onTap: () {},
),
Divider(thickness: 0.7, color: greyColor),
ProfileListTile(
title: 'Keluar',
icon: Iconsax.logout,
iconColor: redColor,
onTap:
() => CustomBottomSheet.show(
context: context,
title: "Logout Sekarang?",
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Yakin ingin logout dari akun ini?"),
// tambahan konten
],
),
button1: CardButtonOne(
textButton: "Logout",
onTap: () {},
fontSized: 14,
colorText: Colors.white,
color: Colors.red,
borderRadius: 10,
horizontal: double.infinity,
vertical: 50,
loadingTrue: false,
usingRow: false,
),
button2: CardButtonOne(
textButton: "Batal",
onTap: () => router.pop(),
fontSized: 14,
colorText: Colors.red,
color: Colors.white,
borderRadius: 10,
horizontal: double.infinity,
vertical: 50,
loadingTrue: false,
usingRow: false,
),
),
),
],
),
);

View File

@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
class SecurityCodeScreen extends StatefulWidget {
const SecurityCodeScreen({super.key});
@override
State<SecurityCodeScreen> createState() => _SecurityCodeScreenState();
}
class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
TextEditingController textEditingController = TextEditingController();
String currentText = "";
@override
void dispose() {
textEditingController.dispose();
super.dispose();
}
void _onKeypadTap(String value) {
if (value == "delete") {
if (currentText.isNotEmpty) {
currentText = currentText.substring(0, currentText.length - 1);
textEditingController.text = currentText;
}
} else if (currentText.length < 6) {
currentText += value;
textEditingController.text = currentText;
}
if (currentText.length == 6) {
_validatePin();
}
}
void _validatePin() {
debugPrint("PIN entered: $currentText");
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("PIN: $currentText")));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [secondaryColor, primaryColor],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
const SizedBox(height: 60),
Text(
'Masukkan Security Code Kamu',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: whiteColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 60),
PinCodeTextField(
appContext: context,
length: 6,
controller: textEditingController,
readOnly: true,
obscureText: true,
obscuringCharacter: '.',
animationType: AnimationType.fade,
pinTheme: PinTheme(
shape: PinCodeFieldShape.circle,
borderRadius: BorderRadius.circular(25),
fieldHeight: 20,
fieldWidth: 20,
activeFillColor: whiteColor,
inactiveFillColor: whiteColor.withValues(alpha: 0.3),
selectedFillColor: whiteColor.withValues(alpha: 0.7),
activeColor: Colors.transparent,
inactiveColor: Colors.transparent,
selectedColor: Colors.transparent,
),
animationDuration: const Duration(milliseconds: 300),
backgroundColor: Colors.transparent,
enableActiveFill: true,
onCompleted: (v) {
_validatePin();
},
onChanged: (value) {
currentText = value;
},
),
const SizedBox(height: 40),
Text(
'Lupa Security Code',
style: TextStyle(
fontSize: 16,
color: whiteColor,
decoration: TextDecoration.underline,
decorationColor: whiteColor,
),
),
const Spacer(),
_buildKeypad(),
const SizedBox(height: 40),
],
),
),
),
),
);
}
Widget _buildKeypad() {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildKeypadButton('1'),
_buildKeypadButton('2'),
_buildKeypadButton('3'),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildKeypadButton('4'),
_buildKeypadButton('5'),
_buildKeypadButton('6'),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildKeypadButton('7'),
_buildKeypadButton('8'),
_buildKeypadButton('9'),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(width: 80),
_buildKeypadButton('0'),
_buildDeleteButton(),
],
),
],
),
);
}
Widget _buildKeypadButton(String number) {
return GestureDetector(
onTap: () => _onKeypadTap(number),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: whiteColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
border: Border.all(
color: whiteColor.withValues(alpha: 0.3),
width: 1,
),
),
child: Center(
child: Text(
number,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w400,
color: whiteColor,
),
),
),
),
);
}
Widget _buildDeleteButton() {
return GestureDetector(
onTap: () => _onKeypadTap('delete'),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: whiteColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
border: Border.all(
color: whiteColor.withValues(alpha: 0.3),
width: 1,
),
),
child: Center(
child: Icon(Icons.backspace_outlined, color: whiteColor, size: 24),
),
),
);
}
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/profil/components/clip_path.dart';
import 'package:rijig_mobile/features/profil/components/logout_button.dart';
import 'package:rijig_mobile/features/profil/components/profile_menu_option.dart';
import 'package:rijig_mobile/widget/buttoncard.dart';
import 'package:rijig_mobile/widget/custom_bottom_sheet.dart';
class ProfilScreen extends StatefulWidget {
const ProfilScreen({super.key});
@ -17,80 +18,306 @@ class _ProfilScreenState extends State<ProfilScreen> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: whiteColor,
body: SafeArea(
child: Stack(
body: SingleChildScrollView(
child: Column(
children: [
ClipPath(
clipper: ClipPathClass(),
child: Container(
height: 180,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
primaryColor,
secondaryColor,
], // Ganti dengan warna gradien pilihan Anda
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
Positioned(
top: 60,
left: 20,
right: 20,
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
Gap(50),
Container(
width: double.infinity,
color: whiteColor,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
spreadRadius: 1,
blurRadius: 8,
padding: PaddingCustom().paddingHorizontalVertical(20, 30),
child: Column(
children: [
Stack(
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.grey.shade200,
width: 3,
),
),
child: CircleAvatar(
backgroundColor: primaryColor,
radius: 47,
backgroundImage: NetworkImage(
'https://plus.unsplash.com/premium_vector-1731922571914-9d0161b5e7b7?q=80&w=1760&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
),
),
),
// Positioned(
// bottom: 5,
// right: 5,
// child: Container(
// width: 24,
// height: 24,
// decoration: const BoxDecoration(
// color: Color(0xFF3B82F6),
// shape: BoxShape.circle,
// ),
// child: Icon(Icons.check, color: whiteColor, size: 14),
// ),
// ),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Fahmi Kurniawan'),
SizedBox(height: 10),
Text('+62878774527342'),
SizedBox(height: 20),
Gap(16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Tervalidasi',
style: TextStyle(color: Colors.green),
'Fahmi Kurniawan',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: Colors.black,
),
Text('edit', style: TextStyle(color: Colors.blue)),
],
),
Gap(4),
Text(
'+6287874527342',
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
),
],
),
),
),
SingleChildScrollView(
child: Container(
margin: EdgeInsets.only(top: 245),
child: Padding(
padding: PaddingCustom().paddingHorizontal(20),
child: Column(
children: [ProfileMenuOptions(), Gap(30), ButtonLogout()],
Gap(20),
_buildMenuSection([
_MenuItemData(
icon: Icons.person,
iconColor: const Color(0xFF3B82F6),
title: 'Profil',
subtitle: 'Edit profil, dan lain lain..',
),
_MenuItemData(
icon: Icons.pin,
iconColor: const Color(0xFF10B981),
title: 'Ubah Pin',
subtitle: 'Ubah pin anda disini',
),
_MenuItemData(
icon: Icons.location_pin,
iconColor: const Color(0xFF3B82F6),
title: 'Alamat',
subtitle: 'kelola daftar alamat anda disini',
),
_MenuItemData(
icon: Icons.help_center,
iconColor: const Color(0xFF3B82F6),
title: 'Bantuan',
subtitle: 'Butuh bantuan tentang aplikasi?',
),
_MenuItemData(
icon: Icons.thumb_up,
iconColor: const Color(0xFF6B7280),
title: 'Ulasan',
subtitle: 'Penilaian anda berarti bagi kami',
),
_MenuItemData(
icon: Icons.logout,
iconColor: const Color(0xFF3B82F6),
title: 'Keluar',
subtitle: 'Keluar aplikasi atau ganti akun',
),
]),
Gap(100),
],
),
),
);
}
Widget _buildMenuSection(List<_MenuItemData> items) {
return Container(
margin: const EdgeInsets.only(top: 20, bottom: 80, left: 20, right: 20),
padding: PaddingCustom().paddingAll(7),
decoration: BoxDecoration(
color: whiteColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children:
items.asMap().entries.map((entry) {
int index = entry.key;
_MenuItemData item = entry.value;
bool isLast = index == items.length - 1;
return _buildMenuItem(
item.icon,
item.iconColor,
item.title,
item.subtitle,
showDivider: !isLast,
onTap: () => _handleMenuTap(item.title),
);
}).toList(),
),
);
}
Widget _buildMenuItem(
IconData icon,
Color iconColor,
String title,
String subtitle, {
bool showDivider = true,
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: iconColor, size: 22),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
Gap(4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
height: 1.3,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey.shade400,
),
],
),
if (showDivider) ...[
Gap(20),
Divider(height: 1, color: Colors.grey.shade200),
],
],
),
),
);
}
void _handleMenuTap(String menuTitle) {
debugPrint('Tapped on: $menuTitle');
switch (menuTitle) {
case 'Profil':
debugPrint('Profil');
break;
case 'Ubah Pin':
debugPrint('Uabh Pin');
break;
case 'Alamat':
debugPrint('Alamat');
break;
case 'Bantuan':
debugPrint('Bantuan');
break;
case 'Ulasan':
debugPrint('Ulasan');
break;
case 'Keluar':
// _showLogoutDialog();
CustomBottomSheet.show(
context: context,
title: "Logout Sekarang?",
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Yakin ingin logout dari akun ini?"),
// tambahan konten
],
),
button1: ButtonLogout(),
// button1: CardButtonOne(
// textButton: "Logout",
// onTap: () {},
// fontSized: 14,
// colorText: Colors.white,
// color: Colors.red,
// borderRadius: 10,
// horizontal: double.infinity,
// vertical: 50,
// loadingTrue: false,
// usingRow: false,
// ),
button2: CardButtonOne(
textButton: "Gak jadi..",
onTap: () => router.pop(),
fontSized: 14,
colorText: primaryColor,
color: whiteColor,
borderRadius: 10,
horizontal: double.infinity,
vertical: 50,
loadingTrue: false,
usingRow: false,
),
);
break;
default:
debugPrint('Routing tidak dikenali: $menuTitle');
}
}
}
class _MenuItemData {
final IconData icon;
final Color iconColor;
final String title;
final String subtitle;
_MenuItemData({
required this.icon,
required this.iconColor,
required this.title,
required this.subtitle,
});
}

View File

@ -16,6 +16,9 @@ class CustomModalDialog extends StatelessWidget {
final Widget? button1;
final Widget? button2;
// Parameter boolean untuk mengontrol tampilan close icon
final bool showCloseIcon;
const CustomModalDialog({
super.key,
required this.variant,
@ -25,6 +28,7 @@ class CustomModalDialog extends StatelessWidget {
this.buttonCount = 0,
this.button1,
this.button2,
this.showCloseIcon = true, // Default true untuk backward compatibility
});
static void show({
@ -36,6 +40,7 @@ class CustomModalDialog extends StatelessWidget {
int buttonCount = 0,
Widget? button1,
Widget? button2,
bool showCloseIcon = true, // Parameter baru di static method
}) {
showGeneralDialog(
context: context,
@ -64,6 +69,7 @@ class CustomModalDialog extends StatelessWidget {
buttonCount: buttonCount,
button1: button1,
button2: button2,
showCloseIcon: showCloseIcon, // Pass parameter ke constructor
),
),
),
@ -122,6 +128,8 @@ class CustomModalDialog extends StatelessWidget {
clipBehavior: Clip.none,
children: [
modalContent,
// Kondisional untuk menampilkan close icon
if (showCloseIcon)
Positioned(
top: -15,
right: 5,