connect api to login feature, and add vibration feature when an object is detected
This commit is contained in:
parent
7d7059d9f8
commit
cb75b69a3c
|
|
@ -2,6 +2,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,71 @@
|
||||||
package com.example.smart_vision_assist
|
package com.example.smart_vision_assist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity: FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
private val vibrationChannel = "smart_vision_assist/vibration"
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
|
MethodChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
vibrationChannel
|
||||||
|
).setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"vibrate" -> {
|
||||||
|
val milliseconds = call.argument<Int>("milliseconds") ?: 7000
|
||||||
|
result.success(vibrate(milliseconds.toLong()))
|
||||||
|
}
|
||||||
|
"stop" -> {
|
||||||
|
stopVibration()
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vibrate(milliseconds: Long): Boolean {
|
||||||
|
val vibrator = getVibrator() ?: return false
|
||||||
|
|
||||||
|
if (!vibrator.hasVibrator()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val effect = VibrationEffect.createOneShot(
|
||||||
|
milliseconds,
|
||||||
|
VibrationEffect.DEFAULT_AMPLITUDE
|
||||||
|
)
|
||||||
|
vibrator.vibrate(effect)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
vibrator.vibrate(milliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopVibration() {
|
||||||
|
getVibrator()?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVibrator(): Vibrator? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val vibratorManager =
|
||||||
|
getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager
|
||||||
|
vibratorManager?.defaultVibrator
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
class LoginRequest {
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const LoginRequest({
|
||||||
|
required this.email,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
class LoginResponse {
|
||||||
|
final String status;
|
||||||
|
final String message;
|
||||||
|
final String? token;
|
||||||
|
|
||||||
|
const LoginResponse({
|
||||||
|
required this.status,
|
||||||
|
required this.message,
|
||||||
|
this.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LoginResponse(
|
||||||
|
status: json["status"]?.toString() ?? "error",
|
||||||
|
message: json["message"]?.toString() ?? "",
|
||||||
|
token: json["token"]?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isSuccess => status.toLowerCase() == "success";
|
||||||
|
}
|
||||||
|
|
@ -5,10 +5,12 @@ import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../services/tts_service.dart';
|
import '../services/tts_service.dart';
|
||||||
|
import '../services/vibration_service.dart';
|
||||||
import '../services/yolo_detection_service.dart';
|
import '../services/yolo_detection_service.dart';
|
||||||
|
import 'login_screen.dart';
|
||||||
|
|
||||||
class CameraScreen extends StatefulWidget {
|
class CameraScreen extends StatefulWidget {
|
||||||
const CameraScreen({super.key});
|
const CameraScreen({super.key});
|
||||||
|
|
@ -23,6 +25,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
|
|
||||||
final YoloDetectionService _detectionService = YoloDetectionService();
|
final YoloDetectionService _detectionService = YoloDetectionService();
|
||||||
final TTSService _tts = TTSService();
|
final TTSService _tts = TTSService();
|
||||||
|
final VibrationService _vibrationService = VibrationService();
|
||||||
|
|
||||||
bool isRecording = false;
|
bool isRecording = false;
|
||||||
bool isDetecting = false;
|
bool isDetecting = false;
|
||||||
|
|
@ -31,7 +34,8 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
Size? detectionFrameSize;
|
Size? detectionFrameSize;
|
||||||
Timer? _detectionTimer;
|
Timer? _detectionTimer;
|
||||||
Timer? _vibrationTimer;
|
Timer? _vibrationTimer;
|
||||||
bool _isVibrating = false;
|
|
||||||
|
bool _isLoggingOut = false;
|
||||||
String _lastSpokenMessage = "";
|
String _lastSpokenMessage = "";
|
||||||
DateTime? _lastSpokenAt;
|
DateTime? _lastSpokenAt;
|
||||||
|
|
||||||
|
|
@ -101,7 +105,8 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
_detectionTimer = null;
|
_detectionTimer = null;
|
||||||
_vibrationTimer?.cancel();
|
_vibrationTimer?.cancel();
|
||||||
_vibrationTimer = null;
|
_vibrationTimer = null;
|
||||||
_isVibrating = false;
|
await _vibrationService.stop();
|
||||||
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -114,6 +119,43 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
await _tts.speak("Deteksi dihentikan");
|
await _tts.speak("Deteksi dihentikan");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
if (_isLoggingOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoggingOut = true;
|
||||||
|
|
||||||
|
if (isRecording ||
|
||||||
|
isDetecting ||
|
||||||
|
_detectionTimer != null ||
|
||||||
|
_vibrationTimer != null) {
|
||||||
|
await stopRecording();
|
||||||
|
} else {
|
||||||
|
_detectionTimer?.cancel();
|
||||||
|
_detectionTimer = null;
|
||||||
|
_vibrationTimer?.cancel();
|
||||||
|
_vibrationTimer = null;
|
||||||
|
await _vibrationService.stop();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove("auth_token");
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const LoginScreen(),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _detectCurrentFrame() async {
|
Future<void> _detectCurrentFrame() async {
|
||||||
final CameraController? cameraController = controller;
|
final CameraController? cameraController = controller;
|
||||||
if (cameraController == null ||
|
if (cameraController == null ||
|
||||||
|
|
@ -196,7 +238,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_vibrateForTwoSeconds();
|
await _vibrateForSevenSeconds();
|
||||||
|
|
||||||
final Map<String, int> objectCounts = {};
|
final Map<String, int> objectCounts = {};
|
||||||
for (final YoloDetection object in response.objects) {
|
for (final YoloDetection object in response.objects) {
|
||||||
|
|
@ -212,29 +254,16 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
await _speakOnce("Terdeteksi $objectSummary");
|
await _speakOnce("Terdeteksi $objectSummary");
|
||||||
}
|
}
|
||||||
|
|
||||||
void _vibrateForTwoSeconds() {
|
Future<void> _vibrateForSevenSeconds() async {
|
||||||
if (_isVibrating) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isVibrating = true;
|
|
||||||
int vibrationCount = 0;
|
|
||||||
const int maxVibrationCount = 8;
|
|
||||||
const Duration vibrationInterval = Duration(milliseconds: 250);
|
|
||||||
|
|
||||||
HapticFeedback.vibrate();
|
|
||||||
vibrationCount++;
|
|
||||||
|
|
||||||
_vibrationTimer?.cancel();
|
_vibrationTimer?.cancel();
|
||||||
_vibrationTimer = Timer.periodic(vibrationInterval, (Timer timer) {
|
|
||||||
if (!mounted || !isRecording || vibrationCount >= maxVibrationCount) {
|
const Duration vibrationDuration = Duration(seconds: 7);
|
||||||
timer.cancel();
|
|
||||||
_isVibrating = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HapticFeedback.vibrate();
|
await _vibrationService.vibrateFor(vibrationDuration);
|
||||||
vibrationCount++;
|
|
||||||
|
_vibrationTimer = Timer(vibrationDuration, () {
|
||||||
|
_vibrationTimer = null;
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,6 +285,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_detectionTimer?.cancel();
|
_detectionTimer?.cancel();
|
||||||
_vibrationTimer?.cancel();
|
_vibrationTimer?.cancel();
|
||||||
|
unawaited(_vibrationService.stop());
|
||||||
controller?.dispose();
|
controller?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -291,94 +321,108 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (controller == null || !controller!.value.isInitialized) {
|
if (controller == null || !controller!.value.isInitialized) {
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
body: Center(
|
canPop: false,
|
||||||
child: statusMessage == "Tekan tombol rekam untuk mulai deteksi"
|
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||||
? const CircularProgressIndicator()
|
if (!didPop) {
|
||||||
: Padding(
|
await logout();
|
||||||
padding: const EdgeInsets.all(24),
|
}
|
||||||
child: Text(
|
},
|
||||||
statusMessage,
|
child: Scaffold(
|
||||||
textAlign: TextAlign.center,
|
body: Center(
|
||||||
style: const TextStyle(fontSize: 16),
|
child: statusMessage == "Tekan tombol rekam untuk mulai deteksi"
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
statusMessage,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
body: Stack(
|
canPop: false,
|
||||||
children: [
|
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||||
Positioned.fill(
|
if (!didPop) {
|
||||||
child: buildCameraPreview(),
|
await logout();
|
||||||
),
|
}
|
||||||
Positioned.fill(
|
},
|
||||||
child: IgnorePointer(
|
child: Scaffold(
|
||||||
child: _BoundingBoxOverlay(
|
body: Stack(
|
||||||
imageSize: detectionFrameSize,
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: buildCameraPreview(),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: _BoundingBoxOverlay(
|
||||||
|
imageSize: detectionFrameSize,
|
||||||
|
objects: detectedObjects,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 48,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: _DetectionStatusPanel(
|
||||||
|
isRecording: isRecording,
|
||||||
|
isDetecting: isDetecting,
|
||||||
|
statusMessage: statusMessage,
|
||||||
objects: detectedObjects,
|
objects: detectedObjects,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
Positioned(
|
bottom: 40,
|
||||||
top: 48,
|
right: 30,
|
||||||
left: 16,
|
child: Semantics(
|
||||||
right: 16,
|
label: "Kembali",
|
||||||
child: _DetectionStatusPanel(
|
button: true,
|
||||||
isRecording: isRecording,
|
child: FloatingActionButton(
|
||||||
isDetecting: isDetecting,
|
backgroundColor: Colors.black,
|
||||||
statusMessage: statusMessage,
|
onPressed: logout,
|
||||||
objects: detectedObjects,
|
child: const Icon(Icons.arrow_back),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 40,
|
|
||||||
right: 30,
|
|
||||||
child: Semantics(
|
|
||||||
label: "Kembali",
|
|
||||||
button: true,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.arrow_back),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
Positioned(
|
bottom: 30,
|
||||||
bottom: 30,
|
left: MediaQuery.of(context).size.width / 2 - 35,
|
||||||
left: MediaQuery.of(context).size.width / 2 - 35,
|
child: Semantics(
|
||||||
child: Semantics(
|
label: isRecording ? "Stop rekam" : "Mulai rekam",
|
||||||
label: isRecording ? "Stop rekam" : "Mulai rekam",
|
button: true,
|
||||||
button: true,
|
child: GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: () {
|
||||||
onTap: () {
|
if (isRecording) {
|
||||||
if (isRecording) {
|
stopRecording();
|
||||||
stopRecording();
|
} else {
|
||||||
} else {
|
startRecording();
|
||||||
startRecording();
|
}
|
||||||
}
|
},
|
||||||
},
|
child: Container(
|
||||||
child: Container(
|
width: 70,
|
||||||
width: 70,
|
height: 70,
|
||||||
height: 70,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
shape: BoxShape.circle,
|
||||||
shape: BoxShape.circle,
|
color: isRecording ? Colors.red : Colors.white,
|
||||||
color: isRecording ? Colors.red : Colors.white,
|
border: Border.all(color: Colors.grey, width: 4),
|
||||||
border: Border.all(color: Colors.grey, width: 4),
|
),
|
||||||
),
|
child: Icon(
|
||||||
child: Icon(
|
isRecording ? Icons.stop : Icons.circle,
|
||||||
isRecording ? Icons.stop : Icons.circle,
|
color: isRecording ? Colors.white : Colors.red,
|
||||||
color: isRecording ? Colors.white : Colors.red,
|
size: 30,
|
||||||
size: 30,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,105 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:smart_vision_assist/screens/camera_screen.dart';
|
||||||
import '../widgets/custom_button.dart';
|
import '../widgets/custom_button.dart';
|
||||||
import '../widgets/custom_textfield.dart';
|
import '../widgets/custom_textfield.dart';
|
||||||
import '../services/tts_service.dart';
|
import '../services/tts_service.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
|
import '../models/login_request.dart';
|
||||||
import '../utils/colors.dart';
|
import '../utils/colors.dart';
|
||||||
import 'home_screen.dart';
|
|
||||||
import 'signup_screen.dart';
|
import 'signup_screen.dart';
|
||||||
import 'forgot_password_screen.dart';
|
import 'forgot_password_screen.dart';
|
||||||
import 'camera_screen.dart';
|
|
||||||
|
|
||||||
class LoginScreen extends StatelessWidget {
|
|
||||||
|
class LoginScreen extends StatefulWidget {
|
||||||
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginScreen> createState() => _LoginScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginScreenState extends State<LoginScreen> {
|
||||||
final TextEditingController emailController = TextEditingController();
|
final TextEditingController emailController = TextEditingController();
|
||||||
final TextEditingController passwordController = TextEditingController();
|
final TextEditingController passwordController = TextEditingController();
|
||||||
|
|
||||||
final TTSService tts = TTSService();
|
final TTSService tts = TTSService();
|
||||||
final AuthService auth = AuthService();
|
final AuthService auth = AuthService();
|
||||||
|
|
||||||
LoginScreen({super.key});
|
bool isLoading = false;
|
||||||
|
|
||||||
// 🔐 LOGIN
|
@override
|
||||||
void handleLogin(BuildContext context) {
|
void initState() {
|
||||||
bool success = auth.login(
|
super.initState();
|
||||||
emailController.text,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
passwordController.text,
|
tts.speak("Silakan login");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
emailController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleLogin(BuildContext context) async {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String email = emailController.text.trim();
|
||||||
|
final String password = passwordController.text;
|
||||||
|
|
||||||
|
if (email.isEmpty) {
|
||||||
|
_showMessage("Email wajib diisi");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.contains("@")) {
|
||||||
|
_showMessage("Email tidak valid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.isEmpty) {
|
||||||
|
_showMessage("Password wajib diisi");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final response = await auth.login(
|
||||||
|
LoginRequest(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (!context.mounted) {
|
||||||
tts.speak("Login berhasil");
|
return;
|
||||||
Navigator.push(
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.isSuccess && response.token != null) {
|
||||||
|
_showMessage(
|
||||||
|
response.message.isNotEmpty ? response.message : "Login berhasil",
|
||||||
|
);
|
||||||
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
MaterialPageRoute(builder: (_) => const CameraScreen()),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tts.speak("Login gagal, periksa email dan password");
|
_showMessage(
|
||||||
|
response.message.isNotEmpty
|
||||||
|
? response.message
|
||||||
|
: "Login gagal, periksa email dan password",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔑 FORGOT PASSWORD
|
|
||||||
void handleForgotPassword(BuildContext context) {
|
void handleForgotPassword(BuildContext context) {
|
||||||
tts.speak("Menu lupa password");
|
tts.speak("Menu lupa password");
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
|
@ -45,94 +108,96 @@ class LoginScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 SIGN UP
|
|
||||||
void handleSignUp(BuildContext context) {
|
void handleSignUp(BuildContext context) {
|
||||||
tts.speak("Menu pendaftaran akun");
|
tts.speak("Menu pendaftaran akun");
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => SignUpScreen()),
|
MaterialPageRoute(builder: (_) => const SignUpScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMessage(String message) {
|
||||||
|
tts.speak(message);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
tts.speak("Silakan login");
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
body: Padding(
|
body: SafeArea(
|
||||||
padding: const EdgeInsets.all(20),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(20),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
// 🔷 TITLE
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Object Detector Assist",
|
"Object Detector Assist",
|
||||||
style: TextStyle(
|
textAlign: TextAlign.center,
|
||||||
color: AppColors.text,
|
style: TextStyle(
|
||||||
fontSize: 24,
|
color: AppColors.text,
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 24,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 40),
|
|
||||||
|
|
||||||
// 📧 EMAIL
|
|
||||||
CustomTextField(
|
|
||||||
label: "Email",
|
|
||||||
controller: emailController,
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 20),
|
|
||||||
|
|
||||||
// 🔒 PASSWORD
|
|
||||||
CustomTextField(
|
|
||||||
label: "Password",
|
|
||||||
controller: passwordController,
|
|
||||||
obscure: true,
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 30),
|
|
||||||
|
|
||||||
// 🔘 LOGIN BUTTON
|
|
||||||
CustomButton(
|
|
||||||
text: "Buka Kamera",
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (_) => CameraScreen()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 15),
|
|
||||||
|
|
||||||
// 🔹 FORGOT PASSWORD
|
|
||||||
Semantics(
|
|
||||||
button: true,
|
|
||||||
label: "Lupa password",
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => handleForgotPassword(context),
|
|
||||||
child: Text(
|
|
||||||
"Forgot Password?",
|
|
||||||
style: TextStyle(color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// 🔹 SIGN UP
|
const SizedBox(height: 40),
|
||||||
Semantics(
|
|
||||||
button: true,
|
CustomTextField(
|
||||||
label: "Daftar akun baru",
|
label: "Email",
|
||||||
child: TextButton(
|
controller: emailController,
|
||||||
onPressed: () => handleSignUp(context),
|
keyboardType: TextInputType.emailAddress,
|
||||||
child: Text(
|
),
|
||||||
"Sign Up",
|
|
||||||
style: TextStyle(color: Colors.yellow),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
CustomTextField(
|
||||||
|
label: "Password",
|
||||||
|
controller: passwordController,
|
||||||
|
obscure: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
CustomButton(
|
||||||
|
text: isLoading ? "Memproses..." : "Login",
|
||||||
|
onPressed: () => handleLogin(context),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
Semantics(
|
||||||
|
button: true,
|
||||||
|
label: "Lupa password",
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => handleForgotPassword(context),
|
||||||
|
child: const Text(
|
||||||
|
"Forgot Password?",
|
||||||
|
style: TextStyle(color: Colors.blue),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
Semantics(
|
||||||
|
button: true,
|
||||||
|
label: "Daftar akun baru",
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => handleSignUp(context),
|
||||||
|
child: const Text(
|
||||||
|
"Sign Up",
|
||||||
|
style: TextStyle(color: Colors.yellow),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ class ApiConfig {
|
||||||
static const bool useAndroidEmulator = false;
|
static const bool useAndroidEmulator = false;
|
||||||
|
|
||||||
// Ganti IP ini dengan IP laptop/PC Anda saat menggunakan HP fisik (ADB).
|
// Ganti IP ini dengan IP laptop/PC Anda saat menggunakan HP fisik (ADB).
|
||||||
static const String deviceHost = "192.168.0.31:8000";
|
static const String deviceHost = "192.168.18.14:8000";
|
||||||
|
|
||||||
static String get baseUrl {
|
static String get baseUrl {
|
||||||
if (Platform.isAndroid && useAndroidEmulator) {
|
if (Platform.isAndroid && useAndroidEmulator) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,57 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../models/login_request.dart';
|
||||||
|
import '../models/login_response.dart';
|
||||||
|
import 'api_config.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
bool login(String email, String password) {
|
static const String _tokenKey = "auth_token";
|
||||||
// dummy login
|
|
||||||
return email == "admin@gmail.com" && password == "1234";
|
Future<LoginResponse> login(LoginRequest request) async {
|
||||||
|
final Uri url = Uri.parse("${ApiConfig.baseUrl}/login");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final http.Response response = await http.post(
|
||||||
|
url,
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode(request.toJson()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.body.isEmpty) {
|
||||||
|
return LoginResponse(
|
||||||
|
status: "error",
|
||||||
|
message: "Respons server kosong (status ${response.statusCode})",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> jsonBody;
|
||||||
|
try {
|
||||||
|
jsonBody = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
} catch (_) {
|
||||||
|
return LoginResponse(
|
||||||
|
status: "error",
|
||||||
|
message: "Respons server tidak valid (status ${response.statusCode})",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final LoginResponse loginResponse = LoginResponse.fromJson(jsonBody);
|
||||||
|
|
||||||
|
if (loginResponse.isSuccess && loginResponse.token != null) {
|
||||||
|
await _saveToken(loginResponse.token!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginResponse;
|
||||||
|
} catch (error) {
|
||||||
|
return const LoginResponse(
|
||||||
|
status: "error",
|
||||||
|
message: "Gagal terhubung ke server",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Future<void> _saveToken(String token) async {
|
||||||
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_tokenKey, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class VibrationService {
|
||||||
|
static const MethodChannel _channel =
|
||||||
|
MethodChannel("smart_vision_assist/vibration");
|
||||||
|
|
||||||
|
Future<void> vibrateFor(Duration duration) async {
|
||||||
|
try {
|
||||||
|
final bool? didVibrate = await _channel.invokeMethod<bool>(
|
||||||
|
"vibrate",
|
||||||
|
{"milliseconds": duration.inMilliseconds},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (didVibrate == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Fallback untuk perangkat/platform yang tidak mendukung channel native.
|
||||||
|
}
|
||||||
|
|
||||||
|
await HapticFeedback.vibrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>("stop");
|
||||||
|
} catch (_) {
|
||||||
|
// Tidak perlu aksi tambahan jika platform tidak mendukung stop vibration.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue