Merge branch 'branch_percobaan'
This commit is contained in:
commit
e91252478e
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
25
lib/app.dart
25
lib/app.dart
|
|
@ -2,19 +2,28 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:klimatologiot/app_view.dart';
|
import 'package:klimatologiot/app_view.dart';
|
||||||
import 'package:user_repository/user_repository.dart';
|
import 'package:user_repository/user_repository.dart';
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import 'blocs/authentication_bloc/authentication_bloc.dart';
|
import 'blocs/authentication_bloc/authentication_bloc.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final UserRepository userRepository;
|
final UserRepository userRepository;
|
||||||
const MyApp(this.userRepository, {super.key});
|
final MonitoringRepository monitoringRepository;
|
||||||
|
const MyApp(this.userRepository, this.monitoringRepository, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RepositoryProvider<AuthenticationBloc>(
|
return MultiRepositoryProvider(
|
||||||
create: (context) => AuthenticationBloc(
|
providers: [
|
||||||
userRepository: userRepository
|
// 1. Daftarkan UserRepository
|
||||||
),
|
RepositoryProvider<UserRepository>.value(value: userRepository),
|
||||||
child: const MyAppView(),
|
RepositoryProvider<MonitoringRepository>.value(
|
||||||
);
|
value: monitoringRepository),
|
||||||
|
],
|
||||||
|
child: BlocProvider<AuthenticationBloc>(
|
||||||
|
create: (context) => AuthenticationBloc(
|
||||||
|
userRepository: userRepository,
|
||||||
|
),
|
||||||
|
child: const MyAppView(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,13 @@ class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState>
|
||||||
emit(AuthenticationState.unauthenticated());
|
emit(AuthenticationState.unauthenticated());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
on<AuthenticationLogoutRequested>((event, emit) async {
|
||||||
|
try {
|
||||||
|
await userRepository.logOut();
|
||||||
|
} catch (_) {
|
||||||
|
// ignore errors here; stream will update state accordingly
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,8 @@ class AuthenticationUserChanged extends AuthenticationEvent {
|
||||||
final MyUser? user;
|
final MyUser? user;
|
||||||
|
|
||||||
const AuthenticationUserChanged(this.user);
|
const AuthenticationUserChanged(this.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationLogoutRequested extends AuthenticationEvent {
|
||||||
|
const AuthenticationLogoutRequested();
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:klimatologiot/simple_bloc_observer.dart';
|
import 'package:klimatologiot/simple_bloc_observer.dart';
|
||||||
import 'package:klimatologiot/app.dart';
|
import 'package:klimatologiot/app.dart';
|
||||||
import 'package:user_repository/user_repository.dart';
|
import 'package:user_repository/user_repository.dart';
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
|
@ -12,5 +13,8 @@ void main() async {
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
);
|
);
|
||||||
Bloc.observer = SimpleBlocObserver();
|
Bloc.observer = SimpleBlocObserver();
|
||||||
runApp(MyApp(FirebaseUserRepo()));
|
runApp(MyApp(
|
||||||
}
|
FirebaseUserRepo(),
|
||||||
|
FirebaseMonitoringRepo(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ part 'sign_in_state.dart';
|
||||||
class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
||||||
final UserRepository _userRepository;
|
final UserRepository _userRepository;
|
||||||
|
|
||||||
SignInBloc(
|
SignInBloc(this._userRepository) : super(SignInInitial()) {
|
||||||
this._userRepository
|
|
||||||
) : super(SignInInitial()) {
|
|
||||||
on<SignInRequired>((event, emit) async {
|
on<SignInRequired>((event, emit) async {
|
||||||
emit(SignInProcess());
|
emit(SignInProcess());
|
||||||
try {
|
try {
|
||||||
|
|
@ -19,7 +17,17 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
||||||
emit(SignInFailure());
|
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());
|
on<SignOutRequired>((event, emit) async => await _userRepository.logOut());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@ class SignInRequired extends SignInEvent {
|
||||||
List<Object> get props => [email, password];
|
List<Object> get props => [email, password];
|
||||||
}
|
}
|
||||||
|
|
||||||
class SignOutRequired extends SignInEvent {
|
class GoogleSignInRequired extends SignInEvent {}
|
||||||
|
|
||||||
}
|
class SignOutRequired extends SignInEvent {}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
|
||||||
on<SignUpRequired>((event, emit) async {
|
on<SignUpRequired>((event, emit) async {
|
||||||
emit(SignUpProcess());
|
emit(SignUpProcess());
|
||||||
try {
|
try {
|
||||||
MyUser myUser = await _userRepository.signUp(event.user, event.password);
|
MyUser myUser =
|
||||||
|
await _userRepository.signUp(event.user, event.password);
|
||||||
await _userRepository.setUserData(myUser);
|
await _userRepository.setUserData(myUser);
|
||||||
emit(SignUpSuccess());
|
emit(SignUpSuccess());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(SignUpFailure());
|
emit(SignUpFailure(e.toString()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,6 @@ class SignUpRequired extends SignUpEvent {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [user, password];
|
List<Object> get props => [user, password];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GoogleSignInRequired extends SignUpEvent {}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,18 @@ sealed class SignUpState extends Equatable {
|
||||||
const SignUpState();
|
const SignUpState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [];
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SignUpInitial extends SignUpState {}
|
final class SignUpInitial extends SignUpState {}
|
||||||
|
|
||||||
final class SignUpSuccess 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 {}
|
final class SignUpProcess extends SignUpState {}
|
||||||
|
|
@ -13,125 +13,153 @@ class SignInScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SignInScreenState extends State<SignInScreen> {
|
class _SignInScreenState extends State<SignInScreen> {
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
final emailController = TextEditingController();
|
final emailController = TextEditingController();
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool signInRequired = false;
|
bool signInRequired = false;
|
||||||
IconData iconPassword = CupertinoIcons.eye_fill;
|
IconData iconPassword = CupertinoIcons.eye_fill;
|
||||||
bool obscurePassword = true;
|
bool obscurePassword = true;
|
||||||
String? _errorMsg;
|
String? _errorMsg;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<SignInBloc, SignInState>(
|
return BlocListener<SignInBloc, SignInState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if(state is SignInSuccess) {
|
if (state is SignInProcess) {
|
||||||
setState(() {
|
setState(() {
|
||||||
signInRequired = false;
|
signInRequired = true;
|
||||||
});
|
});
|
||||||
} else if(state is SignInProcess) {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
signInRequired = true;
|
signInRequired = false;
|
||||||
});
|
});
|
||||||
} else if(state is SignInFailure) {
|
if (state is SignInFailure) {
|
||||||
setState(() {
|
setState(() {
|
||||||
signInRequired = false;
|
// PERUBAHAN: Pesan error lebih umum karena Google gagal belum tentu soal password
|
||||||
_errorMsg = 'Invalid email or password';
|
_errorMsg = 'Sign in failed. Please try again.';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: Form(
|
},
|
||||||
key: _formKey,
|
child: Form(
|
||||||
child: Column(
|
key: _formKey,
|
||||||
children: [
|
child: Column(
|
||||||
const SizedBox(height: 20),
|
children: [
|
||||||
SizedBox(
|
const SizedBox(height: 20),
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
SizedBox(
|
||||||
child: MyTextField(
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
controller: emailController,
|
child: MyTextField(
|
||||||
hintText: 'Email',
|
controller: emailController,
|
||||||
obscureText: false,
|
hintText: 'Email',
|
||||||
keyboardType: TextInputType.emailAddress,
|
obscureText: false,
|
||||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
keyboardType: TextInputType.emailAddress,
|
||||||
errorMsg: _errorMsg,
|
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||||
validator: (val) {
|
errorMsg: _errorMsg,
|
||||||
if (val!.isEmpty) {
|
validator: (val) {
|
||||||
return 'Please fill in this field';
|
if (val!.isEmpty) {
|
||||||
}
|
return 'Please fill in this field';
|
||||||
return null;
|
}
|
||||||
}
|
return null;
|
||||||
)
|
})),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
SizedBox(
|
||||||
SizedBox(
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
child: MyTextField(
|
||||||
child: MyTextField(
|
controller: passwordController,
|
||||||
controller: passwordController,
|
hintText: 'Password',
|
||||||
hintText: 'Password',
|
obscureText: obscurePassword,
|
||||||
obscureText: obscurePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
keyboardType: TextInputType.visiblePassword,
|
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
errorMsg: _errorMsg,
|
||||||
errorMsg: _errorMsg,
|
validator: (val) {
|
||||||
validator: (val) {
|
if (val!.isEmpty) {
|
||||||
if (val!.isEmpty) {
|
return 'Please fill in this field';
|
||||||
return 'Please fill in this field';
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
|
||||||
},
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
obscurePassword = !obscurePassword;
|
|
||||||
if(obscurePassword) {
|
|
||||||
iconPassword = CupertinoIcons.eye_fill;
|
|
||||||
} else {
|
|
||||||
iconPassword = CupertinoIcons.eye_slash_fill;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
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),
|
||||||
const SizedBox(height: 20),
|
!signInRequired
|
||||||
!signInRequired
|
? SizedBox(
|
||||||
? SizedBox(
|
width: MediaQuery.of(context).size.width * 0.5,
|
||||||
width: MediaQuery.of(context).size.width * 0.5,
|
child: TextButton(
|
||||||
child: TextButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
if (_formKey.currentState!.validate()) {
|
||||||
if (_formKey.currentState!.validate()) {
|
context.read<SignInBloc>().add(SignInRequired(
|
||||||
context.read<SignInBloc>().add(SignInRequired(
|
emailController.text,
|
||||||
emailController.text,
|
passwordController.text));
|
||||||
passwordController.text)
|
}
|
||||||
);
|
},
|
||||||
}
|
style: TextButton.styleFrom(
|
||||||
},
|
elevation: 3.0,
|
||||||
style: TextButton.styleFrom(
|
backgroundColor:
|
||||||
elevation: 3.0,
|
Theme.of(context).colorScheme.primary,
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
foregroundColor: Colors.white,
|
||||||
foregroundColor: Colors.white,
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.circular(60))),
|
||||||
borderRadius: BorderRadius.circular(60)
|
child: const Padding(
|
||||||
)
|
padding: EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 25, vertical: 5),
|
||||||
child: const Padding(
|
child: Text(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 25, vertical: 5),
|
'Sign In',
|
||||||
child: Text(
|
textAlign: TextAlign.center,
|
||||||
'Sign In',
|
style: TextStyle(
|
||||||
textAlign: TextAlign.center,
|
color: Colors.white,
|
||||||
style: TextStyle(
|
fontSize: 16,
|
||||||
color: Colors.white,
|
fontWeight: FontWeight.w600),
|
||||||
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
label: const Text(
|
||||||
: const CircularProgressIndicator(),
|
'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))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@ import 'package:user_repository/user_repository.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../components/my_text_field.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 {
|
class SignUpScreen extends StatefulWidget {
|
||||||
const SignUpScreen({super.key});
|
final VoidCallback? onSignInTap;
|
||||||
|
const SignUpScreen({super.key, this.onSignInTap});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SignUpScreen> createState() => _SignUpScreenState();
|
State<SignUpScreen> createState() => _SignUpScreenState();
|
||||||
|
|
@ -23,175 +26,179 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||||
bool signUpRequired = false;
|
bool signUpRequired = false;
|
||||||
|
|
||||||
bool containsUpperCase = false;
|
bool containsUpperCase = false;
|
||||||
bool containsLowerCase = false;
|
bool containsLowerCase = false;
|
||||||
bool containsNumber = false;
|
bool containsNumber = false;
|
||||||
bool containsSpecialChar = false;
|
bool containsSpecialChar = false;
|
||||||
bool contains8Length = false;
|
bool contains8Length = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<SignUpBloc, SignUpState>(
|
return BlocListener<signUp.SignUpBloc, signUp.SignUpState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is SignUpSuccess) {
|
if (state is signUp.SignUpProcess) {
|
||||||
setState(() {
|
setState(() {
|
||||||
signUpRequired = false;
|
signUpRequired = true;
|
||||||
});
|
});
|
||||||
// User sudah terdaftar dan auth state akan otomatis update via AuthenticationBloc
|
} else if (state is signUp.SignUpFailure) {
|
||||||
// Tungur sebentar agar Firestore selesai menyimpan data
|
setState(() {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
signUpRequired = false; // Matikan loading!
|
||||||
const SnackBar(content: Text('Registrasi berhasil! Mengarahkan...')),
|
});
|
||||||
);
|
// Tampilkan pesan error lewat Snackbar biar user tahu kenapa gagal
|
||||||
} else if (state is SignUpProcess) {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
setState(() {
|
SnackBar(
|
||||||
signUpRequired = true;
|
content: Text(state.message ?? 'An error ocurred'),
|
||||||
});
|
backgroundColor: Colors.red),
|
||||||
} else if (state is SignUpFailure) {
|
);
|
||||||
setState(() {
|
} else if (state is signUp.SignUpSuccess) {
|
||||||
signUpRequired = false;
|
setState(() {
|
||||||
});
|
signUpRequired = false;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
});
|
||||||
const SnackBar(content: Text('Registrasi gagal, silakan coba lagi')),
|
}
|
||||||
);
|
},
|
||||||
}
|
child: Form(
|
||||||
},
|
key: _formKey,
|
||||||
child: Form(
|
child: Scaffold(
|
||||||
key: _formKey,
|
backgroundColor: Colors.white,
|
||||||
child: Scaffold(
|
body: Center(
|
||||||
backgroundColor: Colors.white,
|
child: SingleChildScrollView(
|
||||||
body: Center(
|
child: Column(
|
||||||
child: SingleChildScrollView(
|
children: [
|
||||||
child: Column(
|
const SizedBox(height: 30),
|
||||||
children: [
|
Container(
|
||||||
const SizedBox(height: 30),
|
width: MediaQuery.of(context).size.width * 0.92,
|
||||||
Container(
|
padding: const EdgeInsets.symmetric(
|
||||||
width: MediaQuery.of(context).size.width * 0.92,
|
horizontal: 20, vertical: 22),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Colors.white,
|
||||||
color: Colors.white,
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderRadius: BorderRadius.circular(12),
|
boxShadow: [
|
||||||
boxShadow: [
|
BoxShadow(
|
||||||
BoxShadow(
|
color: Colors.black.withOpacity(0.04),
|
||||||
color: Colors.black.withOpacity(0.04),
|
blurRadius: 10,
|
||||||
blurRadius: 10,
|
offset: const Offset(0, 4),
|
||||||
offset: const Offset(0, 4),
|
)
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 6),
|
const Center(
|
||||||
const Center(
|
child: Text(
|
||||||
child: Text(
|
'Buat Akun',
|
||||||
'Buat Akun',
|
style: TextStyle(
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
fontSize: 22, fontWeight: FontWeight.w700),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
),
|
||||||
const Center(
|
const SizedBox(height: 6),
|
||||||
child: Text(
|
const Center(
|
||||||
'Daftar untuk mulai monitoring',
|
child: Text(
|
||||||
style: TextStyle(fontSize: 13, color: Colors.black54),
|
'Daftar untuk mulai monitoring',
|
||||||
),
|
style:
|
||||||
|
TextStyle(fontSize: 13, color: Colors.black54),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
child: MyTextField(
|
child: MyTextField(
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
hintText: 'Masukkan nama lengkap',
|
hintText: 'Masukkan nama lengkap',
|
||||||
obscureText: false,
|
obscureText: false,
|
||||||
keyboardType: TextInputType.name,
|
keyboardType: TextInputType.name,
|
||||||
prefixIcon: const Icon(CupertinoIcons.person_fill),
|
prefixIcon: const Icon(CupertinoIcons.person_fill),
|
||||||
validator: (val) {
|
validator: (val) {
|
||||||
if (val == null || val.isEmpty) {
|
if (val == null || val.isEmpty) {
|
||||||
return 'Please fill in this field';
|
return 'Please fill in this field';
|
||||||
} else if (val.length > 50) {
|
} else if (val.length > 50) {
|
||||||
return 'Name too long';
|
return 'Name too long';
|
||||||
}
|
}
|
||||||
return null;
|
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
|
// Confirm Password
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
child: MyTextField(
|
child: MyTextField(
|
||||||
controller: emailController,
|
controller: confirmPasswordController,
|
||||||
hintText: 'nama@email.com',
|
hintText: 'Ulangi password',
|
||||||
obscureText: false,
|
obscureText: obscurePassword,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||||
validator: (val) {
|
validator: (val) {
|
||||||
if (val == null || val.isEmpty) {
|
if (val == null || val.isEmpty) {
|
||||||
return 'Please fill in this field';
|
return 'Please fill in this field';
|
||||||
}
|
} else if (val != passwordController.text) {
|
||||||
return null;
|
return 'Password tidak cocok';
|
||||||
},
|
}
|
||||||
),
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
|
|
||||||
// Password
|
const SizedBox(height: 18),
|
||||||
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),
|
|
||||||
|
|
||||||
// Confirm Password
|
/// == Sign Up == ///
|
||||||
SizedBox(
|
// Button
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
!signUpRequired
|
||||||
child: MyTextField(
|
? Center(
|
||||||
controller: confirmPasswordController,
|
child: SizedBox(
|
||||||
hintText: 'Ulangi password',
|
width:
|
||||||
obscureText: obscurePassword,
|
MediaQuery.of(context).size.width * 0.5,
|
||||||
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,
|
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
|
|
@ -200,8 +207,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||||
myUser.name = nameController.text;
|
myUser.name = nameController.text;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
context.read<SignUpBloc>().add(
|
context.read<signUp.SignUpBloc>().add(
|
||||||
SignUpRequired(
|
signUp.SignUpRequired(
|
||||||
myUser,
|
myUser,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
),
|
),
|
||||||
|
|
@ -209,47 +216,62 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
backgroundColor: Colors.black87,
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(60),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Padding(
|
||||||
'Daftar',
|
padding: EdgeInsets.symmetric(
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
horizontal: 25, vertical: 5),
|
||||||
|
child: const Text(
|
||||||
|
'Sign Up',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const CircularProgressIndicator(),
|
)
|
||||||
|
: const CircularProgressIndicator(),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Center(
|
Center(
|
||||||
child: Row(
|
child: Text('Atau'),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
children: [
|
|
||||||
const Text('Sudah punya akun? '),
|
const SizedBox(height: 12),
|
||||||
TextButton(
|
Center(
|
||||||
onPressed: () {
|
child: Row(
|
||||||
Navigator.of(context).pop();
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
},
|
children: [
|
||||||
child: const Text('Login di sini'),
|
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),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,90 +13,92 @@ class WelcomeScreen extends StatefulWidget {
|
||||||
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WelcomeScreenState extends State<WelcomeScreen> with TickerProviderStateMixin {
|
class _WelcomeScreenState extends State<WelcomeScreen>
|
||||||
late TabController tabController;
|
with TickerProviderStateMixin {
|
||||||
|
late TabController tabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
tabController = TabController(
|
tabController = TabController(initialIndex: 0, length: 2, vsync: this);
|
||||||
initialIndex: 0,
|
|
||||||
length: 2,
|
|
||||||
vsync: this
|
|
||||||
);
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: MediaQuery.of(context).size.height,
|
height: MediaQuery.of(context).size.height,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: MediaQuery.of(context).size.height / 1.8,
|
height: MediaQuery.of(context).size.height / 1.8,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
unselectedLabelColor: Theme.of(context).colorScheme.onBackground.withOpacity(0.5),
|
unselectedLabelColor: Theme.of(context)
|
||||||
labelColor: Theme.of(context).colorScheme.onBackground,
|
.colorScheme
|
||||||
tabs: const [
|
.onBackground
|
||||||
Padding(
|
.withOpacity(0.5),
|
||||||
padding: EdgeInsets.all(12.0),
|
labelColor:
|
||||||
child: Text(
|
Theme.of(context).colorScheme.onBackground,
|
||||||
'Sign In',
|
tabs: const [
|
||||||
style: TextStyle(
|
Padding(
|
||||||
fontSize: 18,
|
padding: EdgeInsets.all(12.0),
|
||||||
),
|
child: Text(
|
||||||
),
|
'Sign In',
|
||||||
),
|
style: TextStyle(
|
||||||
Padding(
|
fontSize: 18,
|
||||||
padding: EdgeInsets.all(12.0),
|
),
|
||||||
child: Text(
|
),
|
||||||
'Sign Up',
|
),
|
||||||
style: TextStyle(
|
Padding(
|
||||||
fontSize: 18,
|
padding: EdgeInsets.all(12.0),
|
||||||
),
|
child: Text(
|
||||||
),
|
'Sign Up',
|
||||||
),
|
style: TextStyle(
|
||||||
],
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: TabBarView(
|
],
|
||||||
controller: tabController,
|
),
|
||||||
children: [
|
),
|
||||||
BlocProvider<SignInBloc>(
|
Expanded(
|
||||||
create: (context) => SignInBloc(
|
child: TabBarView(
|
||||||
context.read<AuthenticationBloc>().userRepository
|
controller: tabController,
|
||||||
),
|
children: [
|
||||||
child: const SignInScreen(),
|
BlocProvider<SignInBloc>(
|
||||||
),
|
create: (context) => SignInBloc(context
|
||||||
BlocProvider<SignUpBloc>(
|
.read<AuthenticationBloc>()
|
||||||
create: (context) => SignUpBloc(
|
.userRepository),
|
||||||
context.read<AuthenticationBloc>().userRepository
|
child: const SignInScreen(),
|
||||||
),
|
),
|
||||||
child: const SignUpScreen(),
|
BlocProvider<SignUpBloc>(
|
||||||
),
|
create: (context) => SignUpBloc(context
|
||||||
],
|
.read<AuthenticationBloc>()
|
||||||
)
|
.userRepository),
|
||||||
)
|
child: SignUpScreen(onSignInTap: () {
|
||||||
],
|
tabController.animateTo(
|
||||||
),
|
0); // Ini yang bikin dia pindah ke tab Login
|
||||||
),
|
}),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
))
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,66 @@
|
||||||
import 'package:flutter/material.dart';
|
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{
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen ({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
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"),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import Foundation
|
||||||
import cloud_firestore
|
import cloud_firestore
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
|
import firebase_database
|
||||||
import google_sign_in_ios
|
import google_sign_in_ios
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
|
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
||||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
library monitoring_repository;
|
||||||
|
|
||||||
|
export 'src/monitoring_repo.dart';
|
||||||
|
export 'src/firebase_monitoring_repo.dart';
|
||||||
|
export 'src/models/models.dart';
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export 'wind_speed.dart';
|
||||||
|
export 'evaporasi.dart';
|
||||||
|
export 'pressure_sensor.dart';
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
_flutterfire_internals:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _flutterfire_internals
|
||||||
|
sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.68"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -49,6 +57,54 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -58,15 +114,20 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "6.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -95,10 +156,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "6.1.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -131,6 +192,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -140,10 +217,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -200,6 +277,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.10.7 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.22.0"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -12,6 +12,7 @@ class FirebaseUserRepo implements UserRepository {
|
||||||
FirebaseAuth? firebaseAuth,
|
FirebaseAuth? firebaseAuth,
|
||||||
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
||||||
|
|
||||||
|
/// == Melakukan Implement User == ///
|
||||||
@override
|
@override
|
||||||
Stream<MyUser?> get user {
|
Stream<MyUser?> get user {
|
||||||
return _firebaseAuth.authStateChanges().flatMap((firebaseUser) async* {
|
return _firebaseAuth.authStateChanges().flatMap((firebaseUser) async* {
|
||||||
|
|
@ -21,13 +22,15 @@ class FirebaseUserRepo implements UserRepository {
|
||||||
try {
|
try {
|
||||||
final userData = await userCollection.doc(firebaseUser.uid).get();
|
final userData = await userCollection.doc(firebaseUser.uid).get();
|
||||||
if (userData.exists && userData.data() != null) {
|
if (userData.exists && userData.data() != null) {
|
||||||
yield MyUser.fromEntity(MyUserEntity.fromDocument(userData.data()!));
|
yield MyUser.fromEntity(
|
||||||
|
MyUserEntity.fromDocument(userData.data()!));
|
||||||
} else {
|
} else {
|
||||||
// User baru yang belum ada data di Firestore, tunggu sebentar
|
// User baru yang belum ada data di Firestore, tunggu sebentar
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
final retryData = await userCollection.doc(firebaseUser.uid).get();
|
final retryData = await userCollection.doc(firebaseUser.uid).get();
|
||||||
if (retryData.exists && retryData.data() != null) {
|
if (retryData.exists && retryData.data() != null) {
|
||||||
yield MyUser.fromEntity(MyUserEntity.fromDocument(retryData.data()!));
|
yield MyUser.fromEntity(
|
||||||
|
MyUserEntity.fromDocument(retryData.data()!));
|
||||||
} else {
|
} else {
|
||||||
yield MyUser.empty;
|
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 {
|
Future<void> signIn(String email, String password) async {
|
||||||
try {
|
try {
|
||||||
await _firebaseAuth.signInWithEmailAndPassword(
|
await _firebaseAuth.signInWithEmailAndPassword(
|
||||||
email: email,
|
email: email, password: password);
|
||||||
password: password
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// == Melakukan Implement Sign Up == ///
|
||||||
@override
|
@override
|
||||||
Future<MyUser> signUp(MyUser myUser, String password) async {
|
Future<MyUser> signUp(MyUser myUser, String password) async {
|
||||||
try {
|
try {
|
||||||
UserCredential user = await _firebaseAuth.createUserWithEmailAndPassword(
|
UserCredential user = await _firebaseAuth.createUserWithEmailAndPassword(
|
||||||
email: myUser.email,
|
email: myUser.email, password: password);
|
||||||
password: password
|
myUser.userId = user.user!.uid;
|
||||||
);
|
return myUser;
|
||||||
myUser.userId = user.user!.uid;
|
|
||||||
return myUser;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// == Melakukan Implement Log Out == ///
|
||||||
@override
|
@override
|
||||||
Future<void> logOut() async {
|
Future<void> logOut() async {
|
||||||
await _firebaseAuth.signOut();
|
await _firebaseAuth.signOut();
|
||||||
|
|
@ -77,8 +79,8 @@ class FirebaseUserRepo implements UserRepository {
|
||||||
Future<void> setUserData(MyUser myUser) async {
|
Future<void> setUserData(MyUser myUser) async {
|
||||||
try {
|
try {
|
||||||
await userCollection
|
await userCollection
|
||||||
.doc(myUser.userId)
|
.doc(myUser.userId)
|
||||||
.set(myUser.toEntity().toDocument());
|
.set(myUser.toEntity().toDocument());
|
||||||
// Tunggu sebentar agar data tersimpan dengan baik sebelum stream update
|
// Tunggu sebentar agar data tersimpan dengan baik sebelum stream update
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
} catch (e) {
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import 'models/models.dart';
|
||||||
abstract class UserRepository {
|
abstract class UserRepository {
|
||||||
Stream<MyUser?> get user;
|
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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.35"
|
version: "1.3.66"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -45,26 +45,26 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cloud_firestore
|
name: cloud_firestore
|
||||||
sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185
|
sha256: "54484b2fc49f41b46f35b60a54b12351181eeaad22c0e3def276a81e17ae7c9b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.17.5"
|
version: "6.1.2"
|
||||||
cloud_firestore_platform_interface:
|
cloud_firestore_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cloud_firestore_platform_interface
|
name: cloud_firestore_platform_interface
|
||||||
sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81"
|
sha256: dfaa8b2c0d0a824af289d4159816a5c78417feec264c2194081d645687195158
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.5"
|
version: "7.0.6"
|
||||||
cloud_firestore_web:
|
cloud_firestore_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cloud_firestore_web
|
name: cloud_firestore_web
|
||||||
sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9"
|
sha256: "35d01f502b3b701d700470d32a8f82704dac8341a66e86c074900cde5bab343d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.12.5"
|
version: "5.1.2"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -85,50 +85,50 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_auth
|
name: firebase_auth
|
||||||
sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba
|
sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.20.0"
|
version: "6.1.4"
|
||||||
firebase_auth_platform_interface:
|
firebase_auth_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_auth_platform_interface
|
name: firebase_auth_platform_interface
|
||||||
sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093
|
sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
version: "8.1.6"
|
||||||
firebase_auth_web:
|
firebase_auth_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_auth_web
|
name: firebase_auth_web
|
||||||
sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b"
|
sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.12.0"
|
version: "6.1.2"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.32.0"
|
version: "4.4.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_platform_interface
|
name: firebase_core_platform_interface
|
||||||
sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb"
|
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.4.2"
|
version: "6.0.2"
|
||||||
firebase_core_web:
|
firebase_core_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
|
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.17.5"
|
version: "3.4.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -138,10 +138,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "3.0.2"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -152,6 +152,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -188,10 +196,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "3.0.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -321,10 +329,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "1.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.8.0-0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.22.0"
|
||||||
|
|
|
||||||
271
pubspec.lock
271
pubspec.lock
|
|
@ -5,10 +5,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144
|
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -25,6 +33,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.4"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -73,6 +89,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -81,6 +105,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -105,6 +137,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
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:
|
firebase_auth:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -133,10 +181,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9"
|
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.4.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -149,10 +197,42 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f
|
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -184,6 +264,22 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
google_identity_services_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -232,6 +328,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -248,6 +352,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -280,6 +408,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -304,6 +440,21 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
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:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -312,6 +463,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -320,6 +487,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -336,6 +559,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
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:
|
rxdart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -373,6 +604,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -436,6 +675,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.10.7 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.38.4"
|
||||||
|
|
|
||||||
33
pubspec.yaml
33
pubspec.yaml
|
|
@ -1,29 +1,36 @@
|
||||||
name: klimatologiot
|
name: klimatologiot
|
||||||
description: "A new Flutter project."
|
description: 'A new Flutter project.'
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.1.0
|
sdk: ^3.1.0
|
||||||
dependencies:
|
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:
|
flutter:
|
||||||
sdk: 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
|
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:
|
user_repository:
|
||||||
path: packages/user_repository
|
path: packages/user_repository
|
||||||
cupertino_icons: ^1.0.8
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_lints: ^3.0.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
# assets:
|
- images/
|
||||||
# - assets/images/
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,80 @@
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
// 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.
|
// 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/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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() {
|
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.
|
// 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.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!--
|
<!--
|
||||||
If you are serving your web app in a path other than the root, change the
|
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.
|
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
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
the `--base-href` argument provided to `flutter build`.
|
the `--base-href` argument provided to `flutter build`.
|
||||||
-->
|
-->
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
<base href="$FLUTTER_BASE_HREF" />
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A new Flutter project." />
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<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-status-bar-style" content="black" />
|
||||||
<meta name="apple-mobile-web-app-title" content="klimatologiot">
|
<meta name="apple-mobile-web-app-title" content="klimatologiot" />
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
|
||||||
<title>klimatologiot</title>
|
<title>klimatologiot</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json" />
|
||||||
</head>
|
|
||||||
<body>
|
<!-- Google -->
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<meta
|
||||||
</body>
|
name="google-signin-client_id"
|
||||||
|
content="219079385989-8hkp1kg2pprs5k269s4c08r6rpdj1gjd.apps.googleusercontent.com"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue