From 469c500053da574c1b8de70e3fb22e67797b3a41 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Fri, 1 May 2026 18:05:30 +0700 Subject: [PATCH] oke --- lib/screens/home/views/home_screen.dart | 239 +++++++++------- lib/screens/home/widgets/sensor_grid.dart | 257 ++++++++++++++++++ .../wind_speed/blocs/wind_speed_bloc.dart | 4 +- 3 files changed, 401 insertions(+), 99 deletions(-) create mode 100644 lib/screens/home/widgets/sensor_grid.dart diff --git a/lib/screens/home/views/home_screen.dart b/lib/screens/home/views/home_screen.dart index 2e98f38..71a1af0 100644 --- a/lib/screens/home/views/home_screen.dart +++ b/lib/screens/home/views/home_screen.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; import '../../../blocs/authentication_bloc/authentication_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/sensor_grid.dart'; import 'main_drawer.dart'; class HomeScreen extends StatefulWidget { @@ -50,112 +55,152 @@ class _HomeScreenState extends State @override Widget build(BuildContext context) { - return GestureDetector( - // Tap di luar panel → tutup - onTap: () { - 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: Row( - children: [ - Image.asset( - 'images/logo_klimatologi.png', - height: 90, - fit: BoxFit.contain, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => WindSpeedBloc( + repository: context.read(), + notificationBloc: context.read(), + )..add(WatchWindSpeedStarted()), + ), + BlocProvider( + create: (context) => EvaporasiBloc( + repository: context.read(), + notificationBloc: context.read(), + )..add(WatchEvaporasiStarted()), + ), + BlocProvider( + create: (context) => AtmosphericConditionsBloc( + repository: context.read(), + )..add(WatchAtmosphericConditionsStarted()), + ), + ], + child: GestureDetector( + onTap: () { + 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(), ), - ], - ), - actions: [ - // Icon lonceng dengan badge - BlocBuilder( - buildWhen: (prev, cur) => - prev.unreadCount != cur.unreadCount || - prev.hasActiveAlerts != cur.hasActiveAlerts, - builder: (context, state) { - return Stack( - children: [ - IconButton( - icon: Icon( - _showNotifications - ? Icons.notifications - : Icons.notifications_none, - color: Colors.black, + ), + title: Image.asset( + 'images/logo_klimatologi.png', + height: 90, + fit: BoxFit.contain, + ), + actions: [ + // Icon lonceng dengan badge + BlocBuilder( + buildWhen: (prev, cur) => + prev.unreadCount != cur.unreadCount || + prev.hasActiveAlerts != cur.hasActiveAlerts, + builder: (context, state) { + return Stack( + children: [ + IconButton( + icon: Icon( + _showNotifications + ? Icons.notifications + : Icons.notifications_none, + color: Colors.black, + ), + onPressed: _toggleNotifications, ), - onPressed: _toggleNotifications, - ), - if (state.unreadCount > 0) - Positioned( - right: 6, - top: 6, - child: Container( - padding: const EdgeInsets.all(3), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - constraints: const BoxConstraints( - minWidth: 16, - minHeight: 16, - ), - child: Text( - state.unreadCount > 9 - ? '9+' - : '${state.unreadCount}', - style: const TextStyle( - color: Colors.white, - fontSize: 9, - fontWeight: FontWeight.bold, + if (state.unreadCount > 0) + Positioned( + right: 6, + top: 6, + child: Container( + padding: const EdgeInsets.all(3), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Text( + state.unreadCount > 9 + ? '9+' + : '${state.unreadCount}', + style: const TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), ), - ), - ], - ); - }, - ), - IconButton( - icon: const Icon(Icons.logout, color: Colors.black), - onPressed: () => context - .read() - .add(AuthenticationLogoutRequested()), - ), - const SizedBox(width: 8), - ], - ), - body: Stack( - children: [ - // Konten utama - const Center(child: Text('body home page')), - - // Dropdown notification panel - if (_showNotifications) - Positioned( - top: 0, - left: 0, - right: 0, - child: FadeTransition( - opacity: _fadeAnim, - child: GestureDetector( - onTap: () {}, // cegah tap di panel menutup panel - child: const NotificationPanel(), + ], + ); + }, + ), + IconButton( + icon: const Icon(Icons.logout, color: Colors.black), + onPressed: () => context + .read() + .add(AuthenticationLogoutRequested()), + ), + const SizedBox(width: 8), + ], + ), + body: Stack( + children: [ + // Konten utama + const _HomeBody(), + // Dropdown notification panel + if (_showNotifications) + Positioned( + top: 0, + left: 0, + right: 0, + child: FadeTransition( + opacity: _fadeAnim, + child: GestureDetector( + 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(), + ], + ), + ); + } +} diff --git a/lib/screens/home/widgets/sensor_grid.dart b/lib/screens/home/widgets/sensor_grid.dart new file mode 100644 index 0000000..86500cd --- /dev/null +++ b/lib/screens/home/widgets/sensor_grid.dart @@ -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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart index c957cda..0b7fbed 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -11,8 +11,8 @@ part 'wind_speed_state.dart'; /// Threshold kecepatan angin (satuan m/s) /// Referensi: Beaufort scale & BMKG -const double _kWindWarning = 4.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 _kWindWarning = 2.0; // Waspada: 8–12.5 m/s (~29–45 km/h) +const double _kWindDanger = 4.0; // Bahaya: > 12.5 m/s (> 45 km/h) class WindSpeedBloc extends Bloc { final MonitoringRepository _repository;