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_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<HomeScreen>
@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<WindSpeedBloc>(
create: (context) => WindSpeedBloc(
repository: context.read<MonitoringRepository>(),
notificationBloc: context.read<NotificationBloc>(),
)..add(WatchWindSpeedStarted()),
),
BlocProvider<EvaporasiBloc>(
create: (context) => EvaporasiBloc(
repository: context.read<MonitoringRepository>(),
notificationBloc: context.read<NotificationBloc>(),
)..add(WatchEvaporasiStarted()),
),
BlocProvider<AtmosphericConditionsBloc>(
create: (context) => AtmosphericConditionsBloc(
repository: context.read<MonitoringRepository>(),
)..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<NotificationBloc, NotificationState>(
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<NotificationBloc, NotificationState>(
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<AuthenticationBloc>()
.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<AuthenticationBloc>()
.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(),
],
),
);
}
}

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)
/// Referensi: Beaufort scale & BMKG
const double _kWindWarning = 4.0; // Waspada: 812.5 m/s (~2945 km/h)
const double _kWindDanger = 5.5; // Bahaya: > 12.5 m/s (> 45 km/h)
const double _kWindWarning = 2.0; // Waspada: 812.5 m/s (~2945 km/h)
const double _kWindDanger = 4.0; // Bahaya: > 12.5 m/s (> 45 km/h)
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
final MonitoringRepository _repository;