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:go_router/go_router.dart';
import 'package:recolecta_app/features/admin/admin_shell.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/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/driver_shell.dart';
import 'package:recolecta_app/features/driver/screens/driver_collections_screen.dart'; import 'package:recolecta_app/features/driver/screens/driver_collections_screen.dart';
import 'package:recolecta_app/features/driver/screens/driver_home_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 isAuthenticated = authState.value?.isAuthenticated ?? false;
final role = authState.value?.userRole; final role = authState.value?.userRole;
final isLoggingIn = state.matchedLocation == '/login'; final isAuthRoute =
state.matchedLocation == '/login' ||
state.matchedLocation == '/register';
if (!isAuthenticated) { if (!isAuthenticated) {
return isLoggingIn ? null : '/login'; return isAuthRoute ? null : '/login';
} }
if (isLoggingIn) { if (isAuthRoute) {
switch (role) { switch (role) {
case 'admin': case 'admin':
return '/admin'; return '/admin';
@@ -70,6 +73,10 @@ final routerProvider = Provider<GoRouter>((ref) {
}, },
routes: [ routes: [
GoRoute(path: '/login', builder: (context, state) => const LoginPage()), GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
GoRoute(
path: '/register',
builder: (context, state) => const RegisterPage(),
),
ShellRoute( ShellRoute(
builder: (context, state, child) => AdminShell(child: child), builder: (context, state, child) => AdminShell(child: child),
routes: [ routes: [

View File

@@ -15,28 +15,28 @@ class AppStatusBadge extends StatelessWidget {
}); });
factory AppStatusBadge.green(String label) => AppStatusBadge( factory AppStatusBadge.green(String label) => AppStatusBadge(
label: label, label: label,
backgroundColor: AppTheme.primaryLight, backgroundColor: AppTheme.primaryLight,
textColor: AppTheme.primaryDark, textColor: AppTheme.primaryDark,
); );
factory AppStatusBadge.amber(String label) => AppStatusBadge( factory AppStatusBadge.amber(String label) => AppStatusBadge(
label: label, label: label,
backgroundColor: AppTheme.amberLight, backgroundColor: AppTheme.amberLight,
textColor: AppTheme.amber, textColor: AppTheme.amber,
); );
factory AppStatusBadge.gray(String label) => AppStatusBadge( factory AppStatusBadge.gray(String label) => AppStatusBadge(
label: label, label: label,
backgroundColor: const Color(0xFFF1EFE8), backgroundColor: const Color(0xFFF1EFE8),
textColor: const Color(0xFF5F5E5A), textColor: const Color(0xFF5F5E5A),
); );
factory AppStatusBadge.danger(String label) => AppStatusBadge( factory AppStatusBadge.danger(String label) => AppStatusBadge(
label: label, label: label,
backgroundColor: AppTheme.dangerLight, backgroundColor: AppTheme.dangerLight,
textColor: AppTheme.danger, textColor: AppTheme.danger,
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -133,15 +133,22 @@ class AppInfoRow extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(value, Text(
style: const TextStyle( value,
fontSize: 14, style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 14,
color: AppTheme.textPrimary)), fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2), const SizedBox(height: 2),
Text(label, Text(
style: const TextStyle( label,
fontSize: 12, color: AppTheme.textSecondary)), style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
], ],
), ),
), ),
@@ -163,6 +170,7 @@ class AppFormField extends StatelessWidget {
final Widget? suffix; final Widget? suffix;
final int? maxLines; final int? maxLines;
final String? Function(String?)? validator; final String? Function(String?)? validator;
final ValueChanged<String>? onChanged;
const AppFormField({ const AppFormField({
super.key, super.key,
@@ -175,6 +183,7 @@ class AppFormField extends StatelessWidget {
this.suffix, this.suffix,
this.maxLines = 1, this.maxLines = 1,
this.validator, this.validator,
this.onChanged,
}); });
@override @override
@@ -182,11 +191,14 @@ class AppFormField extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, Text(
style: const TextStyle( label,
fontSize: 12, style: const TextStyle(
fontWeight: FontWeight.w500, fontSize: 12,
color: AppTheme.textSecondary)), fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 6), const SizedBox(height: 6),
TextFormField( TextFormField(
controller: controller, controller: controller,
@@ -195,6 +207,7 @@ class AppFormField extends StatelessWidget {
keyboardType: keyboardType, keyboardType: keyboardType,
maxLines: maxLines, maxLines: maxLines,
validator: validator, validator: validator,
onChanged: onChanged,
style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary), style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary),
decoration: InputDecoration(hintText: hint, suffixIcon: suffix), decoration: InputDecoration(hintText: hint, suffixIcon: suffix),
), ),
@@ -253,9 +266,10 @@ class AppLabeledSwitch extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Text(label, child: Text(
style: const TextStyle( label,
fontSize: 14, color: AppTheme.textPrimary)), style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary),
),
), ),
Switch.adaptive( Switch.adaptive(
value: value, value: value,
@@ -310,23 +324,33 @@ class AppMenuTile extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, Text(
style: TextStyle( title,
fontSize: 14, style: TextStyle(
fontWeight: FontWeight.w500, fontSize: 14,
color: titleColor ?? AppTheme.textPrimary)), fontWeight: FontWeight.w500,
color: titleColor ?? AppTheme.textPrimary,
),
),
if (subtitle != null) ...[ if (subtitle != null) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text(subtitle!, Text(
style: const TextStyle( subtitle!,
fontSize: 12, color: AppTheme.textSecondary)), style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
], ],
], ],
), ),
), ),
trailing ?? trailing ??
const Icon(Icons.chevron_right, const Icon(
color: AppTheme.textSecondary, size: 18), Icons.chevron_right,
color: AppTheme.textSecondary,
size: 18,
),
], ],
), ),
), ),
@@ -365,11 +389,14 @@ class AppFormCard extends StatelessWidget {
children: [ children: [
Icon(icon, color: AppTheme.primary, size: 18), Icon(icon, color: AppTheme.primary, size: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(title, Text(
style: const TextStyle( title,
fontSize: 14, style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 14,
color: AppTheme.textPrimary)), fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -10,9 +10,19 @@ import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_widgets.dart'; import '../../core/widgets/app_widgets.dart';
import '../../core/services/auth_controller.dart'; import '../../core/services/auth_controller.dart';
import '../../core/models/auth_state.dart'; import '../../core/models/auth_state.dart';
import '../addresses/colonias_selector.dart';
import '../../core/models/colonia.dart'; import '../../core/models/colonia.dart';
import '../home/colonias_data.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 { class RegisterPage extends ConsumerStatefulWidget {
const RegisterPage({super.key}); const RegisterPage({super.key});
@@ -37,7 +47,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
final _calleCtrl = TextEditingController(); final _calleCtrl = TextEditingController();
Colonia? _selectedColonia; Colonia? _selectedColonia;
LatLng? _selectedLocation; LatLng? _selectedLocation;
int _radioAlerta = 200; String _tipoInmueble = 'Casa';
bool _whatsappNotif = false;
@override @override
void initState() { void initState() {
@@ -94,6 +105,95 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
setState(() => _currentPage = 1); 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 { Future<void> _register() async {
if (_calleCtrl.text.trim().isEmpty || _selectedColonia == null) { if (_calleCtrl.text.trim().isEmpty || _selectedColonia == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -168,6 +268,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loading = ref.watch(authControllerProvider).isLoading; final loading = ref.watch(authControllerProvider).isLoading;
final coloniasAsync = ref.watch(coloniasProvider);
final coloniasList = coloniasAsync.value ?? [];
return Scaffold( return Scaffold(
backgroundColor: AppTheme.background, backgroundColor: AppTheme.background,
@@ -202,18 +304,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
calleCtrl: _calleCtrl, calleCtrl: _calleCtrl,
selectedColonia: _selectedColonia, selectedColonia: _selectedColonia,
selectedLocation: _selectedLocation, selectedLocation: _selectedLocation,
radioAlerta: _radioAlerta, tipoInmueble: _tipoInmueble,
whatsappNotif: _whatsappNotif,
loading: loading, loading: loading,
onColoniaChanged: (c) { onTipoChanged: (v) => setState(() => _tipoInmueble = v),
setState(() { onCPChanged: (v) => _validarCP(v, coloniasList),
_selectedColonia = c; onLocationChanged: _fetchStreetName,
if (c != null && kColoniasCoordinates.containsKey(c.nombre)) { onWhatsappChanged: (v) =>
_selectedLocation = kColoniasCoordinates[c.nombre]; setState(() => _whatsappNotif = v ?? false),
}
});
},
onLocationChanged: (l) => setState(() => _selectedLocation = l),
onRadioChanged: (v) => setState(() => _radioAlerta = v),
onRegister: _register, onRegister: _register,
), ),
], ],
@@ -386,11 +484,13 @@ class _Step2 extends StatelessWidget {
final TextEditingController calleCtrl; final TextEditingController calleCtrl;
final Colonia? selectedColonia; final Colonia? selectedColonia;
final LatLng? selectedLocation; final LatLng? selectedLocation;
final int radioAlerta; final String tipoInmueble;
final bool whatsappNotif;
final bool loading; final bool loading;
final ValueChanged<Colonia?> onColoniaChanged; final ValueChanged<String> onTipoChanged;
final ValueChanged<String> onCPChanged;
final ValueChanged<LatLng> onLocationChanged; final ValueChanged<LatLng> onLocationChanged;
final ValueChanged<int> onRadioChanged; final ValueChanged<bool?> onWhatsappChanged;
final VoidCallback onRegister; final VoidCallback onRegister;
const _Step2({ const _Step2({
@@ -398,17 +498,28 @@ class _Step2 extends StatelessWidget {
required this.calleCtrl, required this.calleCtrl,
required this.selectedColonia, required this.selectedColonia,
required this.selectedLocation, required this.selectedLocation,
required this.radioAlerta, required this.tipoInmueble,
required this.whatsappNotif,
required this.loading, required this.loading,
required this.onColoniaChanged, required this.onTipoChanged,
required this.onCPChanged,
required this.onLocationChanged, required this.onLocationChanged,
required this.onRadioChanged, required this.onWhatsappChanged,
required this.onRegister, required this.onRegister,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mapCenter = selectedLocation ?? const LatLng(20.5222, -100.8123); 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( return SingleChildScrollView(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
@@ -420,84 +531,183 @@ class _Step2 extends StatelessWidget {
title: 'Dirección de tu casa', title: 'Dirección de tu casa',
child: Column( child: Column(
children: [ children: [
AppFormField(
label: 'Código Postal',
hint: 'Ej. 38000',
controller: cpCtrl,
keyboardType: TextInputType.number,
),
const SizedBox(height: 14),
ColoniasSelector(
labelText: 'Colonia',
initialValue: selectedColonia,
onChanged: onColoniaChanged,
),
const SizedBox(height: 14),
AppFormField(
label: 'Calle y número',
hint: 'Av. Insurgentes 245',
controller: calleCtrl,
),
const SizedBox(height: 16),
const Text( const Text(
'Toca el mapa para ubicar tu casa exacta:', 'Tipo de inmueble',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: AppTheme.textSecondary, color: AppTheme.textSecondary,
), ),
), ),
const SizedBox(height: 8), Row(
Container( children: [
height: 200, Expanded(
decoration: BoxDecoration( child: RadioListTile<String>(
borderRadius: BorderRadius.circular(AppTheme.radiusSm), title: const Text(
border: Border.all(color: AppTheme.border), 'Casa',
), style: TextStyle(fontSize: 14),
clipBehavior: Clip.hardEdge, ),
child: FlutterMap( value: 'Casa',
key: ValueKey(selectedColonia?.nombre ?? 'default'), groupValue: tipoInmueble,
options: MapOptions( onChanged: (v) => onTipoChanged(v!),
initialCenter: mapCenter,
initialZoom: 15.0,
onTap: (_, latlng) => onLocationChanged(latlng),
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.onlineshack.recolecta',
), ),
if (selectedLocation != null) ),
MarkerLayer( Expanded(
markers: [ child: RadioListTile<String>(
Marker( title: const Text(
point: selectedLocation!, 'Negocio',
width: 40, style: TextStyle(fontSize: 14),
height: 40, ),
child: const Icon( value: 'Negocio',
Icons.location_on, groupValue: tipoInmueble,
color: AppTheme.danger, onChanged: (v) => onTipoChanged(v!),
size: 40, ),
),
],
),
const SizedBox(height: 8),
AppFormField(
label: 'Código Postal',
hint: 'Ej. 38000',
controller: cpCtrl,
keyboardType: TextInputType.number,
onChanged: onCPChanged,
),
if (selectedColonia != null) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.primaryLight.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
border: Border.all(color: AppTheme.primaryMid),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.check_circle_outline,
color: AppTheme.primary,
size: 18,
),
const SizedBox(width: 8),
Text(
'Colonia: ${selectedColonia!.nombre}',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark,
), ),
), ),
], ],
), ),
], const SizedBox(height: 8),
Text(
'Horario ${selectedColonia!.turno?.toLowerCase() ?? 'asignado'}',
style: const TextStyle(
fontSize: 13,
color: AppTheme.textPrimary,
),
),
Text(
selectedColonia!.horarioEstimado ??
'Sin horario especificado',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
), ),
), const SizedBox(height: 14),
AppFormField(
label: 'Calle y número',
hint: 'Av. Insurgentes 245',
controller: calleCtrl,
),
const SizedBox(height: 16),
const Text(
'Toca el mapa para ubicar tu casa exacta:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
border: Border.all(color: AppTheme.border),
),
clipBehavior: Clip.hardEdge,
child: FlutterMap(
key: ValueKey(selectedColonia?.nombre ?? 'default'),
options: MapOptions(
initialCenter: mapCenter,
initialZoom: 15.0,
cameraConstraint: bounds != null
? CameraConstraint.contain(bounds: bounds)
: const CameraConstraint.unconstrained(),
onTap: (_, latlng) => onLocationChanged(latlng),
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.onlineshack.recolecta',
),
if (selectedLocation != null)
MarkerLayer(
markers: [
Marker(
point: selectedLocation!,
width: 40,
height: 40,
child: const Icon(
Icons.location_on,
color: AppTheme.danger,
size: 40,
),
),
],
),
],
),
),
] else ...[
const SizedBox(height: 24),
const Center(
child: Text(
'Ingresa un código postal con servicio\npara asignar tu colonia.',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 13,
),
),
),
],
], ],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// ── Sección OCR (Privacidad por diseño) ──
AppFormCard( AppFormCard(
icon: Icons.notifications_outlined, icon: Icons.document_scanner_outlined,
title: 'Distancia de alerta', title: 'Verificación de Domicilio',
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( 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( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppTheme.textSecondary, color: AppTheme.textSecondary,
@@ -505,17 +715,46 @@ class _Step2 extends StatelessWidget {
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
...[200, 400, 600].map( SizedBox(
(dist) => _RadioOption( width: double.infinity,
value: dist, child: OutlinedButton.icon(
groupValue: radioAlerta, icon: const Icon(
label: '$dist metros', Icons.upload_file,
sublabel: dist == 200 color: AppTheme.primary,
? '~2-3 min de anticipación' ),
: dist == 400 label: const Text(
? '~4-5 min de anticipación' 'Escanear recibo (OCR)',
: '~6-8 min de anticipación', style: TextStyle(color: AppTheme.primary),
onChanged: onRadioChanged, ),
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: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}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Inicio')), backgroundColor: AppTheme.background,
body: const Center( appBar: AppBar(
child: Text('TODO: Citizen Home Screen - Mostrar tarjeta ETA'), 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),
),
),
),
],
),
),
);
}
}