This commit is contained in:
kleponijo 2026-05-01 18:05:30 +07:00
parent a60c6af54b
commit 469c500053
3 changed files with 401 additions and 99 deletions

View File

@ -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(),
],
),
);
}
}

View File

@ -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,
),
],
),
),
);
}
}

View File

@ -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: 812.5 m/s (~2945 km/h) const double _kWindWarning = 2.0; // Waspada: 812.5 m/s (~2945 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;