Merge branch 'branch_percobaan'

This commit is contained in:
kleponijo 2026-04-14 16:11:30 +07:00
commit e91252478e
53 changed files with 3514 additions and 536 deletions

BIN
images/atmosfer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/google-icon-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/windy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -2,19 +2,28 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:klimatologiot/app_view.dart';
import 'package:user_repository/user_repository.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'blocs/authentication_bloc/authentication_bloc.dart';
class MyApp extends StatelessWidget {
final UserRepository userRepository;
const MyApp(this.userRepository, {super.key});
final MonitoringRepository monitoringRepository;
const MyApp(this.userRepository, this.monitoringRepository, {super.key});
@override
Widget build(BuildContext context) {
return RepositoryProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc(
userRepository: userRepository
),
child: const MyAppView(),
);
return MultiRepositoryProvider(
providers: [
// 1. Daftarkan UserRepository
RepositoryProvider<UserRepository>.value(value: userRepository),
RepositoryProvider<MonitoringRepository>.value(
value: monitoringRepository),
],
child: BlocProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc(
userRepository: userRepository,
),
child: const MyAppView(),
));
}
}
}

View File

@ -26,6 +26,13 @@ class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState>
emit(AuthenticationState.unauthenticated());
}
});
on<AuthenticationLogoutRequested>((event, emit) async {
try {
await userRepository.logOut();
} catch (_) {
// ignore errors here; stream will update state accordingly
}
});
}
@override
Future<void> close() {

View File

@ -11,4 +11,8 @@ class AuthenticationUserChanged extends AuthenticationEvent {
final MyUser? user;
const AuthenticationUserChanged(this.user);
}
class AuthenticationLogoutRequested extends AuthenticationEvent {
const AuthenticationLogoutRequested();
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:klimatologiot/simple_bloc_observer.dart';
import 'package:klimatologiot/app.dart';
import 'package:user_repository/user_repository.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'firebase_options.dart';
void main() async {
@ -12,5 +13,8 @@ void main() async {
options: DefaultFirebaseOptions.currentPlatform,
);
Bloc.observer = SimpleBlocObserver();
runApp(MyApp(FirebaseUserRepo()));
}
runApp(MyApp(
FirebaseUserRepo(),
FirebaseMonitoringRepo(),
));
}

View File

@ -8,9 +8,7 @@ part 'sign_in_state.dart';
class SignInBloc extends Bloc<SignInEvent, SignInState> {
final UserRepository _userRepository;
SignInBloc(
this._userRepository
) : super(SignInInitial()) {
SignInBloc(this._userRepository) : super(SignInInitial()) {
on<SignInRequired>((event, emit) async {
emit(SignInProcess());
try {
@ -19,7 +17,17 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
emit(SignInFailure());
}
});
on<GoogleSignInRequired>((event, emit) async {
emit(SignInProcess());
try {
await _userRepository.signInWithGoogle();
} catch (e) {
emit(SignInFailure());
emit(SignInInitial());
}
});
on<SignOutRequired>((event, emit) async => await _userRepository.logOut());
}
}

View File

@ -17,6 +17,6 @@ class SignInRequired extends SignInEvent {
List<Object> get props => [email, password];
}
class SignOutRequired extends SignInEvent {
}
class GoogleSignInRequired extends SignInEvent {}
class SignOutRequired extends SignInEvent {}

View File

@ -12,11 +12,12 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
on<SignUpRequired>((event, emit) async {
emit(SignUpProcess());
try {
MyUser myUser = await _userRepository.signUp(event.user, event.password);
MyUser myUser =
await _userRepository.signUp(event.user, event.password);
await _userRepository.setUserData(myUser);
emit(SignUpSuccess());
} catch (e) {
emit(SignUpFailure());
emit(SignUpFailure(e.toString()));
}
});
}

View File

@ -15,4 +15,6 @@ class SignUpRequired extends SignUpEvent {
@override
List<Object> get props => [user, password];
}
}
class GoogleSignInRequired extends SignUpEvent {}

View File

@ -4,11 +4,18 @@ sealed class SignUpState extends Equatable {
const SignUpState();
@override
List<Object> get props => [];
List<Object?> get props => [];
}
final class SignUpInitial extends SignUpState {}
final class SignUpSuccess extends SignUpState {}
final class SignUpFailure extends SignUpState {}
final class SignUpFailure extends SignUpState {
final String? message;
const SignUpFailure([this.message]);
@override
List<Object?> get props => [message];
}
final class SignUpProcess extends SignUpState {}

View File

@ -13,125 +13,153 @@ class SignInScreen extends StatefulWidget {
}
class _SignInScreenState extends State<SignInScreen> {
final passwordController = TextEditingController();
final passwordController = TextEditingController();
final emailController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool signInRequired = false;
IconData iconPassword = CupertinoIcons.eye_fill;
bool obscurePassword = true;
String? _errorMsg;
bool signInRequired = false;
IconData iconPassword = CupertinoIcons.eye_fill;
bool obscurePassword = true;
String? _errorMsg;
@override
Widget build(BuildContext context) {
return BlocListener<SignInBloc, SignInState>(
listener: (context, state) {
if(state is SignInSuccess) {
setState(() {
signInRequired = false;
});
} else if(state is SignInProcess) {
setState(() {
signInRequired = true;
});
} else if(state is SignInFailure) {
setState(() {
signInRequired = false;
_errorMsg = 'Invalid email or password';
});
}
},
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'Email',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
}
)
),
const SizedBox(height: 10),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
},
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
if(obscurePassword) {
iconPassword = CupertinoIcons.eye_fill;
} else {
iconPassword = CupertinoIcons.eye_slash_fill;
}
});
listener: (context, state) {
if (state is SignInProcess) {
setState(() {
signInRequired = true;
});
} else {
setState(() {
signInRequired = false;
});
if (state is SignInFailure) {
setState(() {
// PERUBAHAN: Pesan error lebih umum karena Google gagal belum tentu soal password
_errorMsg = 'Sign in failed. Please try again.';
});
}
}
},
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'Email',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
})),
const SizedBox(height: 10),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
},
icon: Icon(iconPassword),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
if (obscurePassword) {
iconPassword = CupertinoIcons.eye_fill;
} else {
iconPassword = CupertinoIcons.eye_slash_fill;
}
});
},
icon: Icon(iconPassword),
),
),
),
),
const SizedBox(height: 20),
!signInRequired
? SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
context.read<SignInBloc>().add(SignInRequired(
emailController.text,
passwordController.text)
);
}
},
style: TextButton.styleFrom(
elevation: 3.0,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60)
)
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 25, vertical: 5),
child: Text(
'Sign In',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600
),
),
const SizedBox(height: 20),
!signInRequired
? SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
context.read<SignInBloc>().add(SignInRequired(
emailController.text,
passwordController.text));
}
},
style: TextButton.styleFrom(
elevation: 3.0,
backgroundColor:
Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60))),
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 25, vertical: 5),
child: Text(
'Sign In',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600),
),
)),
)
: const CircularProgressIndicator(),
/// ======= Tombol Login Google ======= ///
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: TextButton.icon(
onPressed: () {
// == Mengirim event login Google ke Bloc == //
context.read<SignInBloc>().add(GoogleSignInRequired());
},
icon: SizedBox(
height: 20,
width: 20,
child: Image.asset(
'images/google-icon-logo.png',
fit: BoxFit.contain,
),
),
)
: const CircularProgressIndicator(),
],
)
),
);
label: const Text(
'Sign In with Google',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600),
),
style: TextButton.styleFrom(
elevation: 3.0,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60),
side: const BorderSide(color: Colors.grey))),
),
),
],
)),
);
}
}
}

View File

@ -3,10 +3,13 @@ import 'package:user_repository/user_repository.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../components/my_text_field.dart';
import '../blocs/sign_up_bloc/sign_up_bloc.dart';
import '../blocs/sign_up_bloc/sign_up_bloc.dart' as signUp;
// import '../../../blocs/authentication_bloc/authentication_bloc.dart';
import '../blocs/sign_in_bloc/sign_in_bloc.dart' as signIn;
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
final VoidCallback? onSignInTap;
const SignUpScreen({super.key, this.onSignInTap});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
@ -23,175 +26,179 @@ class _SignUpScreenState extends State<SignUpScreen> {
bool signUpRequired = false;
bool containsUpperCase = false;
bool containsLowerCase = false;
bool containsNumber = false;
bool containsSpecialChar = false;
bool contains8Length = false;
bool containsLowerCase = false;
bool containsNumber = false;
bool containsSpecialChar = false;
bool contains8Length = false;
@override
Widget build(BuildContext context) {
return BlocListener<SignUpBloc, SignUpState>(
listener: (context, state) {
if (state is SignUpSuccess) {
setState(() {
signUpRequired = false;
});
// User sudah terdaftar dan auth state akan otomatis update via AuthenticationBloc
// Tungur sebentar agar Firestore selesai menyimpan data
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Registrasi berhasil! Mengarahkan...')),
);
} else if (state is SignUpProcess) {
setState(() {
signUpRequired = true;
});
} else if (state is SignUpFailure) {
setState(() {
signUpRequired = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Registrasi gagal, silakan coba lagi')),
);
}
},
child: Form(
key: _formKey,
child: Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 30),
Container(
width: MediaQuery.of(context).size.width * 0.92,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
const Center(
child: Text(
'Buat Akun',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
),
return BlocListener<signUp.SignUpBloc, signUp.SignUpState>(
listener: (context, state) {
if (state is signUp.SignUpProcess) {
setState(() {
signUpRequired = true;
});
} else if (state is signUp.SignUpFailure) {
setState(() {
signUpRequired = false; // Matikan loading!
});
// Tampilkan pesan error lewat Snackbar biar user tahu kenapa gagal
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message ?? 'An error ocurred'),
backgroundColor: Colors.red),
);
} else if (state is signUp.SignUpSuccess) {
setState(() {
signUpRequired = false;
});
}
},
child: Form(
key: _formKey,
child: Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 30),
Container(
width: MediaQuery.of(context).size.width * 0.92,
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
const Center(
child: Text(
'Buat Akun',
style: TextStyle(
fontSize: 22, fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
const Center(
child: Text(
'Daftar untuk mulai monitoring',
style: TextStyle(fontSize: 13, color: Colors.black54),
),
),
const SizedBox(height: 6),
const Center(
child: Text(
'Daftar untuk mulai monitoring',
style:
TextStyle(fontSize: 13, color: Colors.black54),
),
const SizedBox(height: 14),
),
const SizedBox(height: 14),
// Name
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: nameController,
hintText: 'Masukkan nama lengkap',
obscureText: false,
keyboardType: TextInputType.name,
prefixIcon: const Icon(CupertinoIcons.person_fill),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
} else if (val.length > 50) {
return 'Name too long';
}
return null;
// Name
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: nameController,
hintText: 'Masukkan nama lengkap',
obscureText: false,
keyboardType: TextInputType.name,
prefixIcon: const Icon(CupertinoIcons.person_fill),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
} else if (val.length > 50) {
return 'Name too long';
}
return null;
},
),
),
const SizedBox(height: 10),
// Email
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'nama@email.com',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
),
const SizedBox(height: 10),
// Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Minimal 6 karakter',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
iconPassword = obscurePassword
? CupertinoIcons.eye_fill
: CupertinoIcons.eye_slash_fill;
});
},
icon: Icon(iconPassword),
),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
const SizedBox(height: 10),
),
const SizedBox(height: 10),
// Email
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'nama@email.com',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
// Confirm Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: confirmPasswordController,
hintText: 'Ulangi password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
} else if (val != passwordController.text) {
return 'Password tidak cocok';
}
return null;
},
),
const SizedBox(height: 10),
),
// Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Minimal 6 karakter',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
iconPassword = obscurePassword
? CupertinoIcons.eye_fill
: CupertinoIcons.eye_slash_fill;
});
},
icon: Icon(iconPassword),
),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
),
const SizedBox(height: 10),
const SizedBox(height: 18),
// Confirm Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: confirmPasswordController,
hintText: 'Ulangi password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
} else if (val != passwordController.text) {
return 'Password tidak cocok';
}
return null;
},
),
),
const SizedBox(height: 18),
// Button
!signUpRequired
? SizedBox(
width: MediaQuery.of(context).size.width * 0.92,
/// == Sign Up == ///
// Button
!signUpRequired
? Center(
child: SizedBox(
width:
MediaQuery.of(context).size.width * 0.5,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
@ -200,8 +207,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
myUser.name = nameController.text;
setState(() {
context.read<SignUpBloc>().add(
SignUpRequired(
context.read<signUp.SignUpBloc>().add(
signUp.SignUpRequired(
myUser,
passwordController.text,
),
@ -209,47 +216,62 @@ class _SignUpScreenState extends State<SignUpScreen> {
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black87,
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(60),
),
),
child: const Text(
'Daftar',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 25, vertical: 5),
child: const Text(
'Sign Up',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
),
),
)
: const CircularProgressIndicator(),
),
)
: const CircularProgressIndicator(),
const SizedBox(height: 12),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Sudah punya akun? '),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Login di sini'),
)
],
),
const SizedBox(height: 12),
Center(
child: Text('Atau'),
),
const SizedBox(height: 12),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Sudah punya akun? '),
TextButton(
onPressed: () {
widget.onSignInTap?.call();
},
child: const Text(
'Login di sini',
style: TextStyle(fontWeight: FontWeight.bold),
),
)
],
),
],
),
),
],
),
const SizedBox(height: 30),
],
),
),
const SizedBox(height: 30),
],
),
),
),
),
);
),
);
}
}
}

View File

@ -13,90 +13,92 @@ class WelcomeScreen extends StatefulWidget {
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> with TickerProviderStateMixin {
late TabController tabController;
class _WelcomeScreenState extends State<WelcomeScreen>
with TickerProviderStateMixin {
late TabController tabController;
@override
@override
void initState() {
tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this
);
tabController = TabController(initialIndex: 0, length: 2, vsync: this);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: SizedBox(
height: MediaQuery.of(context).size.height / 1.8,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TabBar(
controller: tabController,
unselectedLabelColor: Theme.of(context).colorScheme.onBackground.withOpacity(0.5),
labelColor: Theme.of(context).colorScheme.onBackground,
tabs: const [
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'Sign In',
style: TextStyle(
fontSize: 18,
),
),
),
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'Sign Up',
style: TextStyle(
fontSize: 18,
),
),
),
],
),
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
BlocProvider<SignInBloc>(
create: (context) => SignInBloc(
context.read<AuthenticationBloc>().userRepository
),
child: const SignInScreen(),
),
BlocProvider<SignUpBloc>(
create: (context) => SignUpBloc(
context.read<AuthenticationBloc>().userRepository
),
child: const SignUpScreen(),
),
],
)
)
],
),
),
)
],
),
),
),
);
return Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: SizedBox(
height: MediaQuery.of(context).size.height / 1.8,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TabBar(
controller: tabController,
unselectedLabelColor: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.5),
labelColor:
Theme.of(context).colorScheme.onBackground,
tabs: const [
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'Sign In',
style: TextStyle(
fontSize: 18,
),
),
),
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'Sign Up',
style: TextStyle(
fontSize: 18,
),
),
),
],
),
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
BlocProvider<SignInBloc>(
create: (context) => SignInBloc(context
.read<AuthenticationBloc>()
.userRepository),
child: const SignInScreen(),
),
BlocProvider<SignUpBloc>(
create: (context) => SignUpBloc(context
.read<AuthenticationBloc>()
.userRepository),
child: SignUpScreen(onSignInTap: () {
tabController.animateTo(
0); // Ini yang bikin dia pindah ke tab Login
}),
),
],
))
],
),
),
)
],
),
),
),
);
}
}
}

View File

View File

View File

View File

@ -1,12 +1,66 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
import 'main_drawer.dart';
class HomeScreen extends StatelessWidget{
const HomeScreen ({super.key});
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: const MainDrawer(),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.5, // Kasih sedikit bayangan tipis agar elegan
centerTitle: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.menu,
color: Colors.black), // Ikon garis 3 horizontal
onPressed: () {
// ini kodenya untuk membuka:
Scaffold.of(context).openDrawer();
},
)),
title: Row(
children: [
// Ganti dengan Image.asset jika sudah ada logo
const Icon(Icons.cloud_queue, color: Colors.blue, size: 28),
const SizedBox(width: 10),
Text(
"Klimatologi",
style: GoogleFonts.rubik(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.notifications_none, color: Colors.black),
onPressed: () {
// Aksi notifikasi
},
),
IconButton(
icon: const Icon(Icons.logout, color: Colors.black),
onPressed: () {
// Trigger Logout di AuthenticationBloc kamu
context
.read<AuthenticationBloc>()
.add(AuthenticationLogoutRequested());
},
),
const SizedBox(width: 8),
],
),
body: const Center(
child: Text("body home page"),
),
);
}
}
}

View File

@ -0,0 +1,98 @@
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 '../../monitoring/wind_speed/views/wind_speed_screen.dart';
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
class MainDrawer extends StatelessWidget {
const MainDrawer({super.key});
@override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: [
// Bagian Header (Tempat Nama/Email)
BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
// Mengambil email user dari Bloc Global
String userEmail = state.user?.email ?? "Guest";
String userName =
userEmail.split('@')[0]; // Ambil depan email saja
return UserAccountsDrawerHeader(
decoration: const BoxDecoration(color: Colors.blue),
currentAccountPicture: const CircleAvatar(
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 40, color: Colors.blue),
),
accountName: Text(
userName,
style: const TextStyle(fontWeight: FontWeight.bold),
),
accountEmail: Text(userEmail),
);
},
),
// Daftar Menu
ListTile(
leading: Image.asset(
'images/windy.png',
height: 20,
width: 20,
),
title: const Text("Wind Speed"),
onTap: () {
// Navigasi ke Wind Speed
// 1. Tutup Drawer dulu supaya tidak menghalangi transisi
Navigator.pop(context);
// 2. Pindah ke halaman WindSpeedScreen sambil membawa Bloc
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider<WindSpeedBloc>(
create: (context) => WindSpeedBloc(
// AMBIL MonitoringRepository
repository: context.read<MonitoringRepository>(),
)..add(
WatchWindSpeedStarted()), // Langsung mulai monitoring
child: const WindSpeedScreen(),
),
),
);
},
),
ListTile(
leading: const Icon(Icons.water_drop),
title: const Text("Evaporasi"),
onTap: () {},
),
ListTile(
leading: Image.asset(
'images/atmosfer.png',
height: 20,
width: 20,
),
title: const Text("Tekanan Udara"),
onTap: () {},
),
const Spacer(), // Dorong menu logout ke paling bawah
const Divider(),
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Logout", style: TextStyle(color: Colors.red)),
onTap: () {
context
.read<AuthenticationBloc>()
.add(AuthenticationLogoutRequested());
},
),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -0,0 +1,784 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'shared/widgets/monitoring_shared.dart';
/// AIR QUALITY MONITORING SCREEN
class AirQualityMonitoringScreen extends StatefulWidget {
const AirQualityMonitoringScreen({super.key});
@override
State<AirQualityMonitoringScreen> createState() =>
_AirQualityMonitoringScreenState();
}
class _AirQualityMonitoringScreenState
extends State<AirQualityMonitoringScreen> {
String _selectedPeriod = "Hari Ini";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Monitoring Kualitas Udara"),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Kualitas Udara",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
/// GRAFIK MONITORING
_buildMonitoringGraphBlock(),
const SizedBox(height: 25),
/// PENJELASAN
_buildAirQualityExplanation(),
const SizedBox(height: 25),
/// STATISTIK TAHUNAN
_buildAnnualAirQualityGraphSection(),
],
),
),
),
);
}
/// EXPORT FUNCTIONS
void _exportDailyAirQualityData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KUALITAS UDARA - HARIAN (24 JAM)");
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
csvContent.writeln("");
csvContent.writeln("Jam,AQI (Air Quality Index)");
final dailyData = [
68.0,
70.0,
72.0,
75.0,
78.0,
80.0,
78.0,
75.0,
72.0,
70.0,
68.0,
65.0,
60.0,
58.0,
60.0,
62.0,
65.0,
68.0,
70.0,
72.0,
75.0,
78.0,
76.0,
74.0
];
for (int i = 0; i < dailyData.length; i++) {
csvContent.writeln("$i:00,${dailyData[i]}");
}
csvContent.writeln("");
csvContent.writeln("Statistik Harian");
csvContent.writeln("Total: 1709 AQI");
csvContent.writeln("Rata-rata: 71.2 AQI");
csvContent.writeln("Maksimal: 80 AQI");
csvContent.writeln("Minimal: 58 AQI");
showExportPreview(
context, csvContent.toString(), "Data Harian Kualitas Udara");
}
void _exportWeeklyAirQualityData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KUALITAS UDARA - MINGGUAN");
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
csvContent.writeln("");
csvContent.writeln("Hari,Rata-rata AQI");
final days = [
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu',
'Minggu'
];
final weeklyData = [68.0, 72.0, 75.0, 78.0, 72.0, 68.0, 62.0];
for (int i = 0; i < days.length; i++) {
csvContent.writeln("${days[i]},${weeklyData[i]}");
}
csvContent.writeln("");
csvContent.writeln("Statistik Mingguan");
csvContent.writeln("Total: 495 AQI");
csvContent.writeln("Rata-rata: 70.7 AQI");
csvContent.writeln("Maksimal: 78 AQI");
csvContent.writeln("Minimal: 62 AQI");
showExportPreview(
context, csvContent.toString(), "Data Mingguan Kualitas Udara");
}
void _exportMonthlyAirQualityData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KUALITAS UDARA - BULANAN");
csvContent.writeln(
"Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
csvContent.writeln("");
csvContent.writeln("Tanggal,Rata-rata AQI");
for (int i = 1; i <= 28; i++) {
final value = (70.0 + (i % 6) * 2.5).toStringAsFixed(1);
csvContent.writeln("$i,$value");
}
csvContent.writeln("");
csvContent.writeln("Statistik Bulanan");
csvContent.writeln("Total: 1990 AQI");
csvContent.writeln("Rata-rata: 71.1 AQI");
csvContent.writeln("Maksimal: 82 AQI");
csvContent.writeln("Minimal: 55 AQI");
showExportPreview(
context, csvContent.toString(), "Data Bulanan Kualitas Udara");
}
void _exportAnnualAirQualityData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KUALITAS UDARA - TAHUNAN");
csvContent.writeln("Tahun: 2025");
csvContent.writeln("");
csvContent.writeln("Bulan,Rata-rata AQI,AQI Max");
final months = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember'
];
final averages = [
68.0,
70.0,
72.5,
75.0,
78.0,
80.0,
82.0,
80.0,
75.0,
70.0,
68.0,
65.0
];
final maxAqi = [
85.0,
88.0,
90.0,
95.0,
98.0,
100.0,
102.0,
100.0,
95.0,
90.0,
85.0,
80.0
];
for (int i = 0; i < months.length; i++) {
csvContent.writeln("${months[i]},${averages[i]},${maxAqi[i]}");
}
csvContent.writeln("");
csvContent.writeln("STATISTIK TAHUNAN");
csvContent.writeln("Rata-rata Tahunan: 74.2 AQI");
csvContent.writeln("AQI Max Teringgi: 102.0");
csvContent.writeln("AQI Min Terendah: 65.0");
showExportPreview(
context, csvContent.toString(), "Data Tahunan Kualitas Udara");
}
/// DATA GENERATORS
List<FlSpot> _getAirQualityPeriodData() {
switch (_selectedPeriod) {
case "Hari Ini":
return List.generate(24, (i) {
final values = [
68.0,
70.0,
72.0,
75.0,
78.0,
80.0,
78.0,
75.0,
72.0,
70.0,
68.0,
65.0,
60.0,
58.0,
60.0,
62.0,
65.0,
68.0,
70.0,
72.0,
75.0,
78.0,
76.0,
74.0
];
return FlSpot(i.toDouble(), values[i]);
});
case "Minggu Ini":
return [
const FlSpot(0, 68.0),
const FlSpot(1, 72.0),
const FlSpot(2, 75.0),
const FlSpot(3, 78.0),
const FlSpot(4, 72.0),
const FlSpot(5, 68.0),
const FlSpot(6, 62.0),
];
case "Bulan Ini":
return [
const FlSpot(0, 70.0),
const FlSpot(1, 72.5),
const FlSpot(2, 75.0),
const FlSpot(3, 77.5),
const FlSpot(4, 80.0),
const FlSpot(5, 82.0),
const FlSpot(6, 80.0),
const FlSpot(7, 78.0),
const FlSpot(8, 75.0),
const FlSpot(9, 72.0),
const FlSpot(10, 70.0),
const FlSpot(11, 68.0),
];
default:
return [];
}
}
/// UI BUILDERS
Widget _buildMonitoringGraphBlock() {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Status Kualitas Udara",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
"Normal",
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.amber,
),
),
),
],
),
const SizedBox(height: 16),
/// PERIOD SELECTOR + EXPORT
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:
["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(period),
selected: _selectedPeriod == period,
onSelected: (selected) {
setState(() {
_selectedPeriod = period;
});
},
selectedColor: Colors.orange,
labelStyle: TextStyle(
color: _selectedPeriod == period
? Colors.white
: Colors.black,
),
),
);
}).toList(),
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
if (_selectedPeriod == "Hari Ini") {
_exportDailyAirQualityData();
} else if (_selectedPeriod == "Minggu Ini") {
_exportWeeklyAirQualityData();
} else if (_selectedPeriod == "Bulan Ini") {
_exportMonthlyAirQualityData();
}
},
icon: const Icon(Icons.download, size: 16),
label: const Text("Export"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
textStyle: const TextStyle(fontSize: 11),
),
),
],
),
const SizedBox(height: 16),
/// GRAPH
SizedBox(
height: 300,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 1,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey.shade300,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: getPeriodInterval(_selectedPeriod),
getTitlesWidget: (value, meta) {
return Text(
getPeriodLabel(_selectedPeriod, value),
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
);
},
reservedSize: 28,
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border(
left: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
bottom: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
lineBarsData: [
LineChartBarData(
spots: _getAirQualityPeriodData(),
isCurved: true,
barWidth: 2.5,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.orange,
strokeWidth: 0,
);
},
),
color: Colors.orange,
belowBarData: BarAreaData(
show: true,
color: Colors.orange.withOpacity(0.1),
),
),
],
),
),
),
],
),
),
);
}
Widget _buildAirQualityExplanation() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Penjelasan Kualitas Udara",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
const SizedBox(height: 10),
const Text(
"Kualitas udara diukur menggunakan Air Quality Index (AQI) yang menunjukkan tingkat polusi udara. AQI berkisar dari 0-500, dengan nilai lebih rendah menunjukkan udara lebih bersih. Monitoring kualitas udara penting untuk kesehatan masyarakat, perencanaan lingkungan, dan identifikasi sumber polusi. Kategori: 0-50 (Baik), 51-100 (Wajar), 101-150 (Tidak Sehat), 151+ (Sangat Tidak Sehat).",
style: TextStyle(
fontSize: 12,
height: 1.6,
color: Colors.orange,
),
),
],
),
);
}
Widget _buildAnnualAirQualityGraphSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Statistik Tahunan Kualitas Udara",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 15),
/// STATS
Row(
children: [
Expanded(
child: StatCard(
title: "Rata-rata Tahunan",
value: "74.2 AQI",
color: Colors.orange,
icon: Icons.cloud,
),
),
const SizedBox(width: 10),
Expanded(
child: StatCard(
title: "AQI Maksimal",
value: "102.0",
color: Colors.red,
icon: Icons.warning,
),
),
],
),
const SizedBox(height: 20),
/// CHART
const Text(
"Grafik Tahunan - Rata-rata & AQI Maksimal",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 320,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 5,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey.shade300,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des'
];
return Text(
months[value.toInt()],
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
);
},
reservedSize: 30,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Text(
'${value.toInt()} AQI',
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
);
},
reservedSize: 50,
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border(
left: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
bottom: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
lineBarsData: [
/// Rata-rata
LineChartBarData(
spots: const [
FlSpot(0, 68.0),
FlSpot(1, 70.0),
FlSpot(2, 72.5),
FlSpot(3, 75.0),
FlSpot(4, 78.0),
FlSpot(5, 80.0),
FlSpot(6, 82.0),
FlSpot(7, 80.0),
FlSpot(8, 75.0),
FlSpot(9, 70.0),
FlSpot(10, 68.0),
FlSpot(11, 65.0),
],
isCurved: true,
barWidth: 2.5,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.orange,
strokeWidth: 0,
);
},
),
color: Colors.orange,
belowBarData: BarAreaData(
show: true,
color: Colors.orange.withOpacity(0.1),
),
),
/// AQI Max
LineChartBarData(
spots: const [
FlSpot(0, 85.0),
FlSpot(1, 88.0),
FlSpot(2, 90.0),
FlSpot(3, 95.0),
FlSpot(4, 98.0),
FlSpot(5, 100.0),
FlSpot(6, 102.0),
FlSpot(7, 100.0),
FlSpot(8, 95.0),
FlSpot(9, 90.0),
FlSpot(10, 85.0),
FlSpot(11, 80.0),
],
isCurved: true,
barWidth: 2.5,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.red,
strokeWidth: 0,
);
},
),
color: Colors.red,
),
],
),
),
),
),
),
const SizedBox(height: 15),
/// LEGEND
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 8),
const Text(
"Rata-rata AQI",
style: TextStyle(fontSize: 12),
),
const SizedBox(width: 20),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 8),
const Text(
"AQI Maksimal",
style: TextStyle(fontSize: 12),
),
],
),
const SizedBox(height: 15),
Center(
child: ElevatedButton.icon(
onPressed: () => _exportAnnualAirQualityData(),
icon: const Icon(Icons.file_download),
label: const Text("Download Excel Tahunan"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
/// SHOW EXPORT PREVIEW DIALOG
/// Helper function untuk menampilkan preview export data
void showExportPreview(BuildContext context, String content, String title) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Preview Export - $title"),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
child: Text(
content,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 10,
),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Tutup"),
),
],
),
);
}
/// STAT CARD WIDGET
/// Reusable card untuk menampilkan statistik
class StatCard extends StatelessWidget {
final String title;
final String value;
final Color color;
final IconData icon;
const StatCard({
super.key,
required this.title,
required this.value,
required this.color,
required this.icon,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
}
/// BUILD BAR GROUP FOR CHART
/// Helper untuk membuat bar group di BarChart
BarChartGroupData buildBarGroup(int x, double value) {
return BarChartGroupData(
x: x,
barRods: [
BarChartRodData(
toY: value.toDouble(),
color: Colors.blue,
width: 12,
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
),
],
);
}
/// GET PERIOD INTERVAL
/// Menentukan spacing interval berdasarkan periode
double getPeriodInterval(String period) {
switch (period) {
case "Hari Ini":
return 3;
case "Minggu Ini":
return 1;
case "Bulan Ini":
return 2;
default:
return 1;
}
}
/// GET PERIOD LABEL
/// Menentukan label untuk X-axis berdasarkan periode
String getPeriodLabel(String period, double value) {
switch (period) {
case "Hari Ini":
return '${value.toInt()}h';
case "Minggu Ini":
final days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
return days[value.toInt()];
case "Bulan Ini":
return 'M${value.toInt() + 1}';
default:
return '';
}
}

View File

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
part 'wind_speed_event.dart';
part 'wind_speed_state.dart';
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
final MonitoringRepository _repository;
WindSpeedBloc({required MonitoringRepository repository})
: _repository = repository,
super(const WindSpeedState()) {
// Handler saat aplikasi minta mulai monitoring
on<WatchWindSpeedStarted>(
(event, emit) async {
emit(state.copyWith(isLoading: true));
await emit.forEach<MyWindSpeed>(
_repository.getSensorStream(
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json)),
onData: (data) {
// Mengambil list lama dan menambah data baru untuk grafik
final updatedSpeeds = List<double>.from(state.dailySpeeds)
..add(data.speed);
// Batasi jumlah data di grafik (misal cuma simpan 20 data terakhir agar tidak berat)
if (updatedSpeeds.length > 20) {
updatedSpeeds.removeAt(0);
}
return state.copyWith(
currentSpeed: data.speed,
dailySpeeds: updatedSpeeds, // Update list grafik
isLoading: false,
);
},
onError: (error, stackTrace) => state.copyWith(isLoading: false),
); // emit forEach
},
transformer: restartable(),
);
on<WindSpeedPeriodChanged>((event, emit) {
emit(state.copyWith(selectedPeriod: event.period));
});
}
@override
Future<void> close() {
return super.close();
}
}

View File

@ -0,0 +1,24 @@
part of 'wind_speed_bloc.dart';
sealed class WindSpeedEvent extends Equatable {
const WindSpeedEvent();
@override
List<Object> get props => [];
}
// Perintah untuk mulai dengerin data Firebase
class WatchWindSpeedStarted extends WindSpeedEvent {}
// Perintah kalau user ganti filter (Hari Ini, Minggu Ini, dll)
class WindSpeedPeriodChanged extends WindSpeedEvent {
final String period;
const WindSpeedPeriodChanged(this.period);
}
// Event internal: saat data baru masuk dari Firebase
class _WindSpeedUpdated extends WindSpeedEvent {
final MyWindSpeed data;
const _WindSpeedUpdated(this.data);
@override
List<Object> get props => [data];
}

View File

@ -0,0 +1,33 @@
part of 'wind_speed_bloc.dart';
class WindSpeedState extends Equatable {
final double currentSpeed;
final String selectedPeriod;
final List<double> dailySpeeds; // Untuk grafik
final bool isLoading;
const WindSpeedState({
this.currentSpeed = 0.0,
this.selectedPeriod = "Hari Ini",
this.dailySpeeds = const [],
this.isLoading = true,
});
WindSpeedState copyWith({
double? currentSpeed,
String? selectedPeriod,
List<double>? dailySpeeds,
bool? isLoading,
}) {
return WindSpeedState(
currentSpeed: currentSpeed ?? this.currentSpeed,
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object> get props =>
[currentSpeed, selectedPeriod, dailySpeeds, isLoading];
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/wind_speed_bloc.dart';
class WindSpeedScreen extends StatelessWidget {
const WindSpeedScreen({super.key});
@override
Widget build(BuildContext context) {
// Pastikan MonitoringRepository sudah diprovide di atas screen ini
return Scaffold(
appBar: AppBar(title: const Text("Wind Speed Monitoring")),
body: BlocBuilder<WindSpeedBloc, WindSpeedState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Card Informasi Kecepatan Saat Ini
_buildCurrentSpeedCard(state.currentSpeed),
const SizedBox(height: 30),
// Area Grafik
const Text("Grafik Kecepatan Angin",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
SizedBox(
height: 300,
child: LineChart(
_mainData(state.dailySpeeds),
),
),
],
),
);
},
),
);
}
Widget _buildCurrentSpeedCard(double speed) {
return Card(
elevation: 4,
child: ListTile(
leading: const Icon(Icons.wind_power, color: Colors.blue, size: 40),
title: const Text("Kecepatan Saat Ini"),
trailing: Text("${speed.toStringAsFixed(2)} m/s",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
),
);
}
LineChartData _mainData(List<double> speeds) {
return LineChartData(
gridData: const FlGridData(show: true),
titlesData: const FlTitlesData(show: true), // Bisa dikustom nanti
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
// Mengubah List<double> menjadi titik koordinat grafik (x, y)
spots: speeds.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
}).toList(),
isCurved: true, // Biar garisnya melengkung halus
color: Colors.blue,
barWidth: 3,
dotData: const FlDotData(show: false), // Sembunyikan titik per data
belowBarData: BarAreaData(
show: true,
color: Colors.blue.withOpacity(0.2), // Efek bayangan di bawah garis
),
),
],
);
}
}

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -8,11 +8,13 @@ import Foundation
import cloud_firestore
import firebase_auth
import firebase_core
import firebase_database
import google_sign_in_ios
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
}

View File

@ -1,26 +0,0 @@
name: module_repository
description: The Repo that handles modules
publish_to: 'none'
version: 0.0.1
environment:
sdk: '>=3.1.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# equatable: ^2.0.5
# firebase_auth: ^4.16.0
# cloud_firestore: ^4.9.2
# firebase_storage: ^11.6.0
# rxdart: ^0.27.7
# path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true

View File

@ -0,0 +1,5 @@
library monitoring_repository;
export 'src/monitoring_repo.dart';
export 'src/firebase_monitoring_repo.dart';
export 'src/models/models.dart';

View File

@ -0,0 +1,41 @@
// import 'dart:developer';
import 'dart:async';
// import 'package:rxdart/rxdart.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
/// === ambil data dari realtime database firebase === ///
class FirebaseMonitoringRepo implements MonitoringRepository {
final FirebaseDatabase _db = FirebaseDatabase.instance;
// Satu fungsi untuk semua jenis sensor
// Kamu cukup masukkan "path" database-nya saja
@override
Stream<T> getSensorStream<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
) {
/// === ambil data mentah dari firebase === ///
return _db.ref(path).onValue.map((event) {
final Object? value = event.snapshot.value;
if (value is Map) {
return mapper(value);
} else {
// Jika data kosong atau bukan Map, berikan Map kosong agar mapper tidak crash
return mapper({});
}
});
}
@override
// Jika ingin mengambil data sekali saja (bukan stream)
Future<T> getSensorSnapshot<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
) async {
final snapshot = await _db.ref(path).get();
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
return mapper(data);
}
}

View File

@ -0,0 +1,3 @@
export 'wind_speed.dart';
export 'evaporasi.dart';
export 'pressure_sensor.dart';

View File

@ -0,0 +1,21 @@
class MyWindSpeed {
double speed;
int pulse;
DateTime timestamp;
MyWindSpeed({
required this.speed,
required this.pulse,
required this.timestamp,
});
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
return MyWindSpeed(
speed: (json['speed'] ?? 0).toDouble(),
pulse: (json['pulse'] ?? 0) as int,
timestamp: DateTime.fromMillisecondsSinceEpoch(
(json['timestamp'] ?? 0) * 1000, // kalau dari Unix detik
).toLocal(),
);
}
}

View File

@ -0,0 +1,13 @@
import 'models/models.dart';
abstract class MonitoringRepository {
Stream<T> getSensorStream<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
Future<T> getSensorSnapshot<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
}

View File

@ -1,14 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746
url: "https://pub.dev"
source: hosted
version: "1.3.68"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
@ -49,6 +57,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
firebase_core:
dependency: transitive
description:
name: firebase_core
sha256: "2f988dab915efde3b3105268dbd69efce0e8570d767a218ccd914afd0c10c8cc"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "1399ab1f0ac3b503d8a9be64a4c997fc066bbf33f701f42866e5569f26205ebe"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
firebase_database:
dependency: "direct main"
description:
name: firebase_database
sha256: "05358855166c943a2fe9321bce69ab6b583c9a49e70e01b5297ef03f8bea50b9"
url: "https://pub.dev"
source: hosted
version: "12.2.0"
firebase_database_platform_interface:
dependency: transitive
description:
name: firebase_database_platform_interface
sha256: "83278cc315ad0f36a5e30fc7f3d1c6edc722340347d41ea17f57e52176bb98f8"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
firebase_database_web:
dependency: transitive
description:
name: firebase_database_web
sha256: "77df7f7c59f4bba33f06644f76e28a8acc594487f1aaa4c79947abf491a7ae70"
url: "https://pub.dev"
source: hosted
version: "0.2.7+5"
flutter:
dependency: "direct main"
description: flutter
@ -58,15 +114,20 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
@ -95,10 +156,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "6.1.0"
matcher:
dependency: transitive
description:
@ -131,6 +192,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
rxdart:
dependency: "direct main"
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
url: "https://pub.dev"
source: hosted
version: "0.27.7"
sky_engine:
dependency: transitive
description: flutter
@ -140,10 +217,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
stack_trace:
dependency: transitive
description:
@ -200,6 +277,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks:
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.22.0"

View File

@ -0,0 +1,56 @@
name: monitoring_repository
description: 'A new Flutter package project.'
version: 0.0.1
homepage:
environment:
sdk: ^3.10.7
flutter: '>=1.17.0'
dependencies:
flutter:
sdk: flutter
rxdart: ^0.27.7
firebase_database: ^12.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

View File

@ -12,6 +12,7 @@ class FirebaseUserRepo implements UserRepository {
FirebaseAuth? firebaseAuth,
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
/// == Melakukan Implement User == ///
@override
Stream<MyUser?> get user {
return _firebaseAuth.authStateChanges().flatMap((firebaseUser) async* {
@ -21,13 +22,15 @@ class FirebaseUserRepo implements UserRepository {
try {
final userData = await userCollection.doc(firebaseUser.uid).get();
if (userData.exists && userData.data() != null) {
yield MyUser.fromEntity(MyUserEntity.fromDocument(userData.data()!));
yield MyUser.fromEntity(
MyUserEntity.fromDocument(userData.data()!));
} else {
// User baru yang belum ada data di Firestore, tunggu sebentar
await Future.delayed(const Duration(milliseconds: 500));
final retryData = await userCollection.doc(firebaseUser.uid).get();
if (retryData.exists && retryData.data() != null) {
yield MyUser.fromEntity(MyUserEntity.fromDocument(retryData.data()!));
yield MyUser.fromEntity(
MyUserEntity.fromDocument(retryData.data()!));
} else {
yield MyUser.empty;
}
@ -40,34 +43,33 @@ class FirebaseUserRepo implements UserRepository {
});
}
@override
/// == Melakukan Implement Sign in == ///
@override
Future<void> signIn(String email, String password) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password
);
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
} catch (e) {
log(e.toString());
log(e.toString());
rethrow;
}
}
/// == Melakukan Implement Sign Up == ///
@override
Future<MyUser> signUp(MyUser myUser, String password) async {
try {
try {
UserCredential user = await _firebaseAuth.createUserWithEmailAndPassword(
email: myUser.email,
password: password
);
myUser.userId = user.user!.uid;
return myUser;
email: myUser.email, password: password);
myUser.userId = user.user!.uid;
return myUser;
} catch (e) {
log(e.toString());
rethrow;
}
}
}
/// == Melakukan Implement Log Out == ///
@override
Future<void> logOut() async {
await _firebaseAuth.signOut();
@ -77,8 +79,8 @@ class FirebaseUserRepo implements UserRepository {
Future<void> setUserData(MyUser myUser) async {
try {
await userCollection
.doc(myUser.userId)
.set(myUser.toEntity().toDocument());
.doc(myUser.userId)
.set(myUser.toEntity().toDocument());
// Tunggu sebentar agar data tersimpan dengan baik sebelum stream update
await Future.delayed(const Duration(milliseconds: 500));
} catch (e) {
@ -87,6 +89,34 @@ class FirebaseUserRepo implements UserRepository {
}
}
/// == Sign in with Google == ///
@override
Future<void> signInWithGoogle() async {
try {
final GoogleAuthProvider googleProvider = GoogleAuthProvider();
final UserCredential userCredential =
await _firebaseAuth.signInWithPopup(googleProvider);
final User? firebaseUser = userCredential.user;
if (firebaseUser != null) {
// Check if user data already exists in Firestore
final existingUser = await userCollection.doc(firebaseUser.uid).get();
}
if (!existingUser.exists) {
// Create new user data from Google account info
final newUser = MyUser(
userId: firebaseUser.uid,
email: firebaseUser.email ?? '',
name: firebaseUser.displayName ?? 'User',
hasActiveCart: false,
);
await setUserData(newUser);
}
}
} catch (e) {
log('Google sign-in error: $e');
rethrow;
}
}
}

View File

@ -3,11 +3,13 @@ import 'models/models.dart';
abstract class UserRepository {
Stream<MyUser?> get user;
Future<MyUser> signUp(MyUser myUser, String password);
Future<MyUser> signUp(MyUser myUser, String password);
Future<void> setUserData(MyUser user);
Future<void> setUserData(MyUser user);
Future<void> signIn(String email, String password);
Future<void> signIn(String email, String password);
Future<void> logOut();
}
Future<void> signInWithGoogle();
Future<void> logOut();
}

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
url: "https://pub.dev"
source: hosted
version: "1.3.35"
version: "1.3.66"
async:
dependency: transitive
description:
@ -45,26 +45,26 @@ packages:
dependency: "direct main"
description:
name: cloud_firestore
sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185
sha256: "54484b2fc49f41b46f35b60a54b12351181eeaad22c0e3def276a81e17ae7c9b"
url: "https://pub.dev"
source: hosted
version: "4.17.5"
version: "6.1.2"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81"
sha256: dfaa8b2c0d0a824af289d4159816a5c78417feec264c2194081d645687195158
url: "https://pub.dev"
source: hosted
version: "6.2.5"
version: "7.0.6"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9"
sha256: "35d01f502b3b701d700470d32a8f82704dac8341a66e86c074900cde5bab343d"
url: "https://pub.dev"
source: hosted
version: "3.12.5"
version: "5.1.2"
collection:
dependency: transitive
description:
@ -85,50 +85,50 @@ packages:
dependency: "direct main"
description:
name: firebase_auth
sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba
sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172
url: "https://pub.dev"
source: hosted
version: "4.20.0"
version: "6.1.4"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093
sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72
url: "https://pub.dev"
source: hosted
version: "7.3.0"
version: "8.1.6"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b"
sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40
url: "https://pub.dev"
source: hosted
version: "5.12.0"
version: "6.1.2"
firebase_core:
dependency: transitive
description:
name: firebase_core
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
url: "https://pub.dev"
source: hosted
version: "2.32.0"
version: "4.4.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb"
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
url: "https://pub.dev"
source: hosted
version: "5.4.2"
version: "6.0.2"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
url: "https://pub.dev"
source: hosted
version: "2.17.5"
version: "3.4.0"
flutter:
dependency: "direct main"
description: flutter
@ -138,10 +138,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
@ -152,6 +152,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
@ -188,10 +196,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "3.0.0"
matcher:
dependency: transitive
description:
@ -321,10 +329,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.1.1"
sdks:
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.22.0"

View File

@ -5,10 +5,18 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
url: "https://pub.dev"
source: hosted
version: "1.3.65"
version: "1.3.66"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@ -25,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.4"
bloc_concurrency:
dependency: "direct main"
description:
name: bloc_concurrency
sha256: "456b7a3616a7c1ceb975c14441b3f198bf57d81cb95b7c6de5cb0c60201afcd8"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
boolean_selector:
dependency: transitive
description:
@ -73,6 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
@ -81,6 +105,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@ -105,6 +137,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
firebase_auth:
dependency: "direct main"
description:
@ -133,10 +181,10 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9"
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
version: "4.4.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -149,10 +197,42 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
version: "3.4.0"
firebase_database:
dependency: "direct main"
description:
name: firebase_database
sha256: f13676fab84671a808eb5eaeecfa2bcb5a3fed2802798c0dd16cb786139a6492
url: "https://pub.dev"
source: hosted
version: "12.1.3"
firebase_database_platform_interface:
dependency: transitive
description:
name: firebase_database_platform_interface
sha256: "2444ec9999a7295b0e7f3fea60105dbbe72fdfad5a9e4c93e72d0b51c181f128"
url: "https://pub.dev"
source: hosted
version: "0.3.0+2"
firebase_database_web:
dependency: transitive
description:
name: firebase_database_web
sha256: "211b6deff7ac373bb539f079d909799e9231397daddc9e0f1768f50f6216f034"
url: "https://pub.dev"
source: hosted
version: "0.2.7+3"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@ -184,6 +264,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e
url: "https://pub.dev"
source: hosted
version: "8.0.2"
google_identity_services_web:
dependency: transitive
description:
@ -232,6 +328,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
url: "https://pub.dev"
source: hosted
version: "1.0.2"
http:
dependency: transitive
description:
@ -248,6 +352,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
leak_tracker:
dependency: transitive
description:
@ -280,6 +408,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@ -304,6 +440,21 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
monitoring_repository:
dependency: "direct main"
description:
path: "packages/monitoring_repository"
relative: true
source: path
version: "0.0.1"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nested:
dependency: transitive
description:
@ -312,6 +463,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
@ -320,6 +487,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
@ -336,6 +559,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
rxdart:
dependency: "direct main"
description:
@ -373,6 +604,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -436,6 +675,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.4"

View File

@ -1,29 +1,36 @@
name: klimatologiot
description: "A new Flutter project."
description: 'A new Flutter project.'
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.1.0
dependencies:
bloc: ^8.1.0
bloc_concurrency: ^0.2.5
cloud_firestore: ^6.1.1
cupertino_icons: ^1.0.8
equatable: ^2.0.5
firebase_auth: ^6.1.3
firebase_core: ^4.3.0
firebase_database: ^12.1.3
fl_chart: ^1.1.1
flutter:
sdk: flutter
firebase_core: ^4.3.0
firebase_auth: ^6.1.3
cloud_firestore: ^6.1.1
rxdart: ^0.27.7
google_sign_in: ^7.2.0
bloc: ^8.1.0
flutter_bloc: ^8.1.0
equatable: ^2.0.5
google_fonts: ^8.0.2
google_sign_in: ^7.2.0
intl: ^0.20.2
monitoring_repository:
path: packages/monitoring_repository
rxdart: ^0.27.7
user_repository:
path: packages/user_repository
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_lints: ^3.0.0
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
# assets:
# - assets/images/
assets:
- images/

View File

@ -5,15 +5,80 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'package:user_repository/user_repository.dart';
import '../lib/app.dart';
import 'package:klimatologiot/main.dart';
// import 'package:klimatologiot/main.dart';
class FakeUserRepository implements UserRepository {
@override
Stream<MyUser?> get user => const Stream.empty();
@override
Future<void> logOut() async {}
@override
Future<void> setUserData(MyUser user) {
// TODO: implement setUserData
throw UnimplementedError();
}
@override
Future<void> signIn(String email, String password) {
// TODO: implement signIn
throw UnimplementedError();
}
@override
Future<void> signInWithGoogle() {
// TODO: implement signInWithGoogle
throw UnimplementedError();
}
@override
Future<MyUser> signUp(MyUser myUser, String password) {
// TODO: implement signUp
throw UnimplementedError();
}
// tambahin kalau ada method lain
}
class FakeMonitoringRepository implements MonitoringRepository {
get _db => null;
// Satu fungsi untuk semua jenis sensor
// Kamu cukup masukkan "path" database-nya saja
@override
// Jika ingin mengambil data sekali saja (bukan stream)
Future<DataSnapshot> getSensorSnapshot(String path) async {
return await _db.ref(path).get();
}
@override
Stream<DatabaseEvent> getSensorStream(String path) {
// TODO: implement getSensorStream
throw UnimplementedError();
}
}
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
testWidgets('App loads', (WidgetTester tester) async {
final fakeRepo = FakeUserRepository();
final fakeMonitoring = FakeUserRepository();
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(
MyApp(fakeRepo, fakeMonitoring as MonitoringRepository),
);
await tester.pump();
expect(find.byType(MaterialApp), findsOneWidget);
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<!--
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
@ -14,25 +14,31 @@
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="klimatologiot">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="klimatologiot" />
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png" />
<title>klimatologiot</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
<title>klimatologiot</title>
<link rel="manifest" href="manifest.json" />
<!-- Google -->
<meta
name="google-signin-client_id"
content="219079385989-8hkp1kg2pprs5k269s4c08r6rpdj1gjd.apps.googleusercontent.com"
/>
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)