From de534b0277c4d5a8cdf09ab28eed27a561acca79 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:35:18 +0700 Subject: [PATCH] joss --- devtools_options.yaml | 3 + .../device_setup/blocs/device_setup_bloc.dart | 141 +++++ .../blocs/device_setup_event.dart | 17 + .../blocs/device_setup_state.dart | 39 ++ .../views/device_setup_screen.dart | 507 ++++++++++++++++++ .../wind_speed/views/wind_speed_screen.dart | 15 + pubspec.lock | 24 + pubspec.yaml | 5 +- 8 files changed, 749 insertions(+), 2 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart create mode 100644 lib/screens/monitoring/device_setup/blocs/device_setup_event.dart create mode 100644 lib/screens/monitoring/device_setup/blocs/device_setup_state.dart create mode 100644 lib/screens/monitoring/device_setup/views/device_setup_screen.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -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: diff --git a/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart b/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart new file mode 100644 index 0000000..8043ac7 --- /dev/null +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart @@ -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 { + DeviceSetupBloc() : super(const DeviceSetupState()) { + on(_onCheckConnection); + on(_onSendCredentials); + on(_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 _onCheckConnection( + CheckEspConnectionEvent event, + Emitter 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 _onSendCredentials( + SendWifiCredentialsEvent event, + Emitter 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 emit, + ) { + emit(const DeviceSetupState()); + } +} diff --git a/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart b/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart new file mode 100644 index 0000000..9a198c0 --- /dev/null +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart @@ -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 {} diff --git a/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart b/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart new file mode 100644 index 0000000..ec58f45 --- /dev/null +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart @@ -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, + ); + } +} diff --git a/lib/screens/monitoring/device_setup/views/device_setup_screen.dart b/lib/screens/monitoring/device_setup/views/device_setup_screen.dart new file mode 100644 index 0000000..b6a4811 --- /dev/null +++ b/lib/screens/monitoring/device_setup/views/device_setup_screen.dart @@ -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 createState() => _DeviceSetupScreenState(); +} + +class _DeviceSetupScreenState extends State { + final _ssidController = TextEditingController(); + final _passwordController = TextEditingController(); + final _formKey = GlobalKey(); + 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( + 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().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().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 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, + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart index 1fd7b42..04f1c96 100644 --- a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart +++ b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart @@ -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 === /// diff --git a/pubspec.lock b/pubspec.lock index 2a56299..ffc5bd8 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 8465d9f..4a46cde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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