Fix local dev config and ALOTTTT OFF FLUTTER, im tired boss..
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../domain/repositories/sos_repository.dart';
|
||||
|
||||
enum SosPhase { idle, sending, sent, error }
|
||||
|
||||
class SosState {
|
||||
final SosPhase phase;
|
||||
final String? message;
|
||||
|
||||
const SosState({this.phase = SosPhase.idle, this.message});
|
||||
}
|
||||
|
||||
class SosCubit extends Cubit<SosState> {
|
||||
final SosRepository _repository;
|
||||
|
||||
SosCubit(this._repository) : super(const SosState());
|
||||
|
||||
Future<void> trigger({
|
||||
String triggerType = 'MANUAL',
|
||||
double? lat,
|
||||
double? lng,
|
||||
}) async {
|
||||
emit(const SosState(phase: SosPhase.sending));
|
||||
final result = await _repository.triggerSos(
|
||||
triggerType: triggerType,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
);
|
||||
result.fold(
|
||||
(failure) => emit(SosState(phase: SosPhase.error, message: failure.message)),
|
||||
(_) => emit(const SosState(
|
||||
phase: SosPhase.sent,
|
||||
message: 'SOS terkirim. Guardian sudah diberi tahu.',
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../domain/repositories/sos_repository.dart';
|
||||
|
||||
class SosRepositoryImpl implements SosRepository {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
const SosRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> triggerSos({
|
||||
required String triggerType,
|
||||
double? lat,
|
||||
double? lng,
|
||||
}) async {
|
||||
try {
|
||||
await _apiClient.dio.post('/user/sos', data: {
|
||||
'triggerType': triggerType,
|
||||
'lat': lat,
|
||||
'lng': lng,
|
||||
});
|
||||
return const Right(null);
|
||||
} catch (_) {
|
||||
return const Left(NetworkFailure('SOS belum terkirim. Coba lagi.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
class SosEvent {
|
||||
final int? id;
|
||||
final String triggerType;
|
||||
final double? lat;
|
||||
final double? lng;
|
||||
final String status;
|
||||
final DateTime? createdAt;
|
||||
|
||||
const SosEvent({
|
||||
this.id,
|
||||
required this.triggerType,
|
||||
this.lat,
|
||||
this.lng,
|
||||
this.status = 'TRIGGERED',
|
||||
this.createdAt,
|
||||
});
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../../core/errors/failures.dart';
|
||||
|
||||
abstract class SosRepository {
|
||||
Future<Either<Failure, void>> triggerSos({
|
||||
required String triggerType,
|
||||
double? lat,
|
||||
double? lng,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export '../../sos_screen.dart';
|
||||
@@ -5,6 +5,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../../app/injection_container.dart';
|
||||
@@ -12,6 +13,7 @@ import '../../core/errors/friendly_error.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/services/haptic_service.dart';
|
||||
import '../../core/services/tts_service.dart';
|
||||
import 'application/sos_cubit.dart';
|
||||
|
||||
Dio get _api => sl<ApiClient>().dio;
|
||||
|
||||
@@ -61,7 +63,7 @@ class SosScreen extends StatefulWidget {
|
||||
class _SosScreenState extends State<SosScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// State
|
||||
bool _sending = false;
|
||||
late final SosCubit _sosCubit;
|
||||
bool _historyLoading = true;
|
||||
List<_SosEvent> _events = const [];
|
||||
String? _historyError;
|
||||
@@ -76,6 +78,7 @@ class _SosScreenState extends State<SosScreen>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sosCubit = sl<SosCubit>();
|
||||
_pulseCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
@@ -89,6 +92,7 @@ class _SosScreenState extends State<SosScreen>
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseCtrl.dispose();
|
||||
_sosCubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -134,7 +138,7 @@ class _SosScreenState extends State<SosScreen>
|
||||
}
|
||||
|
||||
Future<void> _confirmAndSend() async {
|
||||
if (_sending) return;
|
||||
if (_sosCubit.state.phase == SosPhase.sending) return;
|
||||
|
||||
// Confirmation dialog — prevents accidental tap
|
||||
final confirm = await showDialog<bool>(
|
||||
@@ -178,15 +182,17 @@ class _SosScreenState extends State<SosScreen>
|
||||
}
|
||||
|
||||
Future<void> _sendSos() async {
|
||||
setState(() => _sending = true);
|
||||
await runFriendlyAction(
|
||||
() async {
|
||||
final pos = await _getPosition();
|
||||
await _api.post('/user/sos', data: {
|
||||
'triggerType': 'BUTTON',
|
||||
'lat': pos?.latitude,
|
||||
'lng': pos?.longitude,
|
||||
});
|
||||
await _sosCubit.trigger(
|
||||
triggerType: 'BUTTON',
|
||||
lat: pos?.latitude,
|
||||
lng: pos?.longitude,
|
||||
);
|
||||
if (_sosCubit.state.phase == SosPhase.error) {
|
||||
throw StateError(_sosCubit.state.message ?? 'Gagal kirim SOS.');
|
||||
}
|
||||
await sl<HapticService>().sosTriggered();
|
||||
sl<TtsService>().speak('SOS terkirim ke Guardian.');
|
||||
_snack('SOS berhasil dikirim! Guardian sudah diberitahu.');
|
||||
@@ -195,19 +201,22 @@ class _SosScreenState extends State<SosScreen>
|
||||
onError: _snack,
|
||||
fallback: 'Gagal kirim SOS. Coba lagi sebentar.',
|
||||
);
|
||||
if (mounted) setState(() => _sending = false);
|
||||
}
|
||||
|
||||
// ── Build ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
return BlocBuilder<SosCubit, SosState>(
|
||||
bloc: _sosCubit,
|
||||
builder: (context, sosState) {
|
||||
final sending = sosState.phase == SosPhase.sending;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
@@ -247,7 +256,7 @@ class _SosScreenState extends State<SosScreen>
|
||||
|
||||
// SOS Button
|
||||
Center(
|
||||
child: _sending
|
||||
child: sending
|
||||
? const _SendingIndicator()
|
||||
: AnimatedBuilder(
|
||||
animation: _pulseAnim,
|
||||
@@ -288,15 +297,17 @@ class _SosScreenState extends State<SosScreen>
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Expanded(
|
||||
child: _SosHistory(
|
||||
loading: _historyLoading,
|
||||
error: _historyError,
|
||||
events: _events,
|
||||
onRefresh: _loadHistory,
|
||||
)),
|
||||
],
|
||||
child: _SosHistory(
|
||||
loading: _historyLoading,
|
||||
error: _historyError,
|
||||
events: _events,
|
||||
onRefresh: _loadHistory,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user