oke
This commit is contained in:
parent
a60c6af54b
commit
469c500053
|
|
@ -1,8 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||||
import '../../../blocs/notification_bloc/notification_bloc.dart';
|
import '../../../blocs/notification_bloc/notification_bloc.dart';
|
||||||
|
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
|
||||||
|
import '../../monitoring/evaporasi/blocs/evaporasi_bloc.dart';
|
||||||
|
import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart';
|
||||||
import '../widgets/notification_panel.dart';
|
import '../widgets/notification_panel.dart';
|
||||||
|
import '../widgets/sensor_grid.dart';
|
||||||
import 'main_drawer.dart';
|
import 'main_drawer.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
|
|
@ -50,112 +55,152 @@ class _HomeScreenState extends State<HomeScreen>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return MultiBlocProvider(
|
||||||
// Tap di luar panel → tutup
|
providers: [
|
||||||
onTap: () {
|
BlocProvider<WindSpeedBloc>(
|
||||||
if (_showNotifications) _toggleNotifications();
|
create: (context) => WindSpeedBloc(
|
||||||
},
|
repository: context.read<MonitoringRepository>(),
|
||||||
child: Scaffold(
|
notificationBloc: context.read<NotificationBloc>(),
|
||||||
drawer: const MainDrawer(),
|
)..add(WatchWindSpeedStarted()),
|
||||||
appBar: AppBar(
|
),
|
||||||
backgroundColor: Colors.white,
|
BlocProvider<EvaporasiBloc>(
|
||||||
elevation: 0.5,
|
create: (context) => EvaporasiBloc(
|
||||||
centerTitle: false,
|
repository: context.read<MonitoringRepository>(),
|
||||||
leading: Builder(
|
notificationBloc: context.read<NotificationBloc>(),
|
||||||
builder: (context) => IconButton(
|
)..add(WatchEvaporasiStarted()),
|
||||||
icon: const Icon(Icons.menu, color: Colors.black),
|
),
|
||||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
BlocProvider<AtmosphericConditionsBloc>(
|
||||||
),
|
create: (context) => AtmosphericConditionsBloc(
|
||||||
),
|
repository: context.read<MonitoringRepository>(),
|
||||||
title: Row(
|
)..add(WatchAtmosphericConditionsStarted()),
|
||||||
children: [
|
),
|
||||||
Image.asset(
|
],
|
||||||
'images/logo_klimatologi.png',
|
child: GestureDetector(
|
||||||
height: 90,
|
onTap: () {
|
||||||
fit: BoxFit.contain,
|
if (_showNotifications) _toggleNotifications();
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
drawer: const MainDrawer(),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0.5,
|
||||||
|
centerTitle: false,
|
||||||
|
leading: Builder(
|
||||||
|
builder: (context) => IconButton(
|
||||||
|
icon: const Icon(Icons.menu, color: Colors.black),
|
||||||
|
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
title: Image.asset(
|
||||||
actions: [
|
'images/logo_klimatologi.png',
|
||||||
// Icon lonceng dengan badge
|
height: 90,
|
||||||
BlocBuilder<NotificationBloc, NotificationState>(
|
fit: BoxFit.contain,
|
||||||
buildWhen: (prev, cur) =>
|
),
|
||||||
prev.unreadCount != cur.unreadCount ||
|
actions: [
|
||||||
prev.hasActiveAlerts != cur.hasActiveAlerts,
|
// Icon lonceng dengan badge
|
||||||
builder: (context, state) {
|
BlocBuilder<NotificationBloc, NotificationState>(
|
||||||
return Stack(
|
buildWhen: (prev, cur) =>
|
||||||
children: [
|
prev.unreadCount != cur.unreadCount ||
|
||||||
IconButton(
|
prev.hasActiveAlerts != cur.hasActiveAlerts,
|
||||||
icon: Icon(
|
builder: (context, state) {
|
||||||
_showNotifications
|
return Stack(
|
||||||
? Icons.notifications
|
children: [
|
||||||
: Icons.notifications_none,
|
IconButton(
|
||||||
color: Colors.black,
|
icon: Icon(
|
||||||
|
_showNotifications
|
||||||
|
? Icons.notifications
|
||||||
|
: Icons.notifications_none,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
onPressed: _toggleNotifications,
|
||||||
),
|
),
|
||||||
onPressed: _toggleNotifications,
|
if (state.unreadCount > 0)
|
||||||
),
|
Positioned(
|
||||||
if (state.unreadCount > 0)
|
right: 6,
|
||||||
Positioned(
|
top: 6,
|
||||||
right: 6,
|
child: Container(
|
||||||
top: 6,
|
padding: const EdgeInsets.all(3),
|
||||||
child: Container(
|
decoration: const BoxDecoration(
|
||||||
padding: const EdgeInsets.all(3),
|
color: Colors.red,
|
||||||
decoration: const BoxDecoration(
|
shape: BoxShape.circle,
|
||||||
color: Colors.red,
|
),
|
||||||
shape: BoxShape.circle,
|
constraints: const BoxConstraints(
|
||||||
),
|
minWidth: 16,
|
||||||
constraints: const BoxConstraints(
|
minHeight: 16,
|
||||||
minWidth: 16,
|
),
|
||||||
minHeight: 16,
|
child: Text(
|
||||||
),
|
state.unreadCount > 9
|
||||||
child: Text(
|
? '9+'
|
||||||
state.unreadCount > 9
|
: '${state.unreadCount}',
|
||||||
? '9+'
|
style: const TextStyle(
|
||||||
: '${state.unreadCount}',
|
color: Colors.white,
|
||||||
style: const TextStyle(
|
fontSize: 9,
|
||||||
color: Colors.white,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 9,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.logout, color: Colors.black),
|
||||||
icon: const Icon(Icons.logout, color: Colors.black),
|
onPressed: () => context
|
||||||
onPressed: () => context
|
.read<AuthenticationBloc>()
|
||||||
.read<AuthenticationBloc>()
|
.add(AuthenticationLogoutRequested()),
|
||||||
.add(AuthenticationLogoutRequested()),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
],
|
||||||
],
|
),
|
||||||
),
|
body: Stack(
|
||||||
body: Stack(
|
children: [
|
||||||
children: [
|
// Konten utama
|
||||||
// Konten utama
|
const _HomeBody(),
|
||||||
const Center(child: Text('body home page')),
|
// Dropdown notification panel
|
||||||
|
if (_showNotifications)
|
||||||
// Dropdown notification panel
|
Positioned(
|
||||||
if (_showNotifications)
|
top: 0,
|
||||||
Positioned(
|
left: 0,
|
||||||
top: 0,
|
right: 0,
|
||||||
left: 0,
|
child: FadeTransition(
|
||||||
right: 0,
|
opacity: _fadeAnim,
|
||||||
child: FadeTransition(
|
child: GestureDetector(
|
||||||
opacity: _fadeAnim,
|
onTap: () {}, // cegah tap di panel menutup panel
|
||||||
child: GestureDetector(
|
child: const NotificationPanel(),
|
||||||
onTap: () {}, // cegah tap di panel menutup panel
|
),
|
||||||
child: const NotificationPanel(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _HomeBody extends StatelessWidget {
|
||||||
|
const _HomeBody();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Monitoring Realtime',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const SensorGrid(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
|
||||||
|
import '../../monitoring/evaporasi/blocs/evaporasi_bloc.dart';
|
||||||
|
import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart';
|
||||||
|
|
||||||
|
class SensorGrid extends StatelessWidget {
|
||||||
|
const SensorGrid({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Lebar card: 2 kolom dengan gap 12
|
||||||
|
final cardWidth = (constraints.maxWidth - 12) / 2;
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
// --- ANEMOMETER ---
|
||||||
|
BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.air,
|
||||||
|
iconColor: Colors.blue.shade600,
|
||||||
|
iconBgColor: Colors.blue.shade50,
|
||||||
|
label: 'Kecepatan Angin',
|
||||||
|
value: state.isLoading
|
||||||
|
? '—'
|
||||||
|
: state.currentSpeed.toStringAsFixed(1),
|
||||||
|
unit: 'm/s',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- EVAPORASI ---
|
||||||
|
BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.water_drop_outlined,
|
||||||
|
iconColor: Colors.teal.shade600,
|
||||||
|
iconBgColor: Colors.teal.shade50,
|
||||||
|
label: 'Evaporasi',
|
||||||
|
value: state.isLoading
|
||||||
|
? '—'
|
||||||
|
: state.currentValue.toStringAsFixed(1),
|
||||||
|
unit: 'mm',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- SUHU AIR (dari Evaporasi) ---
|
||||||
|
BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.thermostat_outlined,
|
||||||
|
iconColor: Colors.orange.shade600,
|
||||||
|
iconBgColor: Colors.orange.shade50,
|
||||||
|
label: 'Suhu Air',
|
||||||
|
value: state.isLoading
|
||||||
|
? '—'
|
||||||
|
: state.temperature.toStringAsFixed(1),
|
||||||
|
unit: '°C',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- TINGGI AIR (dari Evaporasi) ---
|
||||||
|
BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.straighten_outlined,
|
||||||
|
iconColor: Colors.cyan.shade600,
|
||||||
|
iconBgColor: Colors.cyan.shade50,
|
||||||
|
label: 'Tinggi Air',
|
||||||
|
value:
|
||||||
|
state.isLoading ? '—' : state.waterLevel.toStringAsFixed(1),
|
||||||
|
unit: 'cm',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- SUHU UDARA (dari Atmospheric) ---
|
||||||
|
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.device_thermostat,
|
||||||
|
iconColor: Colors.red.shade400,
|
||||||
|
iconBgColor: Colors.red.shade50,
|
||||||
|
label: 'Suhu Udara',
|
||||||
|
value: state.isLoading
|
||||||
|
? '—'
|
||||||
|
: state.temperature.toStringAsFixed(1),
|
||||||
|
unit: '°C',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- KELEMBAPAN ---
|
||||||
|
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.water_outlined,
|
||||||
|
iconColor: Colors.indigo.shade400,
|
||||||
|
iconBgColor: Colors.indigo.shade50,
|
||||||
|
label: 'Kelembapan',
|
||||||
|
value:
|
||||||
|
state.isLoading ? '—' : state.humidity.toStringAsFixed(1),
|
||||||
|
unit: '%',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- TEKANAN UDARA ---
|
||||||
|
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.compress,
|
||||||
|
iconColor: Colors.purple.shade400,
|
||||||
|
iconBgColor: Colors.purple.shade50,
|
||||||
|
label: 'Tekanan Udara',
|
||||||
|
value:
|
||||||
|
state.isLoading ? '—' : state.pressure.toStringAsFixed(1),
|
||||||
|
unit: 'hPa',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- KETINGGIAN ---
|
||||||
|
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
|
||||||
|
builder: (context, state) => SensorCard(
|
||||||
|
width: cardWidth,
|
||||||
|
icon: Icons.terrain_outlined,
|
||||||
|
iconColor: Colors.green.shade600,
|
||||||
|
iconBgColor: Colors.green.shade50,
|
||||||
|
label: 'Ketinggian',
|
||||||
|
value:
|
||||||
|
state.isLoading ? '—' : state.altitude.toStringAsFixed(1),
|
||||||
|
unit: 'm',
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SensorCard extends StatelessWidget {
|
||||||
|
final double width;
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final Color iconBgColor;
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final String unit;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const SensorCard({
|
||||||
|
super.key,
|
||||||
|
required this.width,
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.iconBgColor,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.unit,
|
||||||
|
this.isLoading = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Ikon
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: iconBgColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: iconColor, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Nilai + satuan
|
||||||
|
isLoading
|
||||||
|
? Container(
|
||||||
|
height: 24,
|
||||||
|
width: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
unit,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Label
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,8 @@ part 'wind_speed_state.dart';
|
||||||
|
|
||||||
/// Threshold kecepatan angin (satuan m/s)
|
/// Threshold kecepatan angin (satuan m/s)
|
||||||
/// Referensi: Beaufort scale & BMKG
|
/// Referensi: Beaufort scale & BMKG
|
||||||
const double _kWindWarning = 4.0; // Waspada: 8–12.5 m/s (~29–45 km/h)
|
const double _kWindWarning = 2.0; // Waspada: 8–12.5 m/s (~29–45 km/h)
|
||||||
const double _kWindDanger = 5.5; // Bahaya: > 12.5 m/s (> 45 km/h)
|
const double _kWindDanger = 4.0; // Bahaya: > 12.5 m/s (> 45 km/h)
|
||||||
|
|
||||||
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
final MonitoringRepository _repository;
|
final MonitoringRepository _repository;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue