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: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';
|
||||
}
|
|
@ -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
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/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(),
|
||||
|
|
|
@ -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]}.',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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: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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue