joss
This commit is contained in:
parent
efbbf8bb21
commit
de534b0277
|
|
@ -0,0 +1,3 @@
|
|||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
part 'device_setup_event.dart';
|
||||
part 'device_setup_state.dart';
|
||||
|
||||
class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
|
||||
DeviceSetupBloc() : super(const DeviceSetupState()) {
|
||||
on<CheckEspConnectionEvent>(_onCheckConnection);
|
||||
on<SendWifiCredentialsEvent>(_onSendCredentials);
|
||||
on<ResetDeviceSetupEvent>(_onReset);
|
||||
}
|
||||
|
||||
// ── Dio instance khusus untuk komunikasi ke ESP ──────────────
|
||||
// Timeout pendek karena ESP di jaringan lokal, harusnya cepat.
|
||||
// Kalau timeout → berarti HP belum konek ke hotspot ESP.
|
||||
final Dio _dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
sendTimeout: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
|
||||
// ── Cek koneksi ke ESP ────────────────────────────────────────
|
||||
Future<void> _onCheckConnection(
|
||||
CheckEspConnectionEvent event,
|
||||
Emitter<DeviceSetupState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: DeviceSetupStatus.checkingConn));
|
||||
|
||||
try {
|
||||
// Ping endpoint root ESP — kalau respond berarti HP sudah
|
||||
// terhubung ke hotspot ESP (192.168.4.1)
|
||||
final response = await _dio.get(
|
||||
'http://${state.espIp}/',
|
||||
options: Options(
|
||||
// Terima semua status code, yang penting server respond
|
||||
validateStatus: (_) => true,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode != null) {
|
||||
emit(state.copyWith(status: DeviceSetupStatus.connected));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.notConnected,
|
||||
errorMessage:
|
||||
'ESP tidak merespons. Pastikan HP sudah terhubung ke hotspot "Anemometer-Setup".',
|
||||
));
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
String msg = 'Tidak bisa terhubung ke ESP.';
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.sendTimeout) {
|
||||
msg =
|
||||
'Koneksi timeout. Pastikan HP terhubung ke hotspot "Anemometer-Setup" dan coba lagi.';
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
msg =
|
||||
'Tidak ada koneksi ke ESP. Sambungkan HP ke hotspot "Anemometer-Setup" terlebih dahulu.';
|
||||
}
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.notConnected,
|
||||
errorMessage: msg,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kirim SSID + Password ke ESP ─────────────────────────────
|
||||
Future<void> _onSendCredentials(
|
||||
SendWifiCredentialsEvent event,
|
||||
Emitter<DeviceSetupState> emit,
|
||||
) async {
|
||||
if (event.ssid.trim().isEmpty) {
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.failure,
|
||||
errorMessage: 'SSID tidak boleh kosong.',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: DeviceSetupStatus.sending));
|
||||
|
||||
try {
|
||||
// Kirim sebagai form-urlencoded — sama persis dengan
|
||||
// yang diterima WebServer ESP di route POST /save
|
||||
final response = await _dio.post(
|
||||
'http://${state.espIp}/save',
|
||||
data: {
|
||||
'ssid': event.ssid.trim(),
|
||||
'pass': event.password,
|
||||
},
|
||||
options: Options(
|
||||
contentType: Headers.formUrlEncodedContentType,
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.success,
|
||||
successMessage:
|
||||
'Berhasil! ESP menyimpan WiFi "${event.ssid}" dan akan restart otomatis.\n\n'
|
||||
'Tunggu beberapa detik, lalu sambungkan HP kembali ke WiFi normal.',
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.failure,
|
||||
errorMessage:
|
||||
'ESP menolak permintaan (${response.statusCode}): ${response.data}',
|
||||
));
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// ESP restart setelah terima kredensial → koneksi putus → itu normal!
|
||||
// DioException di sini kemungkinan besar karena ESP sudah restart.
|
||||
if (e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionError) {
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.success,
|
||||
successMessage:
|
||||
'ESP menerima konfigurasi WiFi dan sedang restart.\n\n'
|
||||
'Sambungkan HP kembali ke WiFi normal dalam beberapa detik.',
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: DeviceSetupStatus.failure,
|
||||
errorMessage: 'Gagal mengirim ke ESP: ${e.message}',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reset state ───────────────────────────────────────────────
|
||||
void _onReset(
|
||||
ResetDeviceSetupEvent event,
|
||||
Emitter<DeviceSetupState> emit,
|
||||
) {
|
||||
emit(const DeviceSetupState());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
part of 'device_setup_bloc.dart';
|
||||
|
||||
abstract class DeviceSetupEvent {}
|
||||
|
||||
/// Cek apakah HP sedang terhubung ke hotspot ESP
|
||||
class CheckEspConnectionEvent extends DeviceSetupEvent {}
|
||||
|
||||
/// Kirim SSID + password baru ke ESP via HTTP
|
||||
class SendWifiCredentialsEvent extends DeviceSetupEvent {
|
||||
final String ssid;
|
||||
final String password;
|
||||
|
||||
SendWifiCredentialsEvent({required this.ssid, required this.password});
|
||||
}
|
||||
|
||||
/// Reset state ke awal (misalnya saat user keluar screen)
|
||||
class ResetDeviceSetupEvent extends DeviceSetupEvent {}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
part of 'device_setup_bloc.dart';
|
||||
|
||||
enum DeviceSetupStatus {
|
||||
idle, // belum ada aksi
|
||||
checkingConn, // sedang cek koneksi ke ESP
|
||||
notConnected, // HP belum konek ke hotspot ESP
|
||||
connected, // HP sudah konek ke hotspot ESP, siap kirim
|
||||
sending, // sedang kirim SSID+password ke ESP
|
||||
success, // ESP berhasil terima & akan restart
|
||||
failure, // gagal (timeout, ESP tidak respond, dll)
|
||||
}
|
||||
|
||||
class DeviceSetupState {
|
||||
final DeviceSetupStatus status;
|
||||
final String? errorMessage;
|
||||
final String? successMessage;
|
||||
final String espIp;
|
||||
|
||||
const DeviceSetupState({
|
||||
this.status = DeviceSetupStatus.idle,
|
||||
this.errorMessage,
|
||||
this.successMessage,
|
||||
this.espIp = '192.168.4.1', // default IP ESP saat AP mode
|
||||
});
|
||||
|
||||
DeviceSetupState copyWith({
|
||||
DeviceSetupStatus? status,
|
||||
String? errorMessage,
|
||||
String? successMessage,
|
||||
String? espIp,
|
||||
}) {
|
||||
return DeviceSetupState(
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage,
|
||||
successMessage: successMessage,
|
||||
espIp: espIp ?? this.espIp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../blocs/device_setup_bloc.dart';
|
||||
|
||||
class DeviceSetupScreen extends StatefulWidget {
|
||||
const DeviceSetupScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DeviceSetupScreen> createState() => _DeviceSetupScreenState();
|
||||
}
|
||||
|
||||
class _DeviceSetupScreenState extends State<DeviceSetupScreen> {
|
||||
final _ssidController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ssidController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => DeviceSetupBloc(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Setup WiFi Anemometer'),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: BlocConsumer<DeviceSetupBloc, DeviceSetupState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == DeviceSetupStatus.success) {
|
||||
_showResultDialog(context,
|
||||
success: true, message: state.successMessage ?? '');
|
||||
} else if (state.status == DeviceSetupStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Terjadi kesalahan.'),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── Info Banner ───────────────────────────────
|
||||
_InfoBanner(status: state.status),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Step 1: Panduan konek hotspot ─────────────
|
||||
_StepCard(
|
||||
step: 1,
|
||||
title: 'Nyalakan Perangkat Anemometer',
|
||||
description:
|
||||
'Pastikan ESP32 anemometer sudah menyala dan belum pernah '
|
||||
'dikonfigurasi WiFi-nya. LED akan berkedip menandakan '
|
||||
'mode hotspot aktif.',
|
||||
isDone: state.status == DeviceSetupStatus.connected ||
|
||||
state.status == DeviceSetupStatus.sending ||
|
||||
state.status == DeviceSetupStatus.success,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_StepCard(
|
||||
step: 2,
|
||||
title: 'Sambungkan HP ke Hotspot ESP',
|
||||
description:
|
||||
'Buka Pengaturan WiFi di HP kamu, lalu sambungkan ke:\n\n'
|
||||
'📶 Anemometer-Setup\n\n'
|
||||
'Hotspot ini tidak punya password. Setelah tersambung, '
|
||||
'kembali ke aplikasi ini.',
|
||||
isDone: state.status == DeviceSetupStatus.connected ||
|
||||
state.status == DeviceSetupStatus.sending ||
|
||||
state.status == DeviceSetupStatus.success,
|
||||
trailing: _OpenWifiSettingsButton(status: state.status),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Step 3: Cek koneksi ───────────────────────
|
||||
_StepCard(
|
||||
step: 3,
|
||||
title: 'Verifikasi Koneksi ke ESP',
|
||||
description: state.status == DeviceSetupStatus.notConnected
|
||||
? state.errorMessage ?? 'Belum terhubung ke ESP.'
|
||||
: state.status == DeviceSetupStatus.connected ||
|
||||
state.status == DeviceSetupStatus.sending ||
|
||||
state.status == DeviceSetupStatus.success
|
||||
? '✅ HP sudah terhubung ke ESP. Lanjutkan ke langkah berikutnya.'
|
||||
: 'Tekan tombol di bawah untuk memverifikasi koneksi ke ESP.',
|
||||
isDone: state.status == DeviceSetupStatus.connected ||
|
||||
state.status == DeviceSetupStatus.sending ||
|
||||
state.status == DeviceSetupStatus.success,
|
||||
trailing: state.status == DeviceSetupStatus.connected ||
|
||||
state.status == DeviceSetupStatus.sending ||
|
||||
state.status == DeviceSetupStatus.success
|
||||
? null
|
||||
: _CheckConnectionButton(state: state),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Step 4: Isi form WiFi ─────────────────────
|
||||
_StepCard(
|
||||
step: 4,
|
||||
title: 'Masukkan Kredensial WiFi',
|
||||
description: 'Isi SSID dan password WiFi yang ingin '
|
||||
'digunakan oleh anemometer.',
|
||||
isDone: state.status == DeviceSetupStatus.success,
|
||||
child: state.status == DeviceSetupStatus.connected ||
|
||||
state.status == DeviceSetupStatus.sending
|
||||
? _WifiForm(
|
||||
ssidController: _ssidController,
|
||||
passwordController: _passwordController,
|
||||
formKey: _formKey,
|
||||
obscurePassword: _obscurePassword,
|
||||
onToggleObscure: () => setState(
|
||||
() => _obscurePassword = !_obscurePassword),
|
||||
onSubmit: state.status == DeviceSetupStatus.sending
|
||||
? null
|
||||
: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<DeviceSetupBloc>().add(
|
||||
SendWifiCredentialsEvent(
|
||||
ssid: _ssidController.text,
|
||||
password:
|
||||
_passwordController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
isLoading:
|
||||
state.status == DeviceSetupStatus.sending,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showResultDialog(
|
||||
BuildContext context, {
|
||||
required bool success,
|
||||
required String message,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
success ? Icons.check_circle_rounded : Icons.error_rounded,
|
||||
color: success ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(success ? 'Berhasil!' : 'Gagal'),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // tutup dialog
|
||||
Navigator.of(context).pop(); // kembali ke wind_speed_screen
|
||||
},
|
||||
child: const Text('Selesai'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// Widgets internal
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
class _InfoBanner extends StatelessWidget {
|
||||
final DeviceSetupStatus status;
|
||||
const _InfoBanner({required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.wifi_tethering_rounded,
|
||||
color: theme.colorScheme.primary, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Setup WiFi Anemometer',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Ikuti langkah-langkah berikut untuk mengkonfigurasi '
|
||||
'koneksi WiFi pada perangkat anemometer.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StepCard extends StatelessWidget {
|
||||
final int step;
|
||||
final String title;
|
||||
final String description;
|
||||
final bool isDone;
|
||||
final Widget? trailing;
|
||||
final Widget? child;
|
||||
|
||||
const _StepCard({
|
||||
required this.step,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.isDone = false,
|
||||
this.trailing,
|
||||
this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color:
|
||||
isDone ? Colors.green.shade300 : theme.colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Step badge
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isDone
|
||||
? Colors.green
|
||||
: theme.colorScheme.primaryContainer,
|
||||
),
|
||||
child: Center(
|
||||
child: isDone
|
||||
? const Icon(Icons.check, size: 16, color: Colors.white)
|
||||
: Text(
|
||||
'$step',
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
trailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 38),
|
||||
child: Text(
|
||||
description,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (child != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 38),
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OpenWifiSettingsButton extends StatelessWidget {
|
||||
final DeviceSetupStatus status;
|
||||
const _OpenWifiSettingsButton({required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Tidak perlu tampil kalau sudah connected
|
||||
if (status == DeviceSetupStatus.connected ||
|
||||
status == DeviceSetupStatus.sending ||
|
||||
status == DeviceSetupStatus.success) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return TextButton.icon(
|
||||
onPressed: () {
|
||||
// Buka pengaturan WiFi sistem HP
|
||||
// Butuh package: app_settings (opsional)
|
||||
// AppSettings.openWIFISettings();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Buka Pengaturan WiFi HP → sambungkan ke "Anemometer-Setup"'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.settings_rounded, size: 16),
|
||||
label: const Text('Buka Setting'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CheckConnectionButton extends StatelessWidget {
|
||||
final DeviceSetupState state;
|
||||
const _CheckConnectionButton({required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isChecking = state.status == DeviceSetupStatus.checkingConn;
|
||||
return ElevatedButton.icon(
|
||||
onPressed: isChecking
|
||||
? null
|
||||
: () =>
|
||||
context.read<DeviceSetupBloc>().add(CheckEspConnectionEvent()),
|
||||
icon: isChecking
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.wifi_find_rounded, size: 16),
|
||||
label: Text(isChecking ? 'Mengecek...' : 'Cek Koneksi'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
textStyle: const TextStyle(fontSize: 13),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WifiForm extends StatelessWidget {
|
||||
final TextEditingController ssidController;
|
||||
final TextEditingController passwordController;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final bool obscurePassword;
|
||||
final VoidCallback onToggleObscure;
|
||||
final VoidCallback? onSubmit;
|
||||
final bool isLoading;
|
||||
|
||||
const _WifiForm({
|
||||
required this.ssidController,
|
||||
required this.passwordController,
|
||||
required this.formKey,
|
||||
required this.obscurePassword,
|
||||
required this.onToggleObscure,
|
||||
required this.onSubmit,
|
||||
required this.isLoading,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// SSID
|
||||
TextFormField(
|
||||
controller: ssidController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama WiFi (SSID)',
|
||||
hintText: 'Contoh: RumahKu_2.4GHz',
|
||||
prefixIcon: const Icon(Icons.wifi_rounded),
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) => (v == null || v.trim().isEmpty)
|
||||
? 'SSID tidak boleh kosong'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Password
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
obscureText: obscurePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password WiFi',
|
||||
hintText: 'Kosongkan jika WiFi terbuka',
|
||||
prefixIcon: const Icon(Icons.lock_rounded),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscurePassword
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
),
|
||||
onPressed: onToggleObscure,
|
||||
),
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) => onSubmit?.call(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Submit button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onSubmit,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.send_rounded),
|
||||
label: Text(
|
||||
isLoading ? 'Mengirim ke ESP...' : 'Kirim ke Anemometer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (isLoading) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'⏳ Mengirim konfigurasi WiFi ke ESP...\n'
|
||||
'Koneksi akan terputus sebentar saat ESP restart.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'widgets/period_selector.dart';
|
|||
|
||||
import '../../shared/utils/pdf/pdf_export_service.dart';
|
||||
import '../../shared/widgets/export_pdf_button.dart';
|
||||
import '../../device_setup/views/device_setup_screen.dart';
|
||||
|
||||
class WindSpeedScreen extends StatelessWidget {
|
||||
const WindSpeedScreen({super.key});
|
||||
|
|
@ -24,6 +25,20 @@ class WindSpeedScreen extends StatelessWidget {
|
|||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Pengaturan Perangkat',
|
||||
icon: const Icon(Icons.settings_rounded),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const DeviceSetupScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
/// === Body area, lokasi dan tata letak Widgets === ///
|
||||
|
|
|
|||
24
pubspec.lock
24
pubspec.lock
|
|
@ -145,6 +145,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -472,6 +488,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
monitoring_repository:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ dependencies:
|
|||
bloc_concurrency: ^0.2.5
|
||||
cloud_firestore: ^6.1.1
|
||||
cupertino_icons: ^1.0.8
|
||||
dio: ^5.9.2
|
||||
equatable: ^2.0.5
|
||||
firebase_auth: ^6.1.3
|
||||
firebase_core: ^4.3.0
|
||||
|
|
@ -22,11 +23,11 @@ dependencies:
|
|||
intl: ^0.20.2
|
||||
monitoring_repository:
|
||||
path: packages/monitoring_repository
|
||||
pdf: ^3.11.1
|
||||
printing: ^5.13.1
|
||||
rxdart: ^0.27.7
|
||||
user_repository:
|
||||
path: packages/user_repository
|
||||
pdf: ^3.11.1
|
||||
printing: ^5.13.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^3.0.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue