feat&refact&imp: code modal internet check and prepare UI statement
This commit is contained in:
parent
42cd53752b
commit
4db92f3d22
|
@ -1,11 +1,13 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:rijig_mobile/core/api/api_exception.dart';
|
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/expired_token.dart';
|
||||||
import 'package:rijig_mobile/core/storage/secure_storage.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? _baseUrl = dotenv.env["BASE_URL"];
|
||||||
final String? _apiKey = dotenv.env["API_KEY"];
|
final String? _apiKey = dotenv.env["API_KEY"];
|
||||||
final SecureStorage _secureStorage = SecureStorage();
|
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 {
|
Future<Map<String, String>> _getHeaders() async {
|
||||||
String? token = await _secureStorage.readSecureData('token');
|
String? token = await _secureStorage.readSecureData('token');
|
||||||
|
@ -51,52 +58,78 @@ class Https {
|
||||||
Encoding? encoding,
|
Encoding? encoding,
|
||||||
String? baseUrl,
|
String? baseUrl,
|
||||||
http.MultipartRequest? multipartRequest,
|
http.MultipartRequest? multipartRequest,
|
||||||
|
bool checkNetwork = true,
|
||||||
|
int retryCount = 0,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (checkNetwork) {
|
||||||
|
final bool isConnected = await _networkService.checkConnection();
|
||||||
|
if (!isConnected) {
|
||||||
|
throw NetworkException(
|
||||||
|
'No internet connection available',
|
||||||
|
NetworkErrorType.noConnection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final requestHeaders = await _getHeaders();
|
final requestHeaders = await _getHeaders();
|
||||||
String url = "${baseUrl ?? _baseUrl}$desturl";
|
String url = "${baseUrl ?? _baseUrl}$desturl";
|
||||||
debugPrint("url $url");
|
debugPrint("Request URL: $url");
|
||||||
|
debugPrint("Network Quality: ${_networkService.currentQuality}");
|
||||||
|
|
||||||
|
final timeout = _networkService.getTimeoutDuration();
|
||||||
|
|
||||||
http.Response response;
|
http.Response response;
|
||||||
try {
|
try {
|
||||||
if (multipartRequest != null) {
|
if (multipartRequest != null) {
|
||||||
response = await multipartRequest.send().then(
|
response = await multipartRequest
|
||||||
(response) => http.Response.fromStream(response),
|
.send()
|
||||||
);
|
.timeout(timeout)
|
||||||
|
.then((response) => http.Response.fromStream(response));
|
||||||
} else {
|
} else {
|
||||||
switch (method.toLowerCase()) {
|
switch (method.toLowerCase()) {
|
||||||
case 'get':
|
case 'get':
|
||||||
response = await http.get(Uri.parse(url), headers: requestHeaders);
|
response = await http
|
||||||
|
.get(Uri.parse(url), headers: requestHeaders)
|
||||||
|
.timeout(timeout);
|
||||||
break;
|
break;
|
||||||
case 'post':
|
case 'post':
|
||||||
response = await http.post(
|
response = await http
|
||||||
Uri.parse(url),
|
.post(
|
||||||
body: jsonEncode(body),
|
Uri.parse(url),
|
||||||
headers: requestHeaders,
|
body: jsonEncode(body),
|
||||||
encoding: encoding,
|
headers: requestHeaders,
|
||||||
);
|
encoding: encoding,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
break;
|
break;
|
||||||
case 'put':
|
case 'put':
|
||||||
response = await http.put(
|
response = await http
|
||||||
Uri.parse(url),
|
.put(
|
||||||
body: jsonEncode(body),
|
Uri.parse(url),
|
||||||
headers: requestHeaders,
|
body: jsonEncode(body),
|
||||||
encoding: encoding,
|
headers: requestHeaders,
|
||||||
);
|
encoding: encoding,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
response = await http.delete(
|
response = await http
|
||||||
Uri.parse(url),
|
.delete(
|
||||||
body: jsonEncode(body),
|
Uri.parse(url),
|
||||||
headers: requestHeaders,
|
body: jsonEncode(body),
|
||||||
);
|
headers: requestHeaders,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
break;
|
break;
|
||||||
case 'patch':
|
case 'patch':
|
||||||
response = await http.patch(
|
response = await http
|
||||||
Uri.parse(url),
|
.patch(
|
||||||
body: jsonEncode(body),
|
Uri.parse(url),
|
||||||
headers: requestHeaders,
|
body: jsonEncode(body),
|
||||||
encoding: encoding,
|
headers: requestHeaders,
|
||||||
);
|
encoding: encoding,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw ApiException('Unsupported HTTP method: $method', 405);
|
throw ApiException('Unsupported HTTP method: $method', 405);
|
||||||
|
@ -104,15 +137,75 @@ class Https {
|
||||||
}
|
}
|
||||||
|
|
||||||
final int statusCode = response.statusCode;
|
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) {
|
if (statusCode < 200 || statusCode >= 400) {
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
'Error during HTTP $method request: ${response.body}',
|
'Error during HTTP $method request: ${response.body}',
|
||||||
statusCode,
|
statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonDecode(response.body);
|
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) {
|
} catch (error) {
|
||||||
if (error is ApiException) {
|
debugPrint('Request error: $error');
|
||||||
|
|
||||||
|
if (error is ApiException || error is NetworkException) {
|
||||||
rethrow;
|
rethrow;
|
||||||
} else {
|
} else {
|
||||||
throw ApiException('Network error: $error', 500);
|
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(
|
Future<dynamic> get(
|
||||||
String desturl, {
|
String desturl, {
|
||||||
Map<String, String> headers = const {},
|
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';
|
||||||
|
}
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,58 @@
|
||||||
// ignore_for_file: unrelated_type_equality_checks
|
// ignore_for_file: unrelated_type_equality_checks
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
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 {
|
class NetworkInfo {
|
||||||
final Connectivity _connectivity = Connectivity();
|
|
||||||
|
|
||||||
Future<bool> checkConnection() async {
|
Future<bool> checkConnection() async {
|
||||||
var connectivityResult = await _connectivity.checkConnectivity();
|
|
||||||
|
|
||||||
if (connectivityResult == ConnectivityResult.none) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final lookupResult = await InternetAddress.lookup('google.com');
|
var connectivityResult = await Connectivity().checkConnectivity();
|
||||||
return lookupResult.isNotEmpty && lookupResult[0].rawAddress.isNotEmpty;
|
if (connectivityResult == ConnectivityResult.none) {
|
||||||
} catch (_) {
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import 'package:rijig_mobile/core/utils/exportimportview.dart';
|
import 'package:rijig_mobile/core/utils/exportimportview.dart';
|
||||||
|
import 'package:rijig_mobile/features/profil/components/secure_pin_input.dart';
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: '/', builder: (context, state) => SplashScreen()),
|
GoRoute(path: '/', builder: (context, state) => SplashScreen()),
|
||||||
GoRoute(path: '/trashview', builder: (context, state) => TestRequestPickScreen()),
|
GoRoute(path: '/trashview', builder: (context, state) => TestRequestPickScreen()),
|
||||||
GoRoute(path: '/ordersumary', builder: (context, state) => OrderSummaryScreen()),
|
GoRoute(path: '/ordersumary', builder: (context, state) => OrderSummaryScreen()),
|
||||||
|
GoRoute(path: '/pinsecureinput', builder: (context, state) => SecurityCodeScreen()),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/cmapview',
|
path: '/cmapview',
|
||||||
builder: (context, state) => CollectorRouteMapScreen(),
|
builder: (context, state) => CollectorRouteMapScreen(),
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:rijig_mobile/core/router.dart';
|
import 'package:rijig_mobile/core/router.dart';
|
||||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||||
|
import 'package:rijig_mobile/widget/appbar.dart';
|
||||||
|
|
||||||
class TestRequestPickScreen extends StatefulWidget {
|
class TestRequestPickScreen extends StatefulWidget {
|
||||||
const TestRequestPickScreen({super.key});
|
const TestRequestPickScreen({super.key});
|
||||||
|
@ -143,40 +145,41 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey.shade50,
|
backgroundColor: whiteColor,
|
||||||
appBar: AppBar(
|
appBar: CustomAppBar(judul: "Pilih Sampah"),
|
||||||
backgroundColor: whiteColor,
|
// appBar: AppBar(
|
||||||
elevation: 0,
|
// backgroundColor: whiteColor,
|
||||||
leading: IconButton(
|
// elevation: 0,
|
||||||
icon: Icon(Icons.arrow_back, color: Colors.black),
|
// leading: IconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
// icon: Icon(Icons.arrow_back, color: Colors.black),
|
||||||
),
|
// onPressed: () => Navigator.pop(context),
|
||||||
title: Row(
|
// ),
|
||||||
children: [
|
// title: Row(
|
||||||
Icon(Icons.location_on, color: Colors.grey),
|
// children: [
|
||||||
SizedBox(width: 8),
|
// Icon(Icons.location_on, color: Colors.grey),
|
||||||
Text(
|
// Gap(8),
|
||||||
'Purbalingga',
|
// Text(
|
||||||
style: TextStyle(
|
// 'Purbalingga',
|
||||||
color: Colors.black,
|
// style: TextStyle(
|
||||||
fontSize: 16,
|
// color: Colors.black,
|
||||||
fontWeight: FontWeight.w500,
|
// fontSize: 16,
|
||||||
),
|
// fontWeight: FontWeight.w500,
|
||||||
),
|
// ),
|
||||||
Spacer(),
|
// ),
|
||||||
TextButton(
|
// Spacer(),
|
||||||
onPressed: () {},
|
// TextButton(
|
||||||
child: Text('Ganti', style: TextStyle(color: Colors.blue)),
|
// onPressed: () {},
|
||||||
),
|
// child: Text('Ganti', style: TextStyle(color: Colors.blue)),
|
||||||
],
|
// ),
|
||||||
),
|
// ],
|
||||||
),
|
// ),
|
||||||
|
// ),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: EdgeInsets.all(16),
|
padding: PaddingCustom().paddingAll(16),
|
||||||
color: whiteColor,
|
color: whiteColor,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Pilih Sampah',
|
'Pilih Sampah',
|
||||||
|
@ -191,7 +194,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
// List Items
|
// List Items
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.all(16),
|
padding: PaddingCustom().paddingAll(16),
|
||||||
itemCount: quantities.keys.length,
|
itemCount: quantities.keys.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
String itemName = quantities.keys.elementAt(index);
|
String itemName = quantities.keys.elementAt(index);
|
||||||
|
@ -200,7 +203,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(bottom: 12),
|
margin: EdgeInsets.only(bottom: 12),
|
||||||
padding: EdgeInsets.all(16),
|
padding: PaddingCustom().paddingAll(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: whiteColor,
|
color: whiteColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
@ -229,7 +232,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 16),
|
Gap(16),
|
||||||
|
|
||||||
// Item info
|
// Item info
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -245,7 +248,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
Gap(4),
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
|
@ -265,7 +268,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Show delete icon when quantity > 0 (below price)
|
// Show delete icon when quantity > 0 (below price)
|
||||||
SizedBox(height: 4),
|
Gap(4),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 24, // Fixed height untuk consistency
|
height: 24, // Fixed height untuk consistency
|
||||||
child:
|
child:
|
||||||
|
@ -273,7 +276,9 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
onTap: () => _resetQuantity(itemName),
|
onTap: () => _resetQuantity(itemName),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(4),
|
padding: PaddingCustom().paddingAll(
|
||||||
|
4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red.shade50,
|
color: Colors.red.shade50,
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
@ -314,7 +319,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
Gap(12),
|
||||||
|
|
||||||
// Quantity display (clickable)
|
// Quantity display (clickable)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
@ -339,8 +344,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
Gap(12),
|
||||||
|
|
||||||
// Increase button
|
// Increase button
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _incrementQuantity(itemName),
|
onTap: () => _incrementQuantity(itemName),
|
||||||
|
@ -395,7 +399,8 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
|
|
||||||
// Bottom summary
|
// Bottom summary
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(16),
|
// padding: PaddingCustom().paddingAll(16),
|
||||||
|
padding: PaddingCustom().paddingAll(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: whiteColor,
|
color: whiteColor,
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
@ -422,7 +427,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
Gap(4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
@ -452,7 +457,7 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
Gap(4),
|
||||||
Text(
|
Text(
|
||||||
'Minimum total berat 3kg',
|
'Minimum total berat 3kg',
|
||||||
style: TextStyle(fontSize: 12, color: Colors.red),
|
style: TextStyle(fontSize: 12, color: Colors.red),
|
||||||
|
@ -460,18 +465,12 @@ class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 16),
|
Gap(16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
totalWeight >= 3
|
totalWeight >= 3
|
||||||
? () {
|
? () {
|
||||||
// Handle continue action
|
|
||||||
router.push('/ordersumary');
|
router.push('/ordersumary');
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
// SnackBar(
|
|
||||||
// content: Text('Lanjut ke proses selanjutnya'),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
|
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
|
||||||
|
@ -71,16 +70,25 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
IconButton(onPressed: ()=>router.push('/trashview'), icon: Icon(
|
IconButton(
|
||||||
Iconsax.notification_copy,
|
onPressed: () => router.push('/trashview'),
|
||||||
color: primaryColor,
|
icon: Icon(
|
||||||
),),
|
Iconsax.notification_copy,
|
||||||
// Icon(
|
color: primaryColor,
|
||||||
// Iconsax.notification_copy,
|
),
|
||||||
// color: primaryColor,
|
),
|
||||||
// ),
|
|
||||||
Gap(10),
|
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(
|
CustomModalDialog.show(
|
||||||
context: context,
|
context: context,
|
||||||
variant: ModalVariant.textVersion,
|
variant: ModalVariant.textVersion,
|
||||||
title: 'Hapus Akun',
|
title: 'Belum Tersedia',
|
||||||
content:
|
content: 'Maaf, fitur ini belum tersedia',
|
||||||
'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
|
|
||||||
buttonCount: 2,
|
buttonCount: 2,
|
||||||
button1: CardButtonOne(
|
button1: CardButtonOne(
|
||||||
textButton: "Ya, Hapus",
|
textButton: "Ya, Hapus",
|
||||||
|
@ -181,7 +188,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
ArticleScreen(),
|
ArticleScreen(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Gap(20),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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/router.dart';
|
||||||
import 'package:rijig_mobile/core/storage/expired_token.dart';
|
import 'package:rijig_mobile/core/storage/expired_token.dart';
|
||||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||||
|
@ -9,21 +13,48 @@ class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SplashScreenState createState() => SplashScreenState();
|
UpdatedSplashScreenState createState() => UpdatedSplashScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class SplashScreenState extends State<SplashScreen> {
|
class UpdatedSplashScreenState extends State<SplashScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
bool _isCheckingConnection = true;
|
bool _isCheckingConnection = true;
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
final NetworkService _networkService = NetworkService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_checkNetworkConnection();
|
_initializeAnimations();
|
||||||
_checkLoginStatus();
|
_startAppInitialization();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkNetworkConnection() async {
|
void _initializeAnimations() {
|
||||||
bool isConnected = await NetworkInfo().checkConnection();
|
_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(() {
|
setState(() {
|
||||||
_isCheckingConnection = false;
|
_isCheckingConnection = false;
|
||||||
|
@ -33,59 +64,87 @@ class SplashScreenState extends State<SplashScreen> {
|
||||||
_showNoInternetDialog();
|
_showNoInternetDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _checkLoginStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkLoginStatus() async {
|
Future<void> _checkLoginStatus() async {
|
||||||
bool expired = await isTokenExpired();
|
try {
|
||||||
if (expired) {
|
bool expired = await isTokenExpired();
|
||||||
debugPrint("tets expired");
|
if (expired) {
|
||||||
|
debugPrint("Token expired - redirecting to onboarding");
|
||||||
|
router.go("/onboarding");
|
||||||
|
} else {
|
||||||
|
debugPrint("Token valid - redirecting to navigation");
|
||||||
|
router.go("/navigasi");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Login status check error: $e");
|
||||||
router.go("/onboarding");
|
router.go("/onboarding");
|
||||||
} else {
|
|
||||||
debugPrint("test not expired");
|
|
||||||
router.go("/navigasi");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showNoInternetDialog() {
|
void _showNoInternetDialog() {
|
||||||
showDialog(
|
NetworkDialogManager.showNoInternetDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
onRetry: () {
|
||||||
(context) => AlertDialog(
|
Navigator.of(context).pop();
|
||||||
title: Text('No Internet'),
|
_checkNetworkAndProceed();
|
||||||
content: Text('Mohon periksa koneksi internet anda, dan coba lagi'),
|
},
|
||||||
actions: [
|
onExit: () {
|
||||||
TextButton(
|
SystemNavigator.pop();
|
||||||
onPressed: () {
|
},
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
void dispose() {
|
||||||
const String assetName = 'assets/icon/logorijig.svg';
|
_animationController.dispose();
|
||||||
return Scaffold(
|
super.dispose();
|
||||||
backgroundColor: primaryColor,
|
}
|
||||||
body: Center(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: SvgPicture.asset(assetName, height: 120),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (_isCheckingConnection)
|
@override
|
||||||
Align(
|
Widget build(BuildContext context) {
|
||||||
alignment: Alignment.bottomCenter,
|
return NetworkStatusIndicator(
|
||||||
child: CircularProgressIndicator(color: whiteColor,),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
// ignore_for_file: use_build_context_synchronously
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:rijig_mobile/core/router.dart';
|
import 'package:rijig_mobile/core/router.dart';
|
||||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||||
import 'package:rijig_mobile/features/auth/presentation/viewmodel/logout_vmod.dart';
|
import 'package:rijig_mobile/features/auth/presentation/viewmodel/logout_vmod.dart';
|
||||||
import 'package:rijig_mobile/widget/buttoncard.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 {
|
class ButtonLogout extends StatefulWidget {
|
||||||
const ButtonLogout({super.key});
|
const ButtonLogout({super.key});
|
||||||
|
@ -26,58 +24,25 @@ class _ButtonLogoutState extends State<ButtonLogout> {
|
||||||
textButton: viewModel.isLoading ? 'Logging out...' : 'Logout',
|
textButton: viewModel.isLoading ? 'Logging out...' : 'Logout',
|
||||||
fontSized: 16,
|
fontSized: 16,
|
||||||
colorText: whiteColor,
|
colorText: whiteColor,
|
||||||
color: redColor,
|
color: primaryColor,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
horizontal: double.infinity,
|
horizontal: double.infinity,
|
||||||
vertical: 50,
|
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 {
|
onTap: () async {
|
||||||
// await viewModel.logout();
|
await viewModel.logout();
|
||||||
|
|
||||||
// if (viewModel.errorMessage == null) {
|
if (viewModel.errorMessage == null) {
|
||||||
// router.go("/login");
|
router.go("/login");
|
||||||
// } else {
|
} else {
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
toastification.show(
|
||||||
// SnackBar(content: Text(viewModel.errorMessage!)),
|
type: ToastificationType.error,
|
||||||
// );
|
title: Text("Belum berhsail logout"),
|
||||||
// }
|
autoCloseDuration: const Duration(seconds: 3),
|
||||||
// },
|
showProgressBar: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
loadingTrue: viewModel.isLoading,
|
loadingTrue: viewModel.isLoading,
|
||||||
usingRow: false,
|
usingRow: false,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:iconsax_flutter/iconsax_flutter.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/core/utils/guide.dart';
|
||||||
import 'package:rijig_mobile/features/profil/components/profile_list_tile.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 {
|
class ProfileMenuOptions extends StatelessWidget {
|
||||||
const ProfileMenuOptions({super.key});
|
const ProfileMenuOptions({super.key});
|
||||||
|
@ -9,11 +12,11 @@ class ProfileMenuOptions extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: PaddingCustom().paddingAll(7),
|
padding: PaddingCustom().paddingAll(10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: whiteColor,
|
color: whiteColor,
|
||||||
border: Border.all(color: greyColor),
|
border: Border.all(color: greyColor),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -21,7 +24,9 @@ class ProfileMenuOptions extends StatelessWidget {
|
||||||
title: 'Ubah Pin',
|
title: 'Ubah Pin',
|
||||||
iconColor: primaryColor,
|
iconColor: primaryColor,
|
||||||
icon: Iconsax.wallet,
|
icon: Iconsax.wallet,
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
router.push('/pinsecureinput');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Divider(thickness: 0.7, color: greyColor),
|
Divider(thickness: 0.7, color: greyColor),
|
||||||
ProfileListTile(
|
ProfileListTile(
|
||||||
|
@ -44,6 +49,48 @@ class ProfileMenuOptions extends StatelessWidget {
|
||||||
iconColor: primaryColor,
|
iconColor: primaryColor,
|
||||||
onTap: () {},
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.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/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/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 {
|
class ProfilScreen extends StatefulWidget {
|
||||||
const ProfilScreen({super.key});
|
const ProfilScreen({super.key});
|
||||||
|
@ -17,80 +18,306 @@ class _ProfilScreenState extends State<ProfilScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: whiteColor,
|
backgroundColor: whiteColor,
|
||||||
body: SafeArea(
|
body: SingleChildScrollView(
|
||||||
child: Stack(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ClipPath(
|
Gap(50),
|
||||||
clipper: ClipPathClass(),
|
Container(
|
||||||
child: Container(
|
width: double.infinity,
|
||||||
height: 180,
|
color: whiteColor,
|
||||||
width: MediaQuery.of(context).size.width,
|
padding: PaddingCustom().paddingHorizontalVertical(20, 30),
|
||||||
decoration: BoxDecoration(
|
child: Column(
|
||||||
gradient: LinearGradient(
|
children: [
|
||||||
colors: [
|
Stack(
|
||||||
primaryColor,
|
children: [
|
||||||
secondaryColor,
|
Container(
|
||||||
], // Ganti dengan warna gradien pilihan Anda
|
width: 100,
|
||||||
begin: Alignment.topLeft,
|
height: 100,
|
||||||
end: Alignment.bottomRight,
|
decoration: BoxDecoration(
|
||||||
),
|
shape: BoxShape.circle,
|
||||||
),
|
border: Border.all(
|
||||||
),
|
color: Colors.grey.shade200,
|
||||||
),
|
width: 3,
|
||||||
Positioned(
|
),
|
||||||
top: 60,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: whiteColor,
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withValues(alpha: 0.3),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text('Fahmi Kurniawan'),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Text('+62878774527342'),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Tervalidasi',
|
|
||||||
style: TextStyle(color: Colors.green),
|
|
||||||
),
|
),
|
||||||
Text('edit', style: TextStyle(color: Colors.blue)),
|
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),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Gap(16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Fahmi Kurniawan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Gap(4),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'+6287874527342',
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SingleChildScrollView(
|
Gap(20),
|
||||||
child: Container(
|
|
||||||
margin: EdgeInsets.only(top: 245),
|
_buildMenuSection([
|
||||||
child: Padding(
|
_MenuItemData(
|
||||||
padding: PaddingCustom().paddingHorizontal(20),
|
icon: Icons.person,
|
||||||
child: Column(
|
iconColor: const Color(0xFF3B82F6),
|
||||||
children: [ProfileMenuOptions(), Gap(30), ButtonLogout()],
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ class CustomModalDialog extends StatelessWidget {
|
||||||
final int buttonCount;
|
final int buttonCount;
|
||||||
final Widget? button1;
|
final Widget? button1;
|
||||||
final Widget? button2;
|
final Widget? button2;
|
||||||
|
|
||||||
|
// Parameter boolean untuk mengontrol tampilan close icon
|
||||||
|
final bool showCloseIcon;
|
||||||
|
|
||||||
const CustomModalDialog({
|
const CustomModalDialog({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -25,6 +28,7 @@ class CustomModalDialog extends StatelessWidget {
|
||||||
this.buttonCount = 0,
|
this.buttonCount = 0,
|
||||||
this.button1,
|
this.button1,
|
||||||
this.button2,
|
this.button2,
|
||||||
|
this.showCloseIcon = true, // Default true untuk backward compatibility
|
||||||
});
|
});
|
||||||
|
|
||||||
static void show({
|
static void show({
|
||||||
|
@ -36,6 +40,7 @@ class CustomModalDialog extends StatelessWidget {
|
||||||
int buttonCount = 0,
|
int buttonCount = 0,
|
||||||
Widget? button1,
|
Widget? button1,
|
||||||
Widget? button2,
|
Widget? button2,
|
||||||
|
bool showCloseIcon = true, // Parameter baru di static method
|
||||||
}) {
|
}) {
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -64,6 +69,7 @@ class CustomModalDialog extends StatelessWidget {
|
||||||
buttonCount: buttonCount,
|
buttonCount: buttonCount,
|
||||||
button1: button1,
|
button1: button1,
|
||||||
button2: button2,
|
button2: button2,
|
||||||
|
showCloseIcon: showCloseIcon, // Pass parameter ke constructor
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -122,19 +128,21 @@ class CustomModalDialog extends StatelessWidget {
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
modalContent,
|
modalContent,
|
||||||
Positioned(
|
// Kondisional untuk menampilkan close icon
|
||||||
top: -15,
|
if (showCloseIcon)
|
||||||
right: 5,
|
Positioned(
|
||||||
child: GestureDetector(
|
top: -15,
|
||||||
onTap: () => router.pop(),
|
right: 5,
|
||||||
child: CircleAvatar(
|
child: GestureDetector(
|
||||||
radius: 18,
|
onTap: () => router.pop(),
|
||||||
backgroundColor: whiteColor,
|
child: CircleAvatar(
|
||||||
child: Icon(Icons.close, color: blackNavyColor),
|
radius: 18,
|
||||||
|
backgroundColor: whiteColor,
|
||||||
|
child: Icon(Icons.close, color: blackNavyColor),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue