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:klimatologiot/app_view.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||
import 'blocs/authentication_bloc/authentication_bloc.dart';
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
final UserRepository userRepository;
|
||||
const MyApp(this.userRepository, {super.key});
|
||||
final MonitoringRepository monitoringRepository;
|
||||
const MyApp(this.userRepository, this.monitoringRepository, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RepositoryProvider<AuthenticationBloc>(
|
||||
create: (context) => AuthenticationBloc(
|
||||
userRepository: userRepository
|
||||
),
|
||||
child: const MyAppView(),
|
||||
);
|
||||
return MultiRepositoryProvider(
|
||||
providers: [
|
||||
// 1. Daftarkan UserRepository
|
||||
RepositoryProvider<UserRepository>.value(value: userRepository),
|
||||
RepositoryProvider<MonitoringRepository>.value(
|
||||
value: monitoringRepository),
|
||||
],
|
||||
child: BlocProvider<AuthenticationBloc>(
|
||||
create: (context) => AuthenticationBloc(
|
||||
userRepository: userRepository,
|
||||
),
|
||||
child: const MyAppView(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState>
|
|||
emit(AuthenticationState.unauthenticated());
|
||||
}
|
||||
});
|
||||
on<AuthenticationLogoutRequested>((event, emit) async {
|
||||
try {
|
||||
await userRepository.logOut();
|
||||
} catch (_) {
|
||||
// ignore errors here; stream will update state accordingly
|
||||
}
|
||||
});
|
||||
}
|
||||
@override
|
||||
Future<void> close() {
|
||||
|
|
|
|||
|
|
@ -11,4 +11,8 @@ class AuthenticationUserChanged extends AuthenticationEvent {
|
|||
final MyUser? 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/app.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||
import 'firebase_options.dart';
|
||||
|
||||
void main() async {
|
||||
|
|
@ -12,5 +13,8 @@ void main() async {
|
|||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
Bloc.observer = SimpleBlocObserver();
|
||||
runApp(MyApp(FirebaseUserRepo()));
|
||||
}
|
||||
runApp(MyApp(
|
||||
FirebaseUserRepo(),
|
||||
FirebaseMonitoringRepo(),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ part 'sign_in_state.dart';
|
|||
class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
||||
final UserRepository _userRepository;
|
||||
|
||||
SignInBloc(
|
||||
this._userRepository
|
||||
) : super(SignInInitial()) {
|
||||
SignInBloc(this._userRepository) : super(SignInInitial()) {
|
||||
on<SignInRequired>((event, emit) async {
|
||||
emit(SignInProcess());
|
||||
try {
|
||||
|
|
@ -19,7 +17,17 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
|||
emit(SignInFailure());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
on<GoogleSignInRequired>((event, emit) async {
|
||||
emit(SignInProcess());
|
||||
try {
|
||||
await _userRepository.signInWithGoogle();
|
||||
} catch (e) {
|
||||
emit(SignInFailure());
|
||||
emit(SignInInitial());
|
||||
}
|
||||
});
|
||||
|
||||
on<SignOutRequired>((event, emit) async => await _userRepository.logOut());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@ class SignInRequired extends SignInEvent {
|
|||
List<Object> get props => [email, password];
|
||||
}
|
||||
|
||||
class SignOutRequired extends SignInEvent {
|
||||
|
||||
}
|
||||
class GoogleSignInRequired extends SignInEvent {}
|
||||
|
||||
class SignOutRequired extends SignInEvent {}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
|
|||
on<SignUpRequired>((event, emit) async {
|
||||
emit(SignUpProcess());
|
||||
try {
|
||||
MyUser myUser = await _userRepository.signUp(event.user, event.password);
|
||||
MyUser myUser =
|
||||
await _userRepository.signUp(event.user, event.password);
|
||||
await _userRepository.setUserData(myUser);
|
||||
emit(SignUpSuccess());
|
||||
} catch (e) {
|
||||
emit(SignUpFailure());
|
||||
emit(SignUpFailure(e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,6 @@ class SignUpRequired extends SignUpEvent {
|
|||
|
||||
@override
|
||||
List<Object> get props => [user, password];
|
||||
}
|
||||
}
|
||||
|
||||
class GoogleSignInRequired extends SignUpEvent {}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,18 @@ sealed class SignUpState extends Equatable {
|
|||
const SignUpState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
final class SignUpInitial extends SignUpState {}
|
||||
|
||||
final class SignUpSuccess extends SignUpState {}
|
||||
final class SignUpFailure extends SignUpState {}
|
||||
final class SignUpFailure extends SignUpState {
|
||||
final String? message;
|
||||
const SignUpFailure([this.message]);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
final class SignUpProcess extends SignUpState {}
|
||||
|
|
@ -13,125 +13,153 @@ class SignInScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
final passwordController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool signInRequired = false;
|
||||
IconData iconPassword = CupertinoIcons.eye_fill;
|
||||
bool obscurePassword = true;
|
||||
String? _errorMsg;
|
||||
|
||||
bool signInRequired = false;
|
||||
IconData iconPassword = CupertinoIcons.eye_fill;
|
||||
bool obscurePassword = true;
|
||||
String? _errorMsg;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<SignInBloc, SignInState>(
|
||||
listener: (context, state) {
|
||||
if(state is SignInSuccess) {
|
||||
setState(() {
|
||||
signInRequired = false;
|
||||
});
|
||||
} else if(state is SignInProcess) {
|
||||
setState(() {
|
||||
signInRequired = true;
|
||||
});
|
||||
} else if(state is SignInFailure) {
|
||||
setState(() {
|
||||
signInRequired = false;
|
||||
_errorMsg = 'Invalid email or password';
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: emailController,
|
||||
hintText: 'Email',
|
||||
obscureText: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||
errorMsg: _errorMsg,
|
||||
validator: (val) {
|
||||
if (val!.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: passwordController,
|
||||
hintText: 'Password',
|
||||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
errorMsg: _errorMsg,
|
||||
validator: (val) {
|
||||
if (val!.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
if(obscurePassword) {
|
||||
iconPassword = CupertinoIcons.eye_fill;
|
||||
} else {
|
||||
iconPassword = CupertinoIcons.eye_slash_fill;
|
||||
}
|
||||
});
|
||||
listener: (context, state) {
|
||||
if (state is SignInProcess) {
|
||||
setState(() {
|
||||
signInRequired = true;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
signInRequired = false;
|
||||
});
|
||||
if (state is SignInFailure) {
|
||||
setState(() {
|
||||
// PERUBAHAN: Pesan error lebih umum karena Google gagal belum tentu soal password
|
||||
_errorMsg = 'Sign in failed. Please try again.';
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: emailController,
|
||||
hintText: 'Email',
|
||||
obscureText: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||
errorMsg: _errorMsg,
|
||||
validator: (val) {
|
||||
if (val!.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
})),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: passwordController,
|
||||
hintText: 'Password',
|
||||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
errorMsg: _errorMsg,
|
||||
validator: (val) {
|
||||
if (val!.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
icon: Icon(iconPassword),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
if (obscurePassword) {
|
||||
iconPassword = CupertinoIcons.eye_fill;
|
||||
} else {
|
||||
iconPassword = CupertinoIcons.eye_slash_fill;
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: Icon(iconPassword),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
!signInRequired
|
||||
? SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<SignInBloc>().add(SignInRequired(
|
||||
emailController.text,
|
||||
passwordController.text)
|
||||
);
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
elevation: 3.0,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(60)
|
||||
)
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 25, vertical: 5),
|
||||
child: Text(
|
||||
'Sign In',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
!signInRequired
|
||||
? SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<SignInBloc>().add(SignInRequired(
|
||||
emailController.text,
|
||||
passwordController.text));
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
elevation: 3.0,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(60))),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 25, vertical: 5),
|
||||
child: Text(
|
||||
'Sign In',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
)),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
|
||||
/// ======= Tombol Login Google ======= ///
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
// == Mengirim event login Google ke Bloc == //
|
||||
context.read<SignInBloc>().add(GoogleSignInRequired());
|
||||
},
|
||||
icon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: Image.asset(
|
||||
'images/google-icon-logo.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
label: const Text(
|
||||
'Sign In with Google',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
elevation: 3.0,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
side: const BorderSide(color: Colors.grey))),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ import 'package:user_repository/user_repository.dart';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../components/my_text_field.dart';
|
||||
import '../blocs/sign_up_bloc/sign_up_bloc.dart';
|
||||
import '../blocs/sign_up_bloc/sign_up_bloc.dart' as signUp;
|
||||
// import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||
import '../blocs/sign_in_bloc/sign_in_bloc.dart' as signIn;
|
||||
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
const SignUpScreen({super.key});
|
||||
final VoidCallback? onSignInTap;
|
||||
const SignUpScreen({super.key, this.onSignInTap});
|
||||
|
||||
@override
|
||||
State<SignUpScreen> createState() => _SignUpScreenState();
|
||||
|
|
@ -23,175 +26,179 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
bool signUpRequired = false;
|
||||
|
||||
bool containsUpperCase = false;
|
||||
bool containsLowerCase = false;
|
||||
bool containsNumber = false;
|
||||
bool containsSpecialChar = false;
|
||||
bool contains8Length = false;
|
||||
bool containsLowerCase = false;
|
||||
bool containsNumber = false;
|
||||
bool containsSpecialChar = false;
|
||||
bool contains8Length = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<SignUpBloc, SignUpState>(
|
||||
listener: (context, state) {
|
||||
if (state is SignUpSuccess) {
|
||||
setState(() {
|
||||
signUpRequired = false;
|
||||
});
|
||||
// User sudah terdaftar dan auth state akan otomatis update via AuthenticationBloc
|
||||
// Tungur sebentar agar Firestore selesai menyimpan data
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Registrasi berhasil! Mengarahkan...')),
|
||||
);
|
||||
} else if (state is SignUpProcess) {
|
||||
setState(() {
|
||||
signUpRequired = true;
|
||||
});
|
||||
} else if (state is SignUpFailure) {
|
||||
setState(() {
|
||||
signUpRequired = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Registrasi gagal, silakan coba lagi')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.92,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Buat Akun',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||||
),
|
||||
return BlocListener<signUp.SignUpBloc, signUp.SignUpState>(
|
||||
listener: (context, state) {
|
||||
if (state is signUp.SignUpProcess) {
|
||||
setState(() {
|
||||
signUpRequired = true;
|
||||
});
|
||||
} else if (state is signUp.SignUpFailure) {
|
||||
setState(() {
|
||||
signUpRequired = false; // Matikan loading!
|
||||
});
|
||||
// Tampilkan pesan error lewat Snackbar biar user tahu kenapa gagal
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message ?? 'An error ocurred'),
|
||||
backgroundColor: Colors.red),
|
||||
);
|
||||
} else if (state is signUp.SignUpSuccess) {
|
||||
setState(() {
|
||||
signUpRequired = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.92,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 22),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Buat Akun',
|
||||
style: TextStyle(
|
||||
fontSize: 22, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Daftar untuk mulai monitoring',
|
||||
style: TextStyle(fontSize: 13, color: Colors.black54),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Daftar untuk mulai monitoring',
|
||||
style:
|
||||
TextStyle(fontSize: 13, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Name
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: nameController,
|
||||
hintText: 'Masukkan nama lengkap',
|
||||
obscureText: false,
|
||||
keyboardType: TextInputType.name,
|
||||
prefixIcon: const Icon(CupertinoIcons.person_fill),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
} else if (val.length > 50) {
|
||||
return 'Name too long';
|
||||
}
|
||||
return null;
|
||||
// Name
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: nameController,
|
||||
hintText: 'Masukkan nama lengkap',
|
||||
obscureText: false,
|
||||
keyboardType: TextInputType.name,
|
||||
prefixIcon: const Icon(CupertinoIcons.person_fill),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
} else if (val.length > 50) {
|
||||
return 'Name too long';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Email
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: emailController,
|
||||
hintText: 'nama@email.com',
|
||||
obscureText: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Password
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: passwordController,
|
||||
hintText: 'Minimal 6 karakter',
|
||||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
iconPassword = obscurePassword
|
||||
? CupertinoIcons.eye_fill
|
||||
: CupertinoIcons.eye_slash_fill;
|
||||
});
|
||||
},
|
||||
icon: Icon(iconPassword),
|
||||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Email
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: emailController,
|
||||
hintText: 'nama@email.com',
|
||||
obscureText: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
// Confirm Password
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: confirmPasswordController,
|
||||
hintText: 'Ulangi password',
|
||||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
} else if (val != passwordController.text) {
|
||||
return 'Password tidak cocok';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
),
|
||||
|
||||
// Password
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: passwordController,
|
||||
hintText: 'Minimal 6 karakter',
|
||||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
iconPassword = obscurePassword
|
||||
? CupertinoIcons.eye_fill
|
||||
: CupertinoIcons.eye_slash_fill;
|
||||
});
|
||||
},
|
||||
icon: Icon(iconPassword),
|
||||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Confirm Password
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
controller: confirmPasswordController,
|
||||
hintText: 'Ulangi password',
|
||||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
} else if (val != passwordController.text) {
|
||||
return 'Password tidak cocok';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Button
|
||||
!signUpRequired
|
||||
? SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.92,
|
||||
/// == Sign Up == ///
|
||||
// Button
|
||||
!signUpRequired
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width:
|
||||
MediaQuery.of(context).size.width * 0.5,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
|
|
@ -200,8 +207,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
myUser.name = nameController.text;
|
||||
|
||||
setState(() {
|
||||
context.read<SignUpBloc>().add(
|
||||
SignUpRequired(
|
||||
context.read<signUp.SignUpBloc>().add(
|
||||
signUp.SignUpRequired(
|
||||
myUser,
|
||||
passwordController.text,
|
||||
),
|
||||
|
|
@ -209,47 +216,62 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
});
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.black87,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Daftar',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 25, vertical: 5),
|
||||
child: const Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Sudah punya akun? '),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Login di sini'),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Text('Atau'),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Sudah punya akun? '),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onSignInTap?.call();
|
||||
},
|
||||
child: const Text(
|
||||
'Login di sini',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,90 +13,92 @@ class WelcomeScreen extends StatefulWidget {
|
|||
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||
}
|
||||
|
||||
class _WelcomeScreenState extends State<WelcomeScreen> with TickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
class _WelcomeScreenState extends State<WelcomeScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
tabController = TabController(
|
||||
initialIndex: 0,
|
||||
length: 2,
|
||||
vsync: this
|
||||
);
|
||||
tabController = TabController(initialIndex: 0, length: 2, vsync: this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height / 1.8,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
unselectedLabelColor: Theme.of(context).colorScheme.onBackground.withOpacity(0.5),
|
||||
labelColor: Theme.of(context).colorScheme.onBackground,
|
||||
tabs: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'Sign In',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
BlocProvider<SignInBloc>(
|
||||
create: (context) => SignInBloc(
|
||||
context.read<AuthenticationBloc>().userRepository
|
||||
),
|
||||
child: const SignInScreen(),
|
||||
),
|
||||
BlocProvider<SignUpBloc>(
|
||||
create: (context) => SignUpBloc(
|
||||
context.read<AuthenticationBloc>().userRepository
|
||||
),
|
||||
child: const SignUpScreen(),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height / 1.8,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
unselectedLabelColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.5),
|
||||
labelColor:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
tabs: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'Sign In',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
BlocProvider<SignInBloc>(
|
||||
create: (context) => SignInBloc(context
|
||||
.read<AuthenticationBloc>()
|
||||
.userRepository),
|
||||
child: const SignInScreen(),
|
||||
),
|
||||
BlocProvider<SignUpBloc>(
|
||||
create: (context) => SignUpBloc(context
|
||||
.read<AuthenticationBloc>()
|
||||
.userRepository),
|
||||
child: SignUpScreen(onSignInTap: () {
|
||||
tabController.animateTo(
|
||||
0); // Ini yang bikin dia pindah ke tab Login
|
||||
}),
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||
import 'main_drawer.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget{
|
||||
const HomeScreen ({super.key});
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
drawer: const MainDrawer(),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.5, // Kasih sedikit bayangan tipis agar elegan
|
||||
centerTitle: false,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.menu,
|
||||
color: Colors.black), // Ikon garis 3 horizontal
|
||||
onPressed: () {
|
||||
// ini kodenya untuk membuka:
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
)),
|
||||
title: Row(
|
||||
children: [
|
||||
// Ganti dengan Image.asset jika sudah ada logo
|
||||
const Icon(Icons.cloud_queue, color: Colors.blue, size: 28),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"Klimatologi",
|
||||
style: GoogleFonts.rubik(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_none, color: Colors.black),
|
||||
onPressed: () {
|
||||
// Aksi notifikasi
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout, color: Colors.black),
|
||||
onPressed: () {
|
||||
// Trigger Logout di AuthenticationBloc kamu
|
||||
context
|
||||
.read<AuthenticationBloc>()
|
||||
.add(AuthenticationLogoutRequested());
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: const Center(
|
||||
child: Text("body home page"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import Foundation
|
|||
import cloud_firestore
|
||||
import firebase_auth
|
||||
import firebase_core
|
||||
import firebase_database
|
||||
import google_sign_in_ios
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.68"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -49,6 +57,54 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
firebase_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "2f988dab915efde3b3105268dbd69efce0e8570d767a218ccd914afd0c10c8cc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "1399ab1f0ac3b503d8a9be64a4c997fc066bbf33f701f42866e5569f26205ebe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.1"
|
||||
firebase_database:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_database
|
||||
sha256: "05358855166c943a2fe9321bce69ab6b583c9a49e70e01b5297ef03f8bea50b9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.2.0"
|
||||
firebase_database_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_database_platform_interface
|
||||
sha256: "83278cc315ad0f36a5e30fc7f3d1c6edc722340347d41ea17f57e52176bb98f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
firebase_database_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_database_web
|
||||
sha256: "77df7f7c59f4bba33f06644f76e28a8acc594487f1aaa4c79947abf491a7ae70"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.7+5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -58,15 +114,20 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "6.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -95,10 +156,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "6.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -131,6 +192,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
rxdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.27.7"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -140,10 +217,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -200,6 +277,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
dart: ">=3.10.7 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
|
@ -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.instance;
|
||||
|
||||
/// == Melakukan Implement User == ///
|
||||
@override
|
||||
Stream<MyUser?> get user {
|
||||
return _firebaseAuth.authStateChanges().flatMap((firebaseUser) async* {
|
||||
|
|
@ -21,13 +22,15 @@ class FirebaseUserRepo implements UserRepository {
|
|||
try {
|
||||
final userData = await userCollection.doc(firebaseUser.uid).get();
|
||||
if (userData.exists && userData.data() != null) {
|
||||
yield MyUser.fromEntity(MyUserEntity.fromDocument(userData.data()!));
|
||||
yield MyUser.fromEntity(
|
||||
MyUserEntity.fromDocument(userData.data()!));
|
||||
} else {
|
||||
// User baru yang belum ada data di Firestore, tunggu sebentar
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final retryData = await userCollection.doc(firebaseUser.uid).get();
|
||||
if (retryData.exists && retryData.data() != null) {
|
||||
yield MyUser.fromEntity(MyUserEntity.fromDocument(retryData.data()!));
|
||||
yield MyUser.fromEntity(
|
||||
MyUserEntity.fromDocument(retryData.data()!));
|
||||
} else {
|
||||
yield MyUser.empty;
|
||||
}
|
||||
|
|
@ -40,34 +43,33 @@ class FirebaseUserRepo implements UserRepository {
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
/// == Melakukan Implement Sign in == ///
|
||||
@override
|
||||
Future<void> signIn(String email, String password) async {
|
||||
try {
|
||||
await _firebaseAuth.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password
|
||||
);
|
||||
await _firebaseAuth.signInWithEmailAndPassword(
|
||||
email: email, password: password);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
log(e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// == Melakukan Implement Sign Up == ///
|
||||
@override
|
||||
Future<MyUser> signUp(MyUser myUser, String password) async {
|
||||
try {
|
||||
try {
|
||||
UserCredential user = await _firebaseAuth.createUserWithEmailAndPassword(
|
||||
email: myUser.email,
|
||||
password: password
|
||||
);
|
||||
myUser.userId = user.user!.uid;
|
||||
return myUser;
|
||||
email: myUser.email, password: password);
|
||||
myUser.userId = user.user!.uid;
|
||||
return myUser;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// == Melakukan Implement Log Out == ///
|
||||
@override
|
||||
Future<void> logOut() async {
|
||||
await _firebaseAuth.signOut();
|
||||
|
|
@ -77,8 +79,8 @@ class FirebaseUserRepo implements UserRepository {
|
|||
Future<void> setUserData(MyUser myUser) async {
|
||||
try {
|
||||
await userCollection
|
||||
.doc(myUser.userId)
|
||||
.set(myUser.toEntity().toDocument());
|
||||
.doc(myUser.userId)
|
||||
.set(myUser.toEntity().toDocument());
|
||||
// Tunggu sebentar agar data tersimpan dengan baik sebelum stream update
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
} catch (e) {
|
||||
|
|
@ -87,6 +89,34 @@ class FirebaseUserRepo implements UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
/// == Sign in with Google == ///
|
||||
@override
|
||||
Future<void> signInWithGoogle() async {
|
||||
try {
|
||||
final GoogleAuthProvider googleProvider = GoogleAuthProvider();
|
||||
final UserCredential userCredential =
|
||||
await _firebaseAuth.signInWithPopup(googleProvider);
|
||||
|
||||
final User? firebaseUser = userCredential.user;
|
||||
if (firebaseUser != null) {
|
||||
// Check if user data already exists in Firestore
|
||||
final existingUser = await userCollection.doc(firebaseUser.uid).get();
|
||||
|
||||
}
|
||||
if (!existingUser.exists) {
|
||||
// Create new user data from Google account info
|
||||
final newUser = MyUser(
|
||||
userId: firebaseUser.uid,
|
||||
email: firebaseUser.email ?? '',
|
||||
name: firebaseUser.displayName ?? 'User',
|
||||
hasActiveCart: false,
|
||||
);
|
||||
|
||||
await setUserData(newUser);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log('Google sign-in error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import 'models/models.dart';
|
|||
abstract class UserRepository {
|
||||
Stream<MyUser?> get user;
|
||||
|
||||
Future<MyUser> signUp(MyUser myUser, String password);
|
||||
Future<MyUser> signUp(MyUser myUser, String password);
|
||||
|
||||
Future<void> setUserData(MyUser user);
|
||||
Future<void> setUserData(MyUser user);
|
||||
|
||||
Future<void> signIn(String email, String password);
|
||||
Future<void> signIn(String email, String password);
|
||||
|
||||
Future<void> logOut();
|
||||
}
|
||||
Future<void> signInWithGoogle();
|
||||
|
||||
Future<void> logOut();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.35"
|
||||
version: "1.3.66"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -45,26 +45,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: cloud_firestore
|
||||
sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185
|
||||
sha256: "54484b2fc49f41b46f35b60a54b12351181eeaad22c0e3def276a81e17ae7c9b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.17.5"
|
||||
version: "6.1.2"
|
||||
cloud_firestore_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cloud_firestore_platform_interface
|
||||
sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81"
|
||||
sha256: dfaa8b2c0d0a824af289d4159816a5c78417feec264c2194081d645687195158
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.5"
|
||||
version: "7.0.6"
|
||||
cloud_firestore_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cloud_firestore_web
|
||||
sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9"
|
||||
sha256: "35d01f502b3b701d700470d32a8f82704dac8341a66e86c074900cde5bab343d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.5"
|
||||
version: "5.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -85,50 +85,50 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_auth
|
||||
sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba
|
||||
sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.20.0"
|
||||
version: "6.1.4"
|
||||
firebase_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_platform_interface
|
||||
sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093
|
||||
sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "8.1.6"
|
||||
firebase_auth_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_web
|
||||
sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b"
|
||||
sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
version: "6.1.2"
|
||||
firebase_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.32.0"
|
||||
version: "4.4.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb"
|
||||
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.2"
|
||||
version: "6.0.2"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
|
||||
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.17.5"
|
||||
version: "3.4.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -138,10 +138,10 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -152,6 +152,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -188,10 +196,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -321,10 +329,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.22.0"
|
||||
|
|
|
|||
271
pubspec.lock
271
pubspec.lock
|
|
@ -5,10 +5,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144
|
||||
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.65"
|
||||
version: "1.3.66"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -25,6 +33,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
bloc_concurrency:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc_concurrency
|
||||
sha256: "456b7a3616a7c1ceb975c14441b3f198bf57d81cb95b7c6de5cb0c60201afcd8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.5"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +89,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -81,6 +105,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -105,6 +137,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
firebase_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -133,10 +181,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9"
|
||||
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
version: "4.4.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -149,10 +197,42 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f
|
||||
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.4.0"
|
||||
firebase_database:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_database
|
||||
sha256: f13676fab84671a808eb5eaeecfa2bcb5a3fed2802798c0dd16cb786139a6492
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.3"
|
||||
firebase_database_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_database_platform_interface
|
||||
sha256: "2444ec9999a7295b0e7f3fea60105dbbe72fdfad5a9e4c93e72d0b51c181f128"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0+2"
|
||||
firebase_database_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_database_web
|
||||
sha256: "211b6deff7ac373bb539f079d909799e9231397daddc9e0f1768f50f6216f034"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.7+3"
|
||||
fl_chart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -184,6 +264,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.2"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -232,6 +328,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -248,6 +352,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -280,6 +408,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -304,6 +440,21 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
monitoring_repository:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/monitoring_repository"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -312,6 +463,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -320,6 +487,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -336,6 +559,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
rxdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -373,6 +604,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -436,6 +675,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
dart: ">=3.10.7 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
|
|||
33
pubspec.yaml
33
pubspec.yaml
|
|
@ -1,29 +1,36 @@
|
|||
name: klimatologiot
|
||||
description: "A new Flutter project."
|
||||
description: 'A new Flutter project.'
|
||||
publish_to: 'none'
|
||||
version: 1.0.0+1
|
||||
environment:
|
||||
sdk: ^3.1.0
|
||||
dependencies:
|
||||
bloc: ^8.1.0
|
||||
bloc_concurrency: ^0.2.5
|
||||
cloud_firestore: ^6.1.1
|
||||
cupertino_icons: ^1.0.8
|
||||
equatable: ^2.0.5
|
||||
firebase_auth: ^6.1.3
|
||||
firebase_core: ^4.3.0
|
||||
firebase_database: ^12.1.3
|
||||
fl_chart: ^1.1.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_core: ^4.3.0
|
||||
firebase_auth: ^6.1.3
|
||||
cloud_firestore: ^6.1.1
|
||||
rxdart: ^0.27.7
|
||||
google_sign_in: ^7.2.0
|
||||
bloc: ^8.1.0
|
||||
flutter_bloc: ^8.1.0
|
||||
equatable: ^2.0.5
|
||||
google_fonts: ^8.0.2
|
||||
google_sign_in: ^7.2.0
|
||||
intl: ^0.20.2
|
||||
monitoring_repository:
|
||||
path: packages/monitoring_repository
|
||||
rxdart: ^0.27.7
|
||||
user_repository:
|
||||
path: packages/user_repository
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
# assets:
|
||||
# - assets/images/
|
||||
assets:
|
||||
- images/
|
||||
|
|
|
|||
|
|
@ -5,15 +5,80 @@
|
|||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
import '../lib/app.dart';
|
||||
|
||||
import 'package:klimatologiot/main.dart';
|
||||
// import 'package:klimatologiot/main.dart';
|
||||
|
||||
class FakeUserRepository implements UserRepository {
|
||||
@override
|
||||
Stream<MyUser?> get user => const Stream.empty();
|
||||
|
||||
@override
|
||||
Future<void> logOut() async {}
|
||||
|
||||
@override
|
||||
Future<void> setUserData(MyUser user) {
|
||||
// TODO: implement setUserData
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signIn(String email, String password) {
|
||||
// TODO: implement signIn
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithGoogle() {
|
||||
// TODO: implement signInWithGoogle
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser> signUp(MyUser myUser, String password) {
|
||||
// TODO: implement signUp
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
// tambahin kalau ada method lain
|
||||
}
|
||||
|
||||
class FakeMonitoringRepository implements MonitoringRepository {
|
||||
get _db => null;
|
||||
|
||||
// Satu fungsi untuk semua jenis sensor
|
||||
// Kamu cukup masukkan "path" database-nya saja
|
||||
|
||||
@override
|
||||
// Jika ingin mengambil data sekali saja (bukan stream)
|
||||
Future<DataSnapshot> getSensorSnapshot(String path) async {
|
||||
return await _db.ref(path).get();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<DatabaseEvent> getSensorStream(String path) {
|
||||
// TODO: implement getSensorStream
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
testWidgets('App loads', (WidgetTester tester) async {
|
||||
final fakeRepo = FakeUserRepository();
|
||||
final fakeMonitoring = FakeUserRepository();
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpWidget(
|
||||
MyApp(fakeRepo, fakeMonitoring as MonitoringRepository),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(MaterialApp), findsOneWidget);
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
|
|
@ -14,25 +14,31 @@
|
|||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
<base href="$FLUTTER_BASE_HREF" />
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
|
||||
<meta name="description" content="A new Flutter project." />
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="klimatologiot">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="klimatologiot" />
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
<title>klimatologiot</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
<title>klimatologiot</title>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<!-- Google -->
|
||||
<meta
|
||||
name="google-signin-client_id"
|
||||
content="219079385989-8hkp1kg2pprs5k269s4c08r6rpdj1gjd.apps.googleusercontent.com"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
|||
Loading…
Reference in New Issue