Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>

Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>
Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>

vistas de mockup actualizaco
This commit is contained in:
shinra32
2026-05-22 23:50:10 -06:00
parent c91b6e2091
commit fd7b0c132c
44 changed files with 4108 additions and 140 deletions

View File

@@ -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<GoRouter>((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<GoRouter>((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: [

View File

@@ -133,15 +133,22 @@ class AppInfoRow extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(value,
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary)),
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(label,
Text(
label,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
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<String>? 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,
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary)),
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,
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: titleColor ?? AppTheme.textPrimary)),
color: titleColor ?? AppTheme.textPrimary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(subtitle!,
Text(
subtitle!,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
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,
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary)),
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),

View File

@@ -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<String, String> _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<RegisterPage> {
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<RegisterPage> {
setState(() => _currentPage = 1);
}
// Llama a la API de OpenStreetMap (Nominatim) para obtener la calle automáticamente
Future<void> _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<Colonia> 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<void> _register() async {
if (_calleCtrl.text.trim().isEmpty || _selectedColonia == null) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -168,6 +268,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
@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<RegisterPage> {
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<Colonia?> onColoniaChanged;
final ValueChanged<String> onTipoChanged;
final ValueChanged<String> onCPChanged;
final ValueChanged<LatLng> onLocationChanged;
final ValueChanged<int> onRadioChanged;
final ValueChanged<bool?> 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,17 +531,97 @@ class _Step2 extends StatelessWidget {
title: 'Dirección de tu casa',
child: Column(
children: [
const Text(
'Tipo de inmueble',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
Row(
children: [
Expanded(
child: RadioListTile<String>(
title: const Text(
'Casa',
style: TextStyle(fontSize: 14),
),
value: 'Casa',
groupValue: tipoInmueble,
onChanged: (v) => onTipoChanged(v!),
),
),
Expanded(
child: RadioListTile<String>(
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),
ColoniasSelector(
labelText: 'Colonia',
initialValue: selectedColonia,
onChanged: onColoniaChanged,
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(
@@ -460,6 +651,9 @@ class _Step2 extends StatelessWidget {
options: MapOptions(
initialCenter: mapCenter,
initialZoom: 15.0,
cameraConstraint: bounds != null
? CameraConstraint.contain(bounds: bounds)
: const CameraConstraint.unconstrained(),
onTap: (_, latlng) => onLocationChanged(latlng),
),
children: [
@@ -486,18 +680,34 @@ class _Step2 extends StatelessWidget {
],
),
),
] 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),
),
),
],

View File

@@ -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<CitizenHomeScreen> createState() => _CitizenHomeScreenState();
}
class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
bool _isLoading = true;
List<UIHouseModel> _casas = [];
Map<String, String> _etas = {};
Map<String, String> _horarios = {};
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _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<UIHouseModel> 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<String, String> 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),
),
],
),
),
],
);
}
}

45
views_v1/.gitignore vendored Normal file
View File

@@ -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

30
views_v1/.metadata Normal file
View File

@@ -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'

3
views_v1/README.md Normal file
View File

@@ -0,0 +1,3 @@
# rutaverde
A new Flutter project.

View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

770
views_v1/pubspec.lock Normal file
View File

@@ -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"

33
views_v1/pubspec.yaml Normal file
View File

@@ -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/

BIN
views_v1/web/favicon.png Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

46
views_v1/web/index.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="rutaverde">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>rutaverde</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
or to give the user feedback during the initialization process.
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@@ -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"
}
]
}

17
views_v1/windows/.gitignore vendored Normal file
View File

@@ -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/

View File

@@ -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 "$<$<CONFIG:Debug>:_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 "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# 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)

View File

@@ -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} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View File

@@ -0,0 +1,20 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -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 $<TARGET_FILE:${plugin}_plugin>)
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)

View File

@@ -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)

View File

@@ -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

View File

@@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#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<flutter::FlutterViewController>(
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<LRESULT> 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);
}

View File

@@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#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::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View File

@@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#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<std::string> 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;
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,69 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
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<std::string> 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::string>();
}
std::vector<std::string> 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<int>(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<size_t>(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;
}

View File

@@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// 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<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View File

@@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#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<int>(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<EnableNonClientDpiScaling*>(
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<LONG>(origin.x),
static_cast<LONG>(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<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(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<RECT*>(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<Win32Window*>(
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));
}
}

View File

@@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// 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_

144
views_v2/app.dart Normal file
View File

@@ -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});
}

53
views_v2/dio_client.dart Normal file
View File

@@ -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<Dio>((ref) => _buildDio());

View File

@@ -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<String, dynamic> 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_*
};
}

View File

@@ -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<void> submit(FeedbackRequest req) async {
await _dio.post<void>('/feedback', data: req.toJson());
}
}
final feedbackServiceProvider = Provider<FeedbackService>(
(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<FeedbackFormState> {
@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<void> 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, FeedbackFormState>(
FeedbackNotifier.new,
);

View File

@@ -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<String>((ref) => '101');
class FeedbackScreen extends ConsumerStatefulWidget {
const FeedbackScreen({super.key});
@override
ConsumerState<FeedbackScreen> createState() => _FeedbackScreenState();
}
class _FeedbackScreenState extends ConsumerState<FeedbackScreen> {
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<FeedbackType> 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<int> 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,
),
);
}
}

25
views_v2/main.dart Normal file
View File

@@ -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(),
),
);
}

View File

@@ -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, List<NotificationItem>>(
NotificationsNotifier.new,
);
class NotificationsNotifier extends Notifier<List<NotificationItem>> {
@override
List<NotificationItem> 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,
),
],
),
);
}
}

View File

@@ -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,
),
),
),
],
),
);
}
}

View File

@@ -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),
),
),
),
],
),
),
);
}
}