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.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,71 @@
|
|||
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.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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../services/tts_service.dart';
|
||||
import '../services/vibration_service.dart';
|
||||
import '../services/yolo_detection_service.dart';
|
||||
import 'login_screen.dart';
|
||||
|
||||
class CameraScreen extends StatefulWidget {
|
||||
const CameraScreen({super.key});
|
||||
|
|
@ -23,6 +25,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
|
||||
final YoloDetectionService _detectionService = YoloDetectionService();
|
||||
final TTSService _tts = TTSService();
|
||||
final VibrationService _vibrationService = VibrationService();
|
||||
|
||||
bool isRecording = false;
|
||||
bool isDetecting = false;
|
||||
|
|
@ -31,7 +34,8 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
Size? detectionFrameSize;
|
||||
Timer? _detectionTimer;
|
||||
Timer? _vibrationTimer;
|
||||
bool _isVibrating = false;
|
||||
|
||||
bool _isLoggingOut = false;
|
||||
String _lastSpokenMessage = "";
|
||||
DateTime? _lastSpokenAt;
|
||||
|
||||
|
|
@ -101,7 +105,8 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
_detectionTimer = null;
|
||||
_vibrationTimer?.cancel();
|
||||
_vibrationTimer = null;
|
||||
_isVibrating = false;
|
||||
await _vibrationService.stop();
|
||||
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
|
|
@ -114,6 +119,43 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
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 {
|
||||
final CameraController? cameraController = controller;
|
||||
if (cameraController == null ||
|
||||
|
|
@ -196,7 +238,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
return;
|
||||
}
|
||||
|
||||
_vibrateForTwoSeconds();
|
||||
await _vibrateForSevenSeconds();
|
||||
|
||||
final Map<String, int> objectCounts = {};
|
||||
for (final YoloDetection object in response.objects) {
|
||||
|
|
@ -212,29 +254,16 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
await _speakOnce("Terdeteksi $objectSummary");
|
||||
}
|
||||
|
||||
void _vibrateForTwoSeconds() {
|
||||
if (_isVibrating) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isVibrating = true;
|
||||
int vibrationCount = 0;
|
||||
const int maxVibrationCount = 8;
|
||||
const Duration vibrationInterval = Duration(milliseconds: 250);
|
||||
|
||||
HapticFeedback.vibrate();
|
||||
vibrationCount++;
|
||||
|
||||
Future<void> _vibrateForSevenSeconds() async {
|
||||
_vibrationTimer?.cancel();
|
||||
_vibrationTimer = Timer.periodic(vibrationInterval, (Timer timer) {
|
||||
if (!mounted || !isRecording || vibrationCount >= maxVibrationCount) {
|
||||
timer.cancel();
|
||||
_isVibrating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const Duration vibrationDuration = Duration(seconds: 7);
|
||||
|
||||
HapticFeedback.vibrate();
|
||||
vibrationCount++;
|
||||
await _vibrationService.vibrateFor(vibrationDuration);
|
||||
|
||||
_vibrationTimer = Timer(vibrationDuration, () {
|
||||
_vibrationTimer = null;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +285,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
void dispose() {
|
||||
_detectionTimer?.cancel();
|
||||
_vibrationTimer?.cancel();
|
||||
unawaited(_vibrationService.stop());
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -291,94 +321,108 @@ class _CameraScreenState extends State<CameraScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller == null || !controller!.value.isInitialized) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
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 PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||
if (!didPop) {
|
||||
await logout();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
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(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: buildCameraPreview(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: _BoundingBoxOverlay(
|
||||
imageSize: detectionFrameSize,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||
if (!didPop) {
|
||||
await logout();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 48,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: _DetectionStatusPanel(
|
||||
isRecording: isRecording,
|
||||
isDetecting: isDetecting,
|
||||
statusMessage: statusMessage,
|
||||
objects: detectedObjects,
|
||||
),
|
||||
),
|
||||
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(
|
||||
bottom: 40,
|
||||
right: 30,
|
||||
child: Semantics(
|
||||
label: "Kembali",
|
||||
button: true,
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.black,
|
||||
onPressed: logout,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: MediaQuery.of(context).size.width / 2 - 35,
|
||||
child: Semantics(
|
||||
label: isRecording ? "Stop rekam" : "Mulai rekam",
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isRecording ? Colors.red : Colors.white,
|
||||
border: Border.all(color: Colors.grey, width: 4),
|
||||
),
|
||||
child: Icon(
|
||||
isRecording ? Icons.stop : Icons.circle,
|
||||
color: isRecording ? Colors.white : Colors.red,
|
||||
size: 30,
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: MediaQuery.of(context).size.width / 2 - 35,
|
||||
child: Semantics(
|
||||
label: isRecording ? "Stop rekam" : "Mulai rekam",
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isRecording ? Colors.red : Colors.white,
|
||||
border: Border.all(color: Colors.grey, width: 4),
|
||||
),
|
||||
child: Icon(
|
||||
isRecording ? Icons.stop : Icons.circle,
|
||||
color: isRecording ? Colors.white : Colors.red,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,105 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:smart_vision_assist/screens/camera_screen.dart';
|
||||
import '../widgets/custom_button.dart';
|
||||
import '../widgets/custom_textfield.dart';
|
||||
import '../services/tts_service.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../models/login_request.dart';
|
||||
import '../utils/colors.dart';
|
||||
import 'home_screen.dart';
|
||||
import 'signup_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 passwordController = TextEditingController();
|
||||
|
||||
final TTSService tts = TTSService();
|
||||
final AuthService auth = AuthService();
|
||||
|
||||
LoginScreen({super.key});
|
||||
bool isLoading = false;
|
||||
|
||||
// 🔐 LOGIN
|
||||
void handleLogin(BuildContext context) {
|
||||
bool success = auth.login(
|
||||
emailController.text,
|
||||
passwordController.text,
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
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) {
|
||||
tts.speak("Login berhasil");
|
||||
Navigator.push(
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
if (response.isSuccess && response.token != null) {
|
||||
_showMessage(
|
||||
response.message.isNotEmpty ? response.message : "Login berhasil",
|
||||
);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
MaterialPageRoute(builder: (_) => const CameraScreen()),
|
||||
);
|
||||
} 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) {
|
||||
tts.speak("Menu lupa password");
|
||||
Navigator.push(
|
||||
|
|
@ -45,94 +108,96 @@ class LoginScreen extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
// 🆕 SIGN UP
|
||||
void handleSignUp(BuildContext context) {
|
||||
tts.speak("Menu pendaftaran akun");
|
||||
Navigator.push(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
tts.speak("Silakan login");
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 🔷 TITLE
|
||||
Text(
|
||||
"Object Detector Assist",
|
||||
style: TextStyle(
|
||||
color: AppColors.text,
|
||||
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),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Object Detector Assist",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: AppColors.text,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔹 SIGN UP
|
||||
Semantics(
|
||||
button: true,
|
||||
label: "Daftar akun baru",
|
||||
child: TextButton(
|
||||
onPressed: () => handleSignUp(context),
|
||||
child: Text(
|
||||
"Sign Up",
|
||||
style: TextStyle(color: Colors.yellow),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
CustomTextField(
|
||||
label: "Email",
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
|
||||
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;
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
bool login(String email, String password) {
|
||||
// dummy login
|
||||
return email == "admin@gmail.com" && password == "1234";
|
||||
static const String _tokenKey = "auth_token";
|
||||
|
||||
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