Fix local dev config and ALOTTTT OFF FLUTTER, im tired boss..

This commit is contained in:
Wowieee4
2026-05-26 21:09:13 +07:00
parent b8ad8df993
commit a629357e8c
72 changed files with 2566 additions and 530 deletions
@@ -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.',
)),
);
}
}
@@ -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,
});
}
@@ -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,
)),
],
),
),
),
);
},
);
}