From fd7b0c132cd6e55c9fbe04c1a928a06602e55c01 Mon Sep 17 00:00:00 2001 From: shinra32 Date: Fri, 22 May 2026 23:50:10 -0600 Subject: [PATCH] Co-authored-by: MENDOZA BALLARDO GAEL RICARDO Co-authored-by: Azareth-Tr Co-authored-by: eddgranados12 vistas de mockup actualizaco --- recolecta_app/lib/core/router/app_router.dart | 13 +- .../lib/core/widgets/app_widgets.dart | 121 +-- .../lib/features/auth/register_page.dart | 411 ++++++++-- .../features/home/citizen_home_screen.dart | 307 ++++++- views_v1/.gitignore | 45 + views_v1/.metadata | 30 + views_v1/README.md | 3 + views_v1/analysis_options.yaml | 1 + views_v1/pubspec.lock | 770 ++++++++++++++++++ views_v1/pubspec.yaml | 33 + views_v1/web/favicon.png | Bin 0 -> 1282 bytes views_v1/web/icons/Icon-192.png | Bin 0 -> 9566 bytes views_v1/web/icons/Icon-512.png | Bin 0 -> 14826 bytes views_v1/web/icons/Icon-maskable-192.png | Bin 0 -> 10086 bytes views_v1/web/icons/Icon-maskable-512.png | Bin 0 -> 37284 bytes views_v1/web/index.html | 46 ++ views_v1/web/manifest.json | 35 + views_v1/windows/.gitignore | 17 + views_v1/windows/CMakeLists.txt | 108 +++ views_v1/windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 20 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 26 + views_v1/windows/runner/CMakeLists.txt | 40 + views_v1/windows/runner/Runner.rc | 121 +++ views_v1/windows/runner/flutter_window.cpp | 71 ++ views_v1/windows/runner/flutter_window.h | 33 + views_v1/windows/runner/main.cpp | 43 + views_v1/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 53658 bytes views_v1/windows/runner/runner.exe.manifest | 14 + views_v1/windows/runner/utils.cpp | 69 ++ views_v1/windows/runner/utils.h | 19 + views_v1/windows/runner/win32_window.cpp | 288 +++++++ views_v1/windows/runner/win32_window.h | 102 +++ views_v2/app.dart | 144 ++++ views_v2/dio_client.dart | 53 ++ views_v2/feedback_model.dart | 39 + views_v2/feedback_provider.dart | 104 +++ views_v2/feedback_screen.dart | 354 ++++++++ views_v2/main.dart | 25 + views_v2/notifications_screen.dart | 378 +++++++++ views_v2/prevention_banner.dart | 51 ++ views_v2/progress_steps.dart | 174 ++++ 44 files changed, 4108 insertions(+), 140 deletions(-) create mode 100644 views_v1/.gitignore create mode 100644 views_v1/.metadata create mode 100644 views_v1/README.md create mode 100644 views_v1/analysis_options.yaml create mode 100644 views_v1/pubspec.lock create mode 100644 views_v1/pubspec.yaml create mode 100644 views_v1/web/favicon.png create mode 100644 views_v1/web/icons/Icon-192.png create mode 100644 views_v1/web/icons/Icon-512.png create mode 100644 views_v1/web/icons/Icon-maskable-192.png create mode 100644 views_v1/web/icons/Icon-maskable-512.png create mode 100644 views_v1/web/index.html create mode 100644 views_v1/web/manifest.json create mode 100644 views_v1/windows/.gitignore create mode 100644 views_v1/windows/CMakeLists.txt create mode 100644 views_v1/windows/flutter/CMakeLists.txt create mode 100644 views_v1/windows/flutter/generated_plugin_registrant.cc create mode 100644 views_v1/windows/flutter/generated_plugin_registrant.h create mode 100644 views_v1/windows/flutter/generated_plugins.cmake create mode 100644 views_v1/windows/runner/CMakeLists.txt create mode 100644 views_v1/windows/runner/Runner.rc create mode 100644 views_v1/windows/runner/flutter_window.cpp create mode 100644 views_v1/windows/runner/flutter_window.h create mode 100644 views_v1/windows/runner/main.cpp create mode 100644 views_v1/windows/runner/resource.h create mode 100644 views_v1/windows/runner/resources/app_icon.ico create mode 100644 views_v1/windows/runner/runner.exe.manifest create mode 100644 views_v1/windows/runner/utils.cpp create mode 100644 views_v1/windows/runner/utils.h create mode 100644 views_v1/windows/runner/win32_window.cpp create mode 100644 views_v1/windows/runner/win32_window.h create mode 100644 views_v2/app.dart create mode 100644 views_v2/dio_client.dart create mode 100644 views_v2/feedback_model.dart create mode 100644 views_v2/feedback_provider.dart create mode 100644 views_v2/feedback_screen.dart create mode 100644 views_v2/main.dart create mode 100644 views_v2/notifications_screen.dart create mode 100644 views_v2/prevention_banner.dart create mode 100644 views_v2/progress_steps.dart diff --git a/recolecta_app/lib/core/router/app_router.dart b/recolecta_app/lib/core/router/app_router.dart index f9f26b0..d263fee 100644 --- a/recolecta_app/lib/core/router/app_router.dart +++ b/recolecta_app/lib/core/router/app_router.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:recolecta_app/features/admin/admin_shell.dart'; import 'package:recolecta_app/features/auth/login_page.dart'; +import 'package:recolecta_app/features/auth/register_page.dart'; import 'package:recolecta_app/features/driver/driver_shell.dart'; import 'package:recolecta_app/features/driver/screens/driver_collections_screen.dart'; import 'package:recolecta_app/features/driver/screens/driver_home_screen.dart'; @@ -47,13 +48,15 @@ final routerProvider = Provider((ref) { final isAuthenticated = authState.value?.isAuthenticated ?? false; final role = authState.value?.userRole; - final isLoggingIn = state.matchedLocation == '/login'; + final isAuthRoute = + state.matchedLocation == '/login' || + state.matchedLocation == '/register'; if (!isAuthenticated) { - return isLoggingIn ? null : '/login'; + return isAuthRoute ? null : '/login'; } - if (isLoggingIn) { + if (isAuthRoute) { switch (role) { case 'admin': return '/admin'; @@ -70,6 +73,10 @@ final routerProvider = Provider((ref) { }, routes: [ GoRoute(path: '/login', builder: (context, state) => const LoginPage()), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterPage(), + ), ShellRoute( builder: (context, state, child) => AdminShell(child: child), routes: [ diff --git a/recolecta_app/lib/core/widgets/app_widgets.dart b/recolecta_app/lib/core/widgets/app_widgets.dart index 78640c6..b8a073e 100644 --- a/recolecta_app/lib/core/widgets/app_widgets.dart +++ b/recolecta_app/lib/core/widgets/app_widgets.dart @@ -15,28 +15,28 @@ class AppStatusBadge extends StatelessWidget { }); factory AppStatusBadge.green(String label) => AppStatusBadge( - label: label, - backgroundColor: AppTheme.primaryLight, - textColor: AppTheme.primaryDark, - ); + label: label, + backgroundColor: AppTheme.primaryLight, + textColor: AppTheme.primaryDark, + ); factory AppStatusBadge.amber(String label) => AppStatusBadge( - label: label, - backgroundColor: AppTheme.amberLight, - textColor: AppTheme.amber, - ); + label: label, + backgroundColor: AppTheme.amberLight, + textColor: AppTheme.amber, + ); factory AppStatusBadge.gray(String label) => AppStatusBadge( - label: label, - backgroundColor: const Color(0xFFF1EFE8), - textColor: const Color(0xFF5F5E5A), - ); + label: label, + backgroundColor: const Color(0xFFF1EFE8), + textColor: const Color(0xFF5F5E5A), + ); factory AppStatusBadge.danger(String label) => AppStatusBadge( - label: label, - backgroundColor: AppTheme.dangerLight, - textColor: AppTheme.danger, - ); + label: label, + backgroundColor: AppTheme.dangerLight, + textColor: AppTheme.danger, + ); @override Widget build(BuildContext context) { @@ -133,15 +133,22 @@ class AppInfoRow extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(value, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppTheme.textPrimary)), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.textPrimary, + ), + ), const SizedBox(height: 2), - Text(label, - style: const TextStyle( - fontSize: 12, color: AppTheme.textSecondary)), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: AppTheme.textSecondary, + ), + ), ], ), ), @@ -163,6 +170,7 @@ class AppFormField extends StatelessWidget { final Widget? suffix; final int? maxLines; final String? Function(String?)? validator; + final ValueChanged? onChanged; const AppFormField({ super.key, @@ -175,6 +183,7 @@ class AppFormField extends StatelessWidget { this.suffix, this.maxLines = 1, this.validator, + this.onChanged, }); @override @@ -182,11 +191,14 @@ class AppFormField extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: AppTheme.textSecondary)), + Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppTheme.textSecondary, + ), + ), const SizedBox(height: 6), TextFormField( controller: controller, @@ -195,6 +207,7 @@ class AppFormField extends StatelessWidget { keyboardType: keyboardType, maxLines: maxLines, validator: validator, + onChanged: onChanged, style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary), decoration: InputDecoration(hintText: hint, suffixIcon: suffix), ), @@ -253,9 +266,10 @@ class AppLabeledSwitch extends StatelessWidget { child: Row( children: [ Expanded( - child: Text(label, - style: const TextStyle( - fontSize: 14, color: AppTheme.textPrimary)), + child: Text( + label, + style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary), + ), ), Switch.adaptive( value: value, @@ -310,23 +324,33 @@ class AppMenuTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: titleColor ?? AppTheme.textPrimary)), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: titleColor ?? AppTheme.textPrimary, + ), + ), if (subtitle != null) ...[ const SizedBox(height: 2), - Text(subtitle!, - style: const TextStyle( - fontSize: 12, color: AppTheme.textSecondary)), + Text( + subtitle!, + style: const TextStyle( + fontSize: 12, + color: AppTheme.textSecondary, + ), + ), ], ], ), ), trailing ?? - const Icon(Icons.chevron_right, - color: AppTheme.textSecondary, size: 18), + const Icon( + Icons.chevron_right, + color: AppTheme.textSecondary, + size: 18, + ), ], ), ), @@ -365,11 +389,14 @@ class AppFormCard extends StatelessWidget { children: [ Icon(icon, color: AppTheme.primary, size: 18), const SizedBox(width: 8), - Text(title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppTheme.textPrimary)), + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppTheme.textPrimary, + ), + ), ], ), const SizedBox(height: 16), diff --git a/recolecta_app/lib/features/auth/register_page.dart b/recolecta_app/lib/features/auth/register_page.dart index d168de8..4d13124 100644 --- a/recolecta_app/lib/features/auth/register_page.dart +++ b/recolecta_app/lib/features/auth/register_page.dart @@ -10,9 +10,19 @@ import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_widgets.dart'; import '../../core/services/auth_controller.dart'; import '../../core/models/auth_state.dart'; -import '../addresses/colonias_selector.dart'; import '../../core/models/colonia.dart'; import '../home/colonias_data.dart'; +import '../addresses/colonias_provider.dart'; + +const Map _cpToColonia = { + '38000': 'Zona Centro', + '38060': 'Las Arboledas', + '38027': 'San Juanico', + '38037': 'Los Olivos', + '38090': 'Rancho Seco', + '38080': 'Las Insurgentes', + '38086': 'Trojes', +}; class RegisterPage extends ConsumerStatefulWidget { const RegisterPage({super.key}); @@ -37,7 +47,8 @@ class _RegisterPageState extends ConsumerState { final _calleCtrl = TextEditingController(); Colonia? _selectedColonia; LatLng? _selectedLocation; - int _radioAlerta = 200; + String _tipoInmueble = 'Casa'; + bool _whatsappNotif = false; @override void initState() { @@ -94,6 +105,95 @@ class _RegisterPageState extends ConsumerState { setState(() => _currentPage = 1); } + // Llama a la API de OpenStreetMap (Nominatim) para obtener la calle automáticamente + Future _fetchStreetName(LatLng latlng) async { + setState(() => _selectedLocation = latlng); + + try { + final dio = Dio(); + final response = await dio.get( + 'https://nominatim.openstreetmap.org/reverse', + queryParameters: { + 'lat': latlng.latitude, + 'lon': latlng.longitude, + 'format': 'json', + 'addressdetails': 1, + }, + options: Options(headers: {'User-Agent': 'com.onlineshack.recolecta'}), + ); + + if (response.data != null && response.data['address'] != null) { + final address = response.data['address']; + final road = + address['road'] ?? address['pedestrian'] ?? address['street'] ?? ''; + final houseNumber = address['house_number'] ?? ''; + + if (road.isNotEmpty) { + setState(() { + _calleCtrl.text = '$road $houseNumber'.trim(); + }); + } + } + } catch (e) { + debugPrint('Aviso: Error al obtener nombre de la calle de OSM: $e'); + } + } + + void _validarCP(String cp, List colonias) { + if (cp.length != 5) { + if (_selectedColonia != null) { + setState(() { + _selectedColonia = null; + _selectedLocation = null; + _calleCtrl.clear(); + }); + } + return; + } + + final nombre = _cpToColonia[cp]; + if (nombre == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Código postal fuera de nuestra zona de servicio actual.', + ), + backgroundColor: AppTheme.danger, + behavior: SnackBarBehavior.floating, + ), + ); + setState(() { + _selectedColonia = null; + _selectedLocation = null; + }); + return; + } + + final backendC = colonias + .where((c) => c.nombre.toLowerCase() == nombre.toLowerCase()) + .firstOrNull; + if (backendC == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Esta colonia aún no tiene horarios configurados.'), + backgroundColor: AppTheme.danger, + behavior: SnackBarBehavior.floating, + ), + ); + setState(() { + _selectedColonia = null; + _selectedLocation = null; + }); + return; + } + + setState(() { + _selectedColonia = backendC; + _selectedLocation = kColoniasCoordinates[nombre]; + }); + FocusScope.of(context).unfocus(); // Cierra el teclado + } + Future _register() async { if (_calleCtrl.text.trim().isEmpty || _selectedColonia == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -168,6 +268,8 @@ class _RegisterPageState extends ConsumerState { @override Widget build(BuildContext context) { final loading = ref.watch(authControllerProvider).isLoading; + final coloniasAsync = ref.watch(coloniasProvider); + final coloniasList = coloniasAsync.value ?? []; return Scaffold( backgroundColor: AppTheme.background, @@ -202,18 +304,14 @@ class _RegisterPageState extends ConsumerState { calleCtrl: _calleCtrl, selectedColonia: _selectedColonia, selectedLocation: _selectedLocation, - radioAlerta: _radioAlerta, + tipoInmueble: _tipoInmueble, + whatsappNotif: _whatsappNotif, loading: loading, - onColoniaChanged: (c) { - setState(() { - _selectedColonia = c; - if (c != null && kColoniasCoordinates.containsKey(c.nombre)) { - _selectedLocation = kColoniasCoordinates[c.nombre]; - } - }); - }, - onLocationChanged: (l) => setState(() => _selectedLocation = l), - onRadioChanged: (v) => setState(() => _radioAlerta = v), + onTipoChanged: (v) => setState(() => _tipoInmueble = v), + onCPChanged: (v) => _validarCP(v, coloniasList), + onLocationChanged: _fetchStreetName, + onWhatsappChanged: (v) => + setState(() => _whatsappNotif = v ?? false), onRegister: _register, ), ], @@ -386,11 +484,13 @@ class _Step2 extends StatelessWidget { final TextEditingController calleCtrl; final Colonia? selectedColonia; final LatLng? selectedLocation; - final int radioAlerta; + final String tipoInmueble; + final bool whatsappNotif; final bool loading; - final ValueChanged onColoniaChanged; + final ValueChanged onTipoChanged; + final ValueChanged onCPChanged; final ValueChanged onLocationChanged; - final ValueChanged onRadioChanged; + final ValueChanged onWhatsappChanged; final VoidCallback onRegister; const _Step2({ @@ -398,17 +498,28 @@ class _Step2 extends StatelessWidget { required this.calleCtrl, required this.selectedColonia, required this.selectedLocation, - required this.radioAlerta, + required this.tipoInmueble, + required this.whatsappNotif, required this.loading, - required this.onColoniaChanged, + required this.onTipoChanged, + required this.onCPChanged, required this.onLocationChanged, - required this.onRadioChanged, + required this.onWhatsappChanged, required this.onRegister, }); @override Widget build(BuildContext context) { final mapCenter = selectedLocation ?? const LatLng(20.5222, -100.8123); + + // Magia de privacidad: Restringir paneo a 1km a la redonda de la colonia + final bounds = selectedColonia != null + ? LatLngBounds( + LatLng(mapCenter.latitude - 0.01, mapCenter.longitude - 0.01), + LatLng(mapCenter.latitude + 0.01, mapCenter.longitude + 0.01), + ) + : null; + return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( @@ -420,84 +531,183 @@ class _Step2 extends StatelessWidget { title: 'Dirección de tu casa', child: Column( children: [ - AppFormField( - label: 'Código Postal', - hint: 'Ej. 38000', - controller: cpCtrl, - keyboardType: TextInputType.number, - ), - const SizedBox(height: 14), - ColoniasSelector( - labelText: 'Colonia', - initialValue: selectedColonia, - onChanged: onColoniaChanged, - ), - const SizedBox(height: 14), - AppFormField( - label: 'Calle y número', - hint: 'Av. Insurgentes 245', - controller: calleCtrl, - ), - const SizedBox(height: 16), const Text( - 'Toca el mapa para ubicar tu casa exacta:', + 'Tipo de inmueble', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textSecondary, ), ), - const SizedBox(height: 8), - Container( - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppTheme.radiusSm), - border: Border.all(color: AppTheme.border), - ), - clipBehavior: Clip.hardEdge, - child: FlutterMap( - key: ValueKey(selectedColonia?.nombre ?? 'default'), - options: MapOptions( - initialCenter: mapCenter, - initialZoom: 15.0, - onTap: (_, latlng) => onLocationChanged(latlng), - ), - children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.onlineshack.recolecta', + Row( + children: [ + Expanded( + child: RadioListTile( + title: const Text( + 'Casa', + style: TextStyle(fontSize: 14), + ), + value: 'Casa', + groupValue: tipoInmueble, + onChanged: (v) => onTipoChanged(v!), ), - if (selectedLocation != null) - MarkerLayer( - markers: [ - Marker( - point: selectedLocation!, - width: 40, - height: 40, - child: const Icon( - Icons.location_on, - color: AppTheme.danger, - size: 40, + ), + Expanded( + child: RadioListTile( + title: const Text( + 'Negocio', + style: TextStyle(fontSize: 14), + ), + value: 'Negocio', + groupValue: tipoInmueble, + onChanged: (v) => onTipoChanged(v!), + ), + ), + ], + ), + const SizedBox(height: 8), + AppFormField( + label: 'Código Postal', + hint: 'Ej. 38000', + controller: cpCtrl, + keyboardType: TextInputType.number, + onChanged: onCPChanged, + ), + + if (selectedColonia != null) ...[ + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppTheme.primaryLight.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(AppTheme.radiusSm), + border: Border.all(color: AppTheme.primaryMid), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.check_circle_outline, + color: AppTheme.primary, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Colonia: ${selectedColonia!.nombre}', + style: const TextStyle( + fontWeight: FontWeight.w600, + color: AppTheme.primaryDark, ), ), ], ), - ], + const SizedBox(height: 8), + Text( + 'Horario ${selectedColonia!.turno?.toLowerCase() ?? 'asignado'}', + style: const TextStyle( + fontSize: 13, + color: AppTheme.textPrimary, + ), + ), + Text( + selectedColonia!.horarioEstimado ?? + 'Sin horario especificado', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + ], + ), ), - ), + const SizedBox(height: 14), + AppFormField( + label: 'Calle y número', + hint: 'Av. Insurgentes 245', + controller: calleCtrl, + ), + const SizedBox(height: 16), + const Text( + 'Toca el mapa para ubicar tu casa exacta:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 8), + Container( + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppTheme.radiusSm), + border: Border.all(color: AppTheme.border), + ), + clipBehavior: Clip.hardEdge, + child: FlutterMap( + key: ValueKey(selectedColonia?.nombre ?? 'default'), + options: MapOptions( + initialCenter: mapCenter, + initialZoom: 15.0, + cameraConstraint: bounds != null + ? CameraConstraint.contain(bounds: bounds) + : const CameraConstraint.unconstrained(), + onTap: (_, latlng) => onLocationChanged(latlng), + ), + children: [ + TileLayer( + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.onlineshack.recolecta', + ), + if (selectedLocation != null) + MarkerLayer( + markers: [ + Marker( + point: selectedLocation!, + width: 40, + height: 40, + child: const Icon( + Icons.location_on, + color: AppTheme.danger, + size: 40, + ), + ), + ], + ), + ], + ), + ), + ] else ...[ + const SizedBox(height: 24), + const Center( + child: Text( + 'Ingresa un código postal con servicio\npara asignar tu colonia.', + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.textSecondary, + fontSize: 13, + ), + ), + ), + ], ], ), ), const SizedBox(height: 16), + + // ── Sección OCR (Privacidad por diseño) ── AppFormCard( - icon: Icons.notifications_outlined, - title: 'Distancia de alerta', + icon: Icons.document_scanner_outlined, + title: 'Verificación de Domicilio', child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Te avisamos cuando el camión esté a esta distancia de tu casa.', + 'Para prevenir abusos, requerimos validar tu dirección con un recibo (luz o agua). ' + 'Por privacidad, la imagen será borrada inmediatamente después de la lectura.', style: TextStyle( fontSize: 13, color: AppTheme.textSecondary, @@ -505,17 +715,46 @@ class _Step2 extends StatelessWidget { ), ), const SizedBox(height: 14), - ...[200, 400, 600].map( - (dist) => _RadioOption( - value: dist, - groupValue: radioAlerta, - label: '$dist metros', - sublabel: dist == 200 - ? '~2-3 min de anticipación' - : dist == 400 - ? '~4-5 min de anticipación' - : '~6-8 min de anticipación', - onChanged: onRadioChanged, + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + icon: const Icon( + Icons.upload_file, + color: AppTheme.primary, + ), + label: const Text( + 'Escanear recibo (OCR)', + style: TextStyle(color: AppTheme.primary), + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Abriendo cámara... (Próximamente)'), + ), + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // ── Sección WhatsApp ── + AppFormCard( + icon: Icons.chat_outlined, + title: 'Notificaciones Externas', + child: Column( + children: [ + CheckboxListTile( + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + activeColor: AppTheme.primary, + value: whatsappNotif, + onChanged: onWhatsappChanged, + title: const Text( + 'Recibir alertas del camión vía WhatsApp (Próximamente)', + style: TextStyle(fontSize: 14, color: AppTheme.textPrimary), ), ), ], diff --git a/recolecta_app/lib/features/home/citizen_home_screen.dart b/recolecta_app/lib/features/home/citizen_home_screen.dart index e0ed58e..22c2ab2 100644 --- a/recolecta_app/lib/features/home/citizen_home_screen.dart +++ b/recolecta_app/lib/features/home/citizen_home_screen.dart @@ -1,15 +1,314 @@ import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; -class CitizenHomeScreen extends StatelessWidget { +import '../../core/theme/app_theme.dart'; +import '../../core/models/ui_models.dart'; +import 'colonias_data.dart'; + +class CitizenHomeScreen extends StatefulWidget { const CitizenHomeScreen({super.key}); + @override + State createState() => _CitizenHomeScreenState(); +} + +class _CitizenHomeScreenState extends State { + bool _isLoading = true; + List _casas = []; + Map _etas = {}; + Map _horarios = {}; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + try { + const storage = FlutterSecureStorage(); + final token = await storage.read(key: 'token') ?? ''; + + if (token.isEmpty) { + if (mounted) setState(() => _isLoading = false); + return; + } + + final dio = Dio( + BaseOptions( + baseUrl: const String.fromEnvironment( + 'API_BASE_URL', + defaultValue: 'http://localhost:8000', + ), + headers: {'Authorization': 'Bearer $token'}, + ), + ); + + // 1. Obtener horarios de las colonias + try { + final colRes = await dio.get('/colonias'); + if (colRes.data is List) { + for (var c in colRes.data) { + final nombre = c['nombre'] ?? c['colonia'] ?? ''; + final horario = c['horario_estimado'] ?? c['schedule'] ?? 'Horario no definido'; + if (nombre.isNotEmpty) { + _horarios[nombre] = horario; + } + } + } + } catch (_) { + debugPrint('Aviso: No se pudieron cargar los horarios.'); + } + + // 2. Obtener los domicilios del ciudadano + final res = await dio.get('/addresses'); + List loadedCasas = []; + if (res.data is List) { + loadedCasas = (res.data as List).map((e) => UIHouseModel.fromJson(e)).toList(); + } + + // 3. Obtener ETA (Tiempo Estimado) para cada domicilio + Map loadedEtas = {}; + for (var casa in loadedCasas) { + try { + final etaRes = await dio.get('/eta', queryParameters: {'address_id': casa.id}); + loadedEtas[casa.id] = etaRes.data['mensaje'] ?? 'Estado desconocido'; + } catch (e) { + loadedEtas[casa.id] = 'Calculando...'; + } + } + + if (mounted) { + setState(() { + _casas = loadedCasas; + _etas = loadedEtas; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Error en CitizenHomeScreen: $e'); + if (mounted) { + setState(() => _isLoading = false); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Inicio')), - body: const Center( - child: Text('TODO: Citizen Home Screen - Mostrar tarjeta ETA'), + backgroundColor: AppTheme.background, + appBar: AppBar( + title: const Text('Estado del Servicio'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Actualizar tiempos', + onPressed: () { + setState(() => _isLoading = true); + _loadData(); + }, + ) + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _casas.isEmpty + ? const Center( + child: Text( + 'No tienes domicilios registrados.', + style: TextStyle(color: AppTheme.textSecondary), + ), + ) + : ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: _casas.length, + separatorBuilder: (_, __) => const SizedBox(height: 24), + itemBuilder: (context, index) { + final casa = _casas[index]; + final eta = _etas[casa.id] ?? 'Actualizando...'; + final horario = _horarios[casa.colonia] ?? 'Horario asignado a la ruta'; + return _HouseEtaCard(casa: casa, etaMsg: eta, horario: horario); + }, + ), + ); + } +} + +// ── Widget para la Tarjeta de Mapa y ETA ───────────────────────────────────── +class _HouseEtaCard extends StatelessWidget { + final UIHouseModel casa; + final String etaMsg; + final String horario; + + const _HouseEtaCard({ + required this.casa, + required this.etaMsg, + required this.horario, + }); + + @override + Widget build(BuildContext context) { + final center = kColoniasCoordinates[casa.colonia] ?? const LatLng(20.5222, -100.8123); + + // Restricción del mapa a la colonia (Privacidad por Diseño) + final bounds = LatLngBounds( + LatLng(center.latitude - 0.01, center.longitude - 0.01), + LatLng(center.latitude + 0.01, center.longitude + 0.01), + ); + + return Container( + decoration: BoxDecoration( + color: AppTheme.surface, + borderRadius: BorderRadius.circular(AppTheme.radiusLg), + border: Border.all(color: AppTheme.border), + boxShadow: AppTheme.cardShadow, + ), + clipBehavior: Clip.hardEdge, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Mapa Restringido ── + SizedBox( + height: 180, + child: FlutterMap( + options: MapOptions( + initialCameraFit: CameraFit.bounds(bounds: bounds), + cameraConstraint: CameraConstraint.contain(bounds: bounds), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.drag | InteractiveFlag.pinchZoom, + ), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.onlineshack.recolecta', + ), + CircleLayer( + circles: [ + CircleMarker( + point: center, + color: AppTheme.primary.withValues(alpha: 0.15), + borderColor: AppTheme.primary, + borderStrokeWidth: 2, + radius: 400, + useRadiusInMeter: true, + ), + ], + ), + ], + ), + ), + + // ── Recuadro de Información ── + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.home, color: AppTheme.primary, size: 20), + const SizedBox(width: 8), + Text( + casa.alias.isNotEmpty ? casa.alias : 'Mi Domicilio', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppTheme.textPrimary, + ), + ), + ], + ), + const SizedBox(height: 14), + _InfoRow(icon: Icons.location_on_outlined, title: 'Dirección', value: casa.direccionCompleta), + const SizedBox(height: 12), + _InfoRow(icon: Icons.schedule_outlined, title: 'Horario Habitual', value: horario), + const SizedBox(height: 18), + + // ── Alerta de ETA en Tiempo Real ── + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.primaryLight, + borderRadius: BorderRadius.circular(AppTheme.radiusSm), + border: Border.all(color: AppTheme.primaryMid), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.local_shipping_outlined, color: AppTheme.primaryDark), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Estado del Camión', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppTheme.primaryDark, + ), + ), + const SizedBox(height: 4), + Text( + etaMsg, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.primaryDark, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], ), ); } } + +// ── Fila auxiliar de info ──────────────────────────────────────────────────── +class _InfoRow extends StatelessWidget { + final IconData icon; + final String title; + final String value; + + const _InfoRow({required this.icon, required this.title, required this.value}); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 18, color: AppTheme.textSecondary), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary, height: 1.3), + ), + ], + ), + ), + ], + ); + } +} diff --git a/views_v1/.gitignore b/views_v1/.gitignore new file mode 100644 index 0000000..6f0d006 --- /dev/null +++ b/views_v1/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/views_v1/.metadata b/views_v1/.metadata new file mode 100644 index 0000000..7457e0e --- /dev/null +++ b/views_v1/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: windows + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/views_v1/README.md b/views_v1/README.md new file mode 100644 index 0000000..9be068e --- /dev/null +++ b/views_v1/README.md @@ -0,0 +1,3 @@ +# rutaverde + +A new Flutter project. diff --git a/views_v1/analysis_options.yaml b/views_v1/analysis_options.yaml new file mode 100644 index 0000000..8e4c4f5 --- /dev/null +++ b/views_v1/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/views_v1/pubspec.lock b/views_v1/pubspec.lock new file mode 100644 index 0000000..4b0f5ad --- /dev/null +++ b/views_v1/pubspec.lock @@ -0,0 +1,770 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598" + url: "https://pub.dev" + source: hosted + version: "1.3.71" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + 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" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" + url: "https://pub.dev" + source: hosted + version: "0.7.13" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + 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_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57" + url: "https://pub.dev" + source: hosted + version: "3.7.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7" + url: "https://pub.dev" + source: hosted + version: "16.2.2" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb" + url: "https://pub.dev" + source: hosted + version: "4.7.11" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776" + url: "https://pub.dev" + source: hosted + version: "4.1.7" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + url: "https://pub.dev" + source: hosted + version: "18.0.1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" + 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" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" + url: "https://pub.dev" + source: hosted + version: "8.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: fc714bf8072e2c121d4277cb6dca23bbfae954b6c7b5d6dd73f1bc8d09762921 + url: "https://pub.dev" + source: hosted + version: "2.17.0" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: f1eb5ffa34ba41f8591e53ce439f78af179a506e8386a1297d0ecd202e05c734 + url: "https://pub.dev" + source: hosted + version: "2.19.8" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: "5ed8d8d0f93dfa7f5039c409c500948e98e59068f8f6fcf9105bfd07e3709d7f" + url: "https://pub.dev" + source: hosted + version: "2.18.1" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: ddbe34435dfb34e83fca295c6a8dcc53c3b51487e9eec3c737ce4ae605574347 + url: "https://pub.dev" + source: hosted + version: "2.15.0" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: "9b068070bf18b5ec6a7d8ac512c7d557377dbe267658d264d2095b7ee4f1f6c5" + url: "https://pub.dev" + source: hosted + version: "0.6.2+1" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + 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" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + 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" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + 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: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" +sdks: + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/views_v1/pubspec.yaml b/views_v1/pubspec.yaml new file mode 100644 index 0000000..520c6e3 --- /dev/null +++ b/views_v1/pubspec.yaml @@ -0,0 +1,33 @@ +name: rutaverde +description: Rastreo del camión de basura en tiempo real + +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + google_maps_flutter: ^2.5.0 + geolocator: ^14.0.2 + flutter_local_notifications: ^18.0.1 + firebase_core: ^4.9.0 + firebase_messaging: ^16.2.2 + provider: ^6.1.1 + shared_preferences: ^2.2.2 + http: ^1.1.0 + intl: ^0.20.2 + permission_handler: ^12.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true + assets: + - assets/images/ diff --git a/views_v1/web/favicon.png b/views_v1/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a1a9858774ff07f0e94f9cbf80f1f96fa234d09 GIT binary patch literal 1282 zcmY*ZO-~bH5Z*#VpirnqBZ7goMNL$8+X_gRZGl>#<)e^NwZ&*Cv`9m{E#0*r@^v(Z z#G8p6xESvo)dL|hYK+lnIEW|x3H$}0>04Lb%skJ`yzkDu@62XT4i9ES<7QL4iQ_nP zFyIe^Eu{5zz^;?S14taEK@E@lIOd-RA=Hoy#8i$G?4*VgfEzXxRX+@Pv9$RIH1e?O5jjwFcRz0@p^elffn@{q6La*Sk zc}+ae%lX8z6!wo*Ux(+ht!P?K62)S%C={;?+57|1>2|wChf8$1da$EMEh}2Q)T5|& zmgG+!e^On_r*c{DZke#S9bmvtJaZ4D*n%5*i9=@T-9rmMZIla(-hi#IfgcagZ`1oz^TnS&rHXN zWplmgMa;iwdixk`3j~3DccXm*3|V6YthG#E(H$68Yk`|)jjfL`x}{kPu*!Dk&a{KASD9mC)a(M6L!b8VY$T3e6cq>D z7&Oh670}r^X6Kv0Yq5Uj0iDcR2g%e;h;fTC@Z&4pOi+;p-KFb6l60$!>%aE(h4~aU z{+^+lYyeIw0~l^df7q=dx24>bnFs6g{h1&3(UXY;3(+FwjY2170Cglxwc8dR0jV=Z z8cR2njx`EpVr}~I92U*B8UWE;R;r&vwI_%)Xc4xBhTB_5P2QMtje+tk4opu1=4ix_ zbI_p?I#{QJjzPZ&CWXrPQSn3;xzP^3>j9|HRp@PvS}mmjH0Gpvi?&o-T|V=Erz0@2 zO_y>O8i9eb@^xq1`tA*EKQvC&1SsUo9>7?)5TJ$QmyO#7&ecgHrJ+OUHxwKV(W;NS F_8W^Bl`H@N literal 0 HcmV?d00001 diff --git a/views_v1/web/icons/Icon-192.png b/views_v1/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e8cb6f90808ddd4ddc88f0144fe7c23fc550d4 GIT binary patch literal 9566 zcmai4`E%6Al^=-_x(`MtItSenx`l2Hx~0*9G?LJLqS2rebj;}1Nb!}#PS%c>UB`rq zy}o06Yj=Z>RI)y*fPjH4I0hVy!n*c{O;u`>Q2QV3=cCtrN5++G`SXr`-QVul@4bEl zBo99Q*wWhI?-S{x)9L&h&9xnn4?g`^xR*QR{ZBBq=yZM%>&JT?9Vb?d^NVvQr)F(N z$J)HjXj`2+sne}u`p(&b`t_MXA-#i-wDX4)+xc)47g&1s!1DWkm z3oNN2sbYGBCvc}M+g|IM4Eq1{(c7Y}4nnr=T>`ylb4x5Ewc1SiQK(LB=%#6!V(Ju? z-;{_NG(hE)41yasMG-!u4Teu^>bI2$tLp+rnV#OeDh`95y$0wi9EnivI}9oi2-*;J zv`pmk+MJJQ_g`_j77NqzT2`>kd?T_4#TLHvQ&D)ig%wk1Mi&%kX9e%E-f-oyG|0&A z)pQ8Kx+zMO%+_+v2&H6b^3WJ$a7VA0dFqm=nGhi>qUS>J@Ne9Ftt0`ad|3fLUQ%vg ztO`IS%$5PPwDSQt%Mf6kCj45$B4h~e6s<@EGZ*1tr2$ii!9xAPW
  • V1h#;3jgEB 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 Qzo2xw#=2JeRXqy!f8B!vtN;K2 literal 0 HcmV?d00001 diff --git a/views_v1/web/icons/Icon-512.png b/views_v1/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..8911d28ae6c71de51eca113c78116e75f071f32c GIT binary patch literal 14826 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 literal 0 HcmV?d00001 diff --git a/views_v1/web/icons/Icon-maskable-192.png b/views_v1/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..f7794c29380cf2bb066833b579bdfca5f3f4be9d GIT binary patch literal 10086 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 literal 0 HcmV?d00001 diff --git a/views_v1/web/index.html b/views_v1/web/index.html new file mode 100644 index 0000000..f170de7 --- /dev/null +++ b/views_v1/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + rutaverde + + + + + + + diff --git a/views_v1/web/manifest.json b/views_v1/web/manifest.json new file mode 100644 index 0000000..d6e9a14 --- /dev/null +++ b/views_v1/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "rutaverde", + "short_name": "rutaverde", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/views_v1/windows/.gitignore b/views_v1/windows/.gitignore new file mode 100644 index 0000000..ec4098a --- /dev/null +++ b/views_v1/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/views_v1/windows/CMakeLists.txt b/views_v1/windows/CMakeLists.txt new file mode 100644 index 0000000..b8ebe4b --- /dev/null +++ b/views_v1/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(rutaverde LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "rutaverde") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/views_v1/windows/flutter/CMakeLists.txt b/views_v1/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..efb62eb --- /dev/null +++ b/views_v1/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/views_v1/windows/flutter/generated_plugin_registrant.cc b/views_v1/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..fae1d07 --- /dev/null +++ b/views_v1/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); +} diff --git a/views_v1/windows/flutter/generated_plugin_registrant.h b/views_v1/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/views_v1/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/views_v1/windows/flutter/generated_plugins.cmake b/views_v1/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..38da8cb --- /dev/null +++ b/views_v1/windows/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + firebase_core + geolocator_windows + permission_handler_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/views_v1/windows/runner/CMakeLists.txt b/views_v1/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..2041a04 --- /dev/null +++ b/views_v1/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/views_v1/windows/runner/Runner.rc b/views_v1/windows/runner/Runner.rc new file mode 100644 index 0000000..1be76d9 --- /dev/null +++ b/views_v1/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "rutaverde" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "rutaverde" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "rutaverde.exe" "\0" + VALUE "ProductName", "rutaverde" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/views_v1/windows/runner/flutter_window.cpp b/views_v1/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..c819cb0 --- /dev/null +++ b/views_v1/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/views_v1/windows/runner/flutter_window.h b/views_v1/windows/runner/flutter_window.h new file mode 100644 index 0000000..28c2383 --- /dev/null +++ b/views_v1/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/views_v1/windows/runner/main.cpp b/views_v1/windows/runner/main.cpp new file mode 100644 index 0000000..6669ced --- /dev/null +++ b/views_v1/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"rutaverde", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/views_v1/windows/runner/resource.h b/views_v1/windows/runner/resource.h new file mode 100644 index 0000000..ddc7f3e --- /dev/null +++ b/views_v1/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/views_v1/windows/runner/resources/app_icon.ico b/views_v1/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..515c281e1c50a3be16ba85ed28de65f4167b1bc3 GIT binary patch literal 53658 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 + + + + PerMonitorV2 + + + + + + + + + diff --git a/views_v1/windows/runner/utils.cpp b/views_v1/windows/runner/utils.cpp new file mode 100644 index 0000000..b7f62cc --- /dev/null +++ b/views_v1/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/views_v1/windows/runner/utils.h b/views_v1/windows/runner/utils.h new file mode 100644 index 0000000..3f0e05c --- /dev/null +++ b/views_v1/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/views_v1/windows/runner/win32_window.cpp b/views_v1/windows/runner/win32_window.cpp new file mode 100644 index 0000000..b5ba2a0 --- /dev/null +++ b/views_v1/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/views_v1/windows/runner/win32_window.h b/views_v1/windows/runner/win32_window.h new file mode 100644 index 0000000..49b847f --- /dev/null +++ b/views_v1/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/views_v2/app.dart b/views_v2/app.dart new file mode 100644 index 0000000..e90cff9 --- /dev/null +++ b/views_v2/app.dart @@ -0,0 +1,144 @@ +// lib/app.dart +// Root de la app: go_router + bottom navigation de 4 tabs (P3). + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'features/eta/eta_screen.dart'; +import 'features/notifications/notifications_screen.dart'; +import 'features/feedback/feedback_screen.dart'; +import 'features/quiz/quiz_screen.dart'; + +// ────────────────────────────────────────── +// Router +// ────────────────────────────────────────── +final _router = GoRouter( + initialLocation: '/eta', + routes: [ + ShellRoute( + builder: (context, state, child) => _ScaffoldWithNav(child: child), + routes: [ + GoRoute( + path: '/eta', + pageBuilder: (context, state) => const NoTransitionPage( + child: EtaScreen(), + ), + ), + GoRoute( + path: '/notifications', + pageBuilder: (context, state) => const NoTransitionPage( + child: NotificationsScreen(), + ), + ), + GoRoute( + path: '/feedback', + pageBuilder: (context, state) => const NoTransitionPage( + child: FeedbackScreen(), + ), + ), + GoRoute( + path: '/quiz', + pageBuilder: (context, state) => const NoTransitionPage( + child: QuizScreen(), + ), + ), + ], + ), + ], +); + +// ────────────────────────────────────────── +// App widget +// ────────────────────────────────────────── +class RecolectaApp extends StatelessWidget { + const RecolectaApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Recolecta', + debugShowCheckedModeBanner: false, + theme: _buildTheme(Brightness.light), + darkTheme: _buildTheme(Brightness.dark), + themeMode: ThemeMode.system, + routerConfig: _router, + ); + } + + ThemeData _buildTheme(Brightness brightness) { + final isDark = brightness == Brightness.dark; + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF1D9E75), // teal-400 + brightness: brightness, + ), + useMaterial3: true, + appBarTheme: AppBarTheme( + backgroundColor: + isDark ? const Color(0xFF1A1A1A) : Colors.white, + foregroundColor: isDark ? Colors.white : Colors.black87, + elevation: 0, + scrolledUnderElevation: 1, + titleTextStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Colors.black87, + ), + ), + ); + } +} + +// ────────────────────────────────────────── +// Bottom navigation shell +// ────────────────────────────────────────── +class _ScaffoldWithNav extends StatelessWidget { + final Widget child; + const _ScaffoldWithNav({required this.child}); + + static const _tabs = [ + _TabItem(path: '/eta', icon: Icons.schedule_rounded, label: 'ETA'), + _TabItem( + path: '/notifications', + icon: Icons.notifications_outlined, + label: 'Avisos'), + _TabItem( + path: '/feedback', + icon: Icons.feedback_outlined, + label: 'Buzón'), + _TabItem( + path: '/quiz', icon: Icons.quiz_outlined, label: 'Quiz'), + ]; + + @override + Widget build(BuildContext context) { + final location = GoRouterState.of(context).uri.path; + final currentIndex = + _tabs.indexWhere((t) => location.startsWith(t.path)); + + return Scaffold( + body: child, + bottomNavigationBar: NavigationBar( + selectedIndex: currentIndex < 0 ? 0 : currentIndex, + onDestinationSelected: (i) => context.go(_tabs[i].path), + destinations: _tabs + .map( + (t) => NavigationDestination( + icon: Icon(t.icon), + label: t.label, + ), + ) + .toList(), + height: 64, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ), + ); + } +} + +class _TabItem { + final String path; + final IconData icon; + final String label; + const _TabItem( + {required this.path, required this.icon, required this.label}); +} \ No newline at end of file diff --git a/views_v2/dio_client.dart b/views_v2/dio_client.dart new file mode 100644 index 0000000..c55dbe4 --- /dev/null +++ b/views_v2/dio_client.dart @@ -0,0 +1,53 @@ +// lib/core/dio_client.dart +// Cliente HTTP configurado con base URL, interceptor de JWT y timeouts. + +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// La base URL viene de las variables de entorno en flutter_dotenv o dart-define. +// Para el emulador Android: http://10.0.2.2:8000 +// Para producción: https://tu-backend.run.app +const String _kBaseUrl = String.fromEnvironment( + 'API_BASE_URL', + defaultValue: 'http://10.0.2.2:8000', +); + +// Token JWT — se rellena desde el provider de auth tras login +String? _jwtToken; + +void setJwtToken(String token) => _jwtToken = token; +void clearJwtToken() => _jwtToken = null; + +Dio _buildDio() { + final dio = Dio( + BaseOptions( + baseUrl: _kBaseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 15), + headers: {'Content-Type': 'application/json'}, + ), + ); + + // Interceptor: adjunta JWT en cada petición + dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + if (_jwtToken != null) { + options.headers['Authorization'] = 'Bearer $_jwtToken'; + } + handler.next(options); + }, + onError: (error, handler) { + // 401 → limpiar token (el router de go_router redirige al login) + if (error.response?.statusCode == 401) { + clearJwtToken(); + } + handler.next(error); + }, + ), + ); + + return dio; +} + +final dioProvider = Provider((ref) => _buildDio()); \ No newline at end of file diff --git a/views_v2/feedback_model.dart b/views_v2/feedback_model.dart new file mode 100644 index 0000000..e0ee2f5 --- /dev/null +++ b/views_v2/feedback_model.dart @@ -0,0 +1,39 @@ +// lib/features/feedback/feedback_model.dart +// La queja solo registra target_unit_id (número de unidad), NUNCA el chofer. + +enum FeedbackType { + noPaso('no_paso', 'No pasó el camión'), + llegoTarde('llego_tarde', 'Llegó tarde'), + comportamiento('comportamiento', 'Comportamiento'), + otro('otro', 'Otro'); + + final String value; + final String label; + const FeedbackType(this.value, this.label); +} + +class FeedbackRequest { + final String addressId; + final FeedbackType type; + final int rating; // 1-5 + final String? message; + /// Solo el número de unidad — nunca el ID del chofer. + final String targetUnitId; + + const FeedbackRequest({ + required this.addressId, + required this.type, + required this.rating, + required this.targetUnitId, + this.message, + }); + + Map toJson() => { + 'address_id': addressId, + 'type': type.value, + 'rating': rating, + 'target_unit_id': targetUnitId, // ej. "101" + if (message != null && message!.isNotEmpty) 'message': message, + // ⚠️ NUNCA se manda: driver_id, driver_name, chofer_* + }; +} \ No newline at end of file diff --git a/views_v2/feedback_provider.dart b/views_v2/feedback_provider.dart new file mode 100644 index 0000000..c822111 --- /dev/null +++ b/views_v2/feedback_provider.dart @@ -0,0 +1,104 @@ +// lib/features/feedback/feedback_provider.dart + +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/dio_client.dart'; +import 'feedback_model.dart'; + +// ────────────────────────────────────────── +// Service +// ────────────────────────────────────────── +class FeedbackService { + final Dio _dio; + FeedbackService(this._dio); + + Future submit(FeedbackRequest req) async { + await _dio.post('/feedback', data: req.toJson()); + } +} + +final feedbackServiceProvider = Provider( + (ref) => FeedbackService(ref.read(dioProvider)), +); + +// ────────────────────────────────────────── +// Estado del formulario +// ────────────────────────────────────────── +enum FeedbackFormStatus { idle, loading, success, error } + +class FeedbackFormState { + final FeedbackType selectedType; + final int rating; + final String message; + final FeedbackFormStatus status; + final String? errorMessage; + + const FeedbackFormState({ + this.selectedType = FeedbackType.noPaso, + this.rating = 3, + this.message = '', + this.status = FeedbackFormStatus.idle, + this.errorMessage, + }); + + FeedbackFormState copyWith({ + FeedbackType? selectedType, + int? rating, + String? message, + FeedbackFormStatus? status, + String? errorMessage, + }) { + return FeedbackFormState( + selectedType: selectedType ?? this.selectedType, + rating: rating ?? this.rating, + message: message ?? this.message, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} + +// ────────────────────────────────────────── +// Notifier +// ────────────────────────────────────────── +class FeedbackNotifier extends Notifier { + @override + FeedbackFormState build() => const FeedbackFormState(); + + void setType(FeedbackType type) => + state = state.copyWith(selectedType: type); + + void setRating(int r) => state = state.copyWith(rating: r); + + void setMessage(String m) => state = state.copyWith(message: m); + + void reset() => state = const FeedbackFormState(); + + Future submit({ + required String addressId, + required String unitId, + }) async { + state = state.copyWith(status: FeedbackFormStatus.loading); + try { + final req = FeedbackRequest( + addressId: addressId, + type: state.selectedType, + rating: state.rating, + message: state.message, + targetUnitId: unitId, + ); + await ref.read(feedbackServiceProvider).submit(req); + state = state.copyWith(status: FeedbackFormStatus.success); + } on DioException catch (e) { + state = state.copyWith( + status: FeedbackFormStatus.error, + errorMessage: e.message ?? 'Error al enviar', + ); + } + } +} + +final feedbackProvider = + NotifierProvider( + FeedbackNotifier.new, +); \ No newline at end of file diff --git a/views_v2/feedback_screen.dart b/views_v2/feedback_screen.dart new file mode 100644 index 0000000..b9ffee1 --- /dev/null +++ b/views_v2/feedback_screen.dart @@ -0,0 +1,354 @@ +// lib/features/feedback/feedback_screen.dart +// Buzón de retroalimentación. Expone "Unidad 101", nunca el nombre del chofer. + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'feedback_model.dart'; +import 'feedback_provider.dart'; +import '../eta/eta_provider.dart'; // activeAddressIdProvider + +// El unitId activo se obtiene del ETA response o de la sesión del chofer. +// Por simplidad se provee aquí; en producción viene del provider de sesión. +final activeUnitIdProvider = StateProvider((ref) => '101'); + +class FeedbackScreen extends ConsumerStatefulWidget { + const FeedbackScreen({super.key}); + + @override + ConsumerState createState() => _FeedbackScreenState(); +} + +class _FeedbackScreenState extends ConsumerState { + final _messageController = TextEditingController(); + + @override + void dispose() { + _messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final formState = ref.watch(feedbackProvider); + + if (formState.status == FeedbackFormStatus.success) { + return _SuccessView(onReset: () { + ref.read(feedbackProvider.notifier).reset(); + _messageController.clear(); + }); + } + + return Scaffold( + appBar: AppBar(title: const Text('Buzón de retroalimentación')), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Tipo de reporte + const _SectionLabel('Tipo de reporte'), + const SizedBox(height: 8), + _TypeChips( + selected: formState.selectedType, + onSelect: (t) => ref.read(feedbackProvider.notifier).setType(t), + ), + const SizedBox(height: 20), + + // Rating + const _SectionLabel('Calificación del servicio'), + const SizedBox(height: 8), + _StarRating( + rating: formState.rating, + onRate: (r) => ref.read(feedbackProvider.notifier).setRating(r), + ), + const SizedBox(height: 20), + + // Unidad (sin exponer chofer) + const _SectionLabel('Unidad involucrada'), + const SizedBox(height: 8), + const _UnitBadge(), + const SizedBox(height: 20), + + // Mensaje libre + const _SectionLabel('Descripción (opcional)'), + const SizedBox(height: 8), + TextField( + controller: _messageController, + maxLines: 4, + maxLength: 300, + onChanged: (v) => + ref.read(feedbackProvider.notifier).setMessage(v), + decoration: InputDecoration( + hintText: 'Cuéntanos qué pasó...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + const SizedBox(height: 8), + + // Error + if (formState.status == FeedbackFormStatus.error) + _ErrorBanner(message: formState.errorMessage ?? 'Error'), + + const SizedBox(height: 8), + + // Submit + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: formState.status == FeedbackFormStatus.loading + ? null + : () { + final addressId = ref.read(activeAddressIdProvider); + final unitId = ref.read(activeUnitIdProvider); + if (addressId == null) return; + ref.read(feedbackProvider.notifier).submit( + addressId: addressId, + unitId: unitId, + ); + }, + child: formState.status == FeedbackFormStatus.loading + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Enviar reporte'), + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } +} + +// ────────────────────────────────────────── +// Chips de tipo +// ────────────────────────────────────────── +class _TypeChips extends StatelessWidget { + final FeedbackType selected; + final ValueChanged onSelect; + const _TypeChips({required this.selected, required this.onSelect}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: FeedbackType.values.map((t) { + final isSelected = t == selected; + return ChoiceChip( + label: Text(t.label), + selected: isSelected, + onSelected: (_) => onSelect(t), + selectedColor: const Color(0xFFE1F5EE), + side: BorderSide( + color: isSelected + ? const Color(0xFF5DCAA5) + : Theme.of(context).colorScheme.outlineVariant, + ), + labelStyle: TextStyle( + color: isSelected + ? const Color(0xFF085041) + : Theme.of(context).colorScheme.onSurface, + fontSize: 13, + ), + ); + }).toList(), + ); + } +} + +// ────────────────────────────────────────── +// Stars +// ────────────────────────────────────────── +class _StarRating extends StatelessWidget { + final int rating; + final ValueChanged onRate; + const _StarRating({required this.rating, required this.onRate}); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(5, (i) { + final filled = i < rating; + return GestureDetector( + onTap: () => onRate(i + 1), + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Icon( + filled ? Icons.star_rounded : Icons.star_outline_rounded, + size: 32, + color: filled + ? const Color(0xFFEF9F27) + : Theme.of(context).colorScheme.outlineVariant, + ), + ), + ); + }), + ); + } +} + +// ────────────────────────────────────────── +// Badge de unidad (sin exponer chofer) +// ────────────────────────────────────────── +class _UnitBadge extends ConsumerWidget { + const _UnitBadge(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final unitId = ref.watch(activeUnitIdProvider); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.local_shipping_outlined, size: 16), + const SizedBox(width: 8), + Text( + 'Unidad $unitId', + style: const TextStyle(fontSize: 13), + ), + ], + ), + ), + const SizedBox(height: 6), + Row( + children: [ + Icon( + Icons.shield_outlined, + size: 13, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + 'Solo se registra el número de unidad. El operador no es identificado.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ); + } +} + +// ────────────────────────────────────────── +// Error banner +// ────────────────────────────────────────── +class _ErrorBanner extends StatelessWidget { + final String message; + const _ErrorBanner({required this.message}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.error_outline, size: 16), + const SizedBox(width: 8), + Expanded(child: Text(message, style: const TextStyle(fontSize: 12))), + ], + ), + ); + } +} + +// ────────────────────────────────────────── +// Success view +// ────────────────────────────────────────── +class _SuccessView extends StatelessWidget { + final VoidCallback onReset; + const _SuccessView({required this.onReset}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Buzón de retroalimentación')), + body: Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + color: Color(0xFFE1F5EE), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check_circle_outline_rounded, + size: 36, + color: Color(0xFF1D9E75), + ), + ), + const SizedBox(height: 16), + const Text( + 'Reporte enviado', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + Text( + 'Gracias. Tu retroalimentación ayuda a mejorar el servicio. El reporte fue registrado de forma anónima.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.5, + ), + ), + const SizedBox(height: 24), + OutlinedButton( + onPressed: onReset, + child: const Text('Enviar otro reporte'), + ), + ], + ), + ), + ), + ); + } +} + +// ────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────── +class _SectionLabel extends StatelessWidget { + final String text; + const _SectionLabel(this.text); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } +} \ No newline at end of file diff --git a/views_v2/main.dart b/views_v2/main.dart new file mode 100644 index 0000000..46b8388 --- /dev/null +++ b/views_v2/main.dart @@ -0,0 +1,25 @@ +// lib/main.dart +// Punto de entrada. Inicializa Firebase, FCM, y monta el árbol Riverpod. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'features/notifications/notification_service.dart'; +import 'app.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Inicializar Firebase (requiere google-services.json en android/app/) + await Firebase.initializeApp(); + + // Inicializar FCM: permisos, canal Android, handlers foreground/background + await NotificationService.initialize(); + + runApp( + // ProviderScope es el contenedor global de Riverpod + const ProviderScope( + child: RecolectaApp(), + ), + ); +} \ No newline at end of file diff --git a/views_v2/notifications_screen.dart b/views_v2/notifications_screen.dart new file mode 100644 index 0000000..ef7a4b3 --- /dev/null +++ b/views_v2/notifications_screen.dart @@ -0,0 +1,378 @@ +// lib/features/notifications/notifications_screen.dart +// Historial de notificaciones FCM recibidas. +// Los items se almacenan en memoria (no en BD) — solo mensajes del topic propio. + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'notification_service.dart'; +import '../eta/eta_screen.dart'; // activeRouteIdProvider + +// ────────────────────────────────────────── +// Modelo local de item de notificación +// ────────────────────────────────────────── +enum FcmEventType { routeStart, truckProximity, routeCompleted, reassignment, unknown } + +FcmEventType _eventTypeFromMessage(RemoteMessage msg) { + final type = msg.data['event'] as String?; + switch (type) { + case 'ROUTE_START': + return FcmEventType.routeStart; + case 'TRUCK_PROXIMITY': + return FcmEventType.truckProximity; + case 'ROUTE_COMPLETED': + return FcmEventType.routeCompleted; + case 'reasignacion': + case 'retraso': + return FcmEventType.reassignment; + default: + return FcmEventType.unknown; + } +} + +class NotificationItem { + final String title; + final String body; + final FcmEventType type; + final DateTime receivedAt; + + const NotificationItem({ + required this.title, + required this.body, + required this.type, + required this.receivedAt, + }); +} + +// ────────────────────────────────────────── +// Provider: lista de notificaciones en memoria +// ────────────────────────────────────────── +final notificationsListProvider = + NotifierProvider>( + NotificationsNotifier.new, +); + +class NotificationsNotifier extends Notifier> { + @override + List build() { + // Escuchar mensajes FCM en foreground + NotificationService.onFcmMessage.addListener(_onMessage); + ref.onDispose( + () => NotificationService.onFcmMessage.removeListener(_onMessage), + ); + return []; + } + + void _onMessage() { + final msg = NotificationService.onFcmMessage.lastMessage; + if (msg == null) return; + final item = NotificationItem( + title: msg.notification?.title ?? 'Recolección', + body: msg.notification?.body ?? '', + type: _eventTypeFromMessage(msg), + receivedAt: DateTime.now(), + ); + state = [item, ...state]; + } + + void clearAll() => state = []; +} + +// ────────────────────────────────────────── +// Pantalla de notificaciones +// ────────────────────────────────────────── +class NotificationsScreen extends ConsumerWidget { + const NotificationsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final items = ref.watch(notificationsListProvider); + final routeId = ref.watch(activeRouteIdProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Notificaciones'), + actions: [ + if (items.isNotEmpty) + TextButton( + onPressed: () => + ref.read(notificationsListProvider.notifier).clearAll(), + child: const Text('Limpiar'), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + children: [ + // Badge de suscripción FCM + _FcmTopicBadge(routeId: routeId), + const SizedBox(height: 12), + + // Aviso de privacidad + _PrivacyNote(), + const SizedBox(height: 16), + + if (items.isEmpty) + const _EmptyState() + else ...[ + const _SectionLabel(label: 'Recientes'), + ...items.map((item) => _NotificationCard(item: item)), + ], + ], + ), + ); + } +} + +// ────────────────────────────────────────── +// Widgets auxiliares +// ────────────────────────────────────────── +class _FcmTopicBadge extends StatelessWidget { + final String? routeId; + const _FcmTopicBadge({required this.routeId}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFF1D9E75), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text.rich( + TextSpan(children: [ + const TextSpan( + text: 'Suscrito a ', + style: TextStyle(fontSize: 12), + ), + TextSpan( + text: routeId != null + ? 'topic_$routeId' + : 'topic pendiente', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const TextSpan( + text: ' · Solo recibes eventos de tu ruta', + style: TextStyle(fontSize: 12), + ), + ]), + ), + ), + ], + ), + ); + } +} + +class _PrivacyNote extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFAEEDA), // amber-50 + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFFAC775)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.info_outline_rounded, + size: 18, color: Color(0xFFBA7517)), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Los mensajes no revelan la ubicación del camión. Solo se muestra el tiempo estimado de llegada.', + style: const TextStyle(fontSize: 12, color: Color(0xFF633806)), + maxLines: 3, + ), + ), + ], + ), + ); + } +} + +class _SectionLabel extends StatelessWidget { + final String label; + const _SectionLabel({required this.label}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + label.toUpperCase(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.8, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ); + } +} + +class _NotificationCard extends StatelessWidget { + final NotificationItem item; + const _NotificationCard({required this.item}); + + IconData get _icon { + switch (item.type) { + case FcmEventType.routeStart: + return Icons.arrow_forward_rounded; + case FcmEventType.truckProximity: + return Icons.local_shipping_rounded; + case FcmEventType.routeCompleted: + return Icons.check_circle_outline_rounded; + case FcmEventType.reassignment: + return Icons.swap_horiz_rounded; + default: + return Icons.notifications_outlined; + } + } + + Color _accentColor() { + switch (item.type) { + case FcmEventType.routeStart: + return const Color(0xFF1D9E75); + case FcmEventType.truckProximity: + return const Color(0xFFBA7517); + case FcmEventType.routeCompleted: + return Colors.grey; + case FcmEventType.reassignment: + return const Color(0xFF378ADD); + default: + return Colors.grey; + } + } + + String _relativeTime() { + final diff = DateTime.now().difference(item.receivedAt); + if (diff.inMinutes < 1) return 'Ahora mismo'; + if (diff.inMinutes < 60) return 'Hace ${diff.inMinutes} min'; + if (diff.inHours < 24) return 'Hace ${diff.inHours} h'; + return 'Ayer'; + } + + @override + Widget build(BuildContext context) { + final accent = _accentColor(); + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10), + border: Border( + left: BorderSide(color: accent, width: 3), + top: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5), + right: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5), + bottom: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: accent.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(_icon, size: 16, color: accent), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + item.body, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.4, + ), + ), + const SizedBox(height: 4), + Text( + _relativeTime(), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + const _EmptyState(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 48), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.notifications_none_rounded, + size: 48, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'Sin notificaciones aún', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + 'Recibirás un aviso cuando el camión esté cerca.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/views_v2/prevention_banner.dart b/views_v2/prevention_banner.dart new file mode 100644 index 0000000..4edb065 --- /dev/null +++ b/views_v2/prevention_banner.dart @@ -0,0 +1,51 @@ +// lib/shared/widgets/prevention_banner.dart +// Banner de mensajería preventiva — obligatorio en la vista ETA. +// Regla de privacidad #5: textos que desalientan sacar basura fuera de horario +// o perseguir la unidad. + +import 'package:flutter/material.dart'; + +class PreventionBanner extends StatelessWidget { + final String? customMessage; + + const PreventionBanner({super.key, this.customMessage}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFAEEDA), // amber-50 + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFFAC775)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 1), + child: Icon( + Icons.warning_amber_rounded, + size: 18, + color: Color(0xFFBA7517), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + customMessage ?? + 'No saques tu basura antes de recibir el aviso de proximidad ' + 'ni dejes bolsas en la calle por más de 30 min. ' + 'No persigas ni detengas la unidad recolectora.', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF633806), + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/views_v2/progress_steps.dart b/views_v2/progress_steps.dart new file mode 100644 index 0000000..0a95e31 --- /dev/null +++ b/views_v2/progress_steps.dart @@ -0,0 +1,174 @@ +// lib/shared/widgets/progress_steps.dart +// Barra de 4 pasos del servicio. Sin mapa ni coordenadas. +// Los pasos mapean a los eventos de positionId del backend: +// 0 = pendiente, 1 = ROUTE_START (pos 2), 2 = TRUCK_PROXIMITY (pos 4), 3 = ROUTE_COMPLETED (pos 8) + +import 'package:flutter/material.dart'; + +class ProgressSteps extends StatelessWidget { + /// 0 = pendiente, 1 = en camino, 2 = cerca, 3 = completado + final int stepIndex; + + const ProgressSteps({super.key, required this.stepIndex}); + + static const _steps = [ + _StepData('Servicio pendiente', Icons.access_time_rounded), + _StepData('Salió al sector', Icons.arrow_forward_rounded), + _StepData('Cerca (~15 min)', Icons.local_shipping_rounded), + _StepData('Finalizado', Icons.check_circle_outline_rounded), + ]; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), + child: Row( + children: [ + const Icon(Icons.route_rounded, + size: 16, color: Color(0xFF1D9E75)), + const SizedBox(width: 6), + Text( + 'Progreso del servicio', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + ), + const Divider(height: 1, thickness: 0.5), + ...List.generate(_steps.length, (i) { + final status = _stepStatus(i); + return _StepRow( + data: _steps[i], + status: status, + isLast: i == _steps.length - 1, + ); + }), + ], + ), + ); + } + + _Status _stepStatus(int i) { + if (i < stepIndex) return _Status.done; + if (i == stepIndex) return _Status.active; + return _Status.pending; + } +} + +enum _Status { done, active, pending } + +class _StepData { + final String label; + final IconData icon; + const _StepData(this.label, this.icon); +} + +class _StepRow extends StatelessWidget { + final _StepData data; + final _Status status; + final bool isLast; + + const _StepRow({ + required this.data, + required this.status, + required this.isLast, + }); + + @override + Widget build(BuildContext context) { + Color iconBg; + Color iconColor; + IconData displayIcon; + + switch (status) { + case _Status.done: + iconBg = const Color(0xFFE1F5EE); + iconColor = const Color(0xFF1D9E75); + displayIcon = Icons.check_rounded; + break; + case _Status.active: + iconBg = const Color(0xFFFAEEDA); + iconColor = const Color(0xFFBA7517); + displayIcon = data.icon; + break; + case _Status.pending: + iconBg = Theme.of(context).colorScheme.surfaceContainerLow; + iconColor = Theme.of(context).colorScheme.onSurfaceVariant; + displayIcon = Icons.radio_button_unchecked_rounded; + break; + } + + return Container( + decoration: BoxDecoration( + border: isLast + ? null + : Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + width: 0.5, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + children: [ + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: iconBg, + shape: BoxShape.circle, + ), + child: Icon(displayIcon, size: 15, color: iconColor), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + data.label, + style: TextStyle( + fontSize: 13, + color: status == _Status.pending + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ), + ), + ), + if (status == _Status.active) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFFAEEDA), + borderRadius: BorderRadius.circular(100), + ), + child: const Text( + 'Ahora', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Color(0xFF633806), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file