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:
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:recolecta_app/features/admin/admin_shell.dart';
|
||||
import 'package:recolecta_app/features/auth/login_page.dart';
|
||||
import 'package:recolecta_app/features/auth/register_page.dart';
|
||||
import 'package:recolecta_app/features/driver/driver_shell.dart';
|
||||
import 'package:recolecta_app/features/driver/screens/driver_collections_screen.dart';
|
||||
import 'package:recolecta_app/features/driver/screens/driver_home_screen.dart';
|
||||
@@ -47,13 +48,15 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
final isAuthenticated = authState.value?.isAuthenticated ?? false;
|
||||
final role = authState.value?.userRole;
|
||||
|
||||
final isLoggingIn = state.matchedLocation == '/login';
|
||||
final isAuthRoute =
|
||||
state.matchedLocation == '/login' ||
|
||||
state.matchedLocation == '/register';
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return isLoggingIn ? null : '/login';
|
||||
return isAuthRoute ? null : '/login';
|
||||
}
|
||||
|
||||
if (isLoggingIn) {
|
||||
if (isAuthRoute) {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return '/admin';
|
||||
@@ -70,6 +73,10 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
|
||||
GoRoute(
|
||||
path: '/register',
|
||||
builder: (context, state) => const RegisterPage(),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AdminShell(child: child),
|
||||
routes: [
|
||||
|
||||
@@ -133,15 +133,22 @@ class AppInfoRow extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(value,
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textPrimary)),
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(label,
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textSecondary)),
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -163,6 +170,7 @@ class AppFormField extends StatelessWidget {
|
||||
final Widget? suffix;
|
||||
final int? maxLines;
|
||||
final String? Function(String?)? validator;
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
const AppFormField({
|
||||
super.key,
|
||||
@@ -175,6 +183,7 @@ class AppFormField extends StatelessWidget {
|
||||
this.suffix,
|
||||
this.maxLines = 1,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -182,11 +191,14 @@ class AppFormField extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textSecondary)),
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
@@ -195,6 +207,7 @@ class AppFormField extends StatelessWidget {
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary),
|
||||
decoration: InputDecoration(hintText: hint, suffixIcon: suffix),
|
||||
),
|
||||
@@ -253,9 +266,10 @@ class AppLabeledSwitch extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: AppTheme.textPrimary)),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: value,
|
||||
@@ -310,23 +324,33 @@ class AppMenuTile extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: titleColor ?? AppTheme.textPrimary)),
|
||||
color: titleColor ?? AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle!,
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textSecondary)),
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing ??
|
||||
const Icon(Icons.chevron_right,
|
||||
color: AppTheme.textSecondary, size: 18),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
color: AppTheme.textSecondary,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -365,11 +389,14 @@ class AppFormCard extends StatelessWidget {
|
||||
children: [
|
||||
Icon(icon, color: AppTheme.primary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(title,
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary)),
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -10,9 +10,19 @@ import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/app_widgets.dart';
|
||||
import '../../core/services/auth_controller.dart';
|
||||
import '../../core/models/auth_state.dart';
|
||||
import '../addresses/colonias_selector.dart';
|
||||
import '../../core/models/colonia.dart';
|
||||
import '../home/colonias_data.dart';
|
||||
import '../addresses/colonias_provider.dart';
|
||||
|
||||
const Map<String, String> _cpToColonia = {
|
||||
'38000': 'Zona Centro',
|
||||
'38060': 'Las Arboledas',
|
||||
'38027': 'San Juanico',
|
||||
'38037': 'Los Olivos',
|
||||
'38090': 'Rancho Seco',
|
||||
'38080': 'Las Insurgentes',
|
||||
'38086': 'Trojes',
|
||||
};
|
||||
|
||||
class RegisterPage extends ConsumerStatefulWidget {
|
||||
const RegisterPage({super.key});
|
||||
@@ -37,7 +47,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
final _calleCtrl = TextEditingController();
|
||||
Colonia? _selectedColonia;
|
||||
LatLng? _selectedLocation;
|
||||
int _radioAlerta = 200;
|
||||
String _tipoInmueble = 'Casa';
|
||||
bool _whatsappNotif = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -94,6 +105,95 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
setState(() => _currentPage = 1);
|
||||
}
|
||||
|
||||
// Llama a la API de OpenStreetMap (Nominatim) para obtener la calle automáticamente
|
||||
Future<void> _fetchStreetName(LatLng latlng) async {
|
||||
setState(() => _selectedLocation = latlng);
|
||||
|
||||
try {
|
||||
final dio = Dio();
|
||||
final response = await dio.get(
|
||||
'https://nominatim.openstreetmap.org/reverse',
|
||||
queryParameters: {
|
||||
'lat': latlng.latitude,
|
||||
'lon': latlng.longitude,
|
||||
'format': 'json',
|
||||
'addressdetails': 1,
|
||||
},
|
||||
options: Options(headers: {'User-Agent': 'com.onlineshack.recolecta'}),
|
||||
);
|
||||
|
||||
if (response.data != null && response.data['address'] != null) {
|
||||
final address = response.data['address'];
|
||||
final road =
|
||||
address['road'] ?? address['pedestrian'] ?? address['street'] ?? '';
|
||||
final houseNumber = address['house_number'] ?? '';
|
||||
|
||||
if (road.isNotEmpty) {
|
||||
setState(() {
|
||||
_calleCtrl.text = '$road $houseNumber'.trim();
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Aviso: Error al obtener nombre de la calle de OSM: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _validarCP(String cp, List<Colonia> colonias) {
|
||||
if (cp.length != 5) {
|
||||
if (_selectedColonia != null) {
|
||||
setState(() {
|
||||
_selectedColonia = null;
|
||||
_selectedLocation = null;
|
||||
_calleCtrl.clear();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final nombre = _cpToColonia[cp];
|
||||
if (nombre == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Código postal fuera de nuestra zona de servicio actual.',
|
||||
),
|
||||
backgroundColor: AppTheme.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_selectedColonia = null;
|
||||
_selectedLocation = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final backendC = colonias
|
||||
.where((c) => c.nombre.toLowerCase() == nombre.toLowerCase())
|
||||
.firstOrNull;
|
||||
if (backendC == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Esta colonia aún no tiene horarios configurados.'),
|
||||
backgroundColor: AppTheme.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_selectedColonia = null;
|
||||
_selectedLocation = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_selectedColonia = backendC;
|
||||
_selectedLocation = kColoniasCoordinates[nombre];
|
||||
});
|
||||
FocusScope.of(context).unfocus(); // Cierra el teclado
|
||||
}
|
||||
|
||||
Future<void> _register() async {
|
||||
if (_calleCtrl.text.trim().isEmpty || _selectedColonia == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -168,6 +268,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loading = ref.watch(authControllerProvider).isLoading;
|
||||
final coloniasAsync = ref.watch(coloniasProvider);
|
||||
final coloniasList = coloniasAsync.value ?? [];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
@@ -202,18 +304,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
calleCtrl: _calleCtrl,
|
||||
selectedColonia: _selectedColonia,
|
||||
selectedLocation: _selectedLocation,
|
||||
radioAlerta: _radioAlerta,
|
||||
tipoInmueble: _tipoInmueble,
|
||||
whatsappNotif: _whatsappNotif,
|
||||
loading: loading,
|
||||
onColoniaChanged: (c) {
|
||||
setState(() {
|
||||
_selectedColonia = c;
|
||||
if (c != null && kColoniasCoordinates.containsKey(c.nombre)) {
|
||||
_selectedLocation = kColoniasCoordinates[c.nombre];
|
||||
}
|
||||
});
|
||||
},
|
||||
onLocationChanged: (l) => setState(() => _selectedLocation = l),
|
||||
onRadioChanged: (v) => setState(() => _radioAlerta = v),
|
||||
onTipoChanged: (v) => setState(() => _tipoInmueble = v),
|
||||
onCPChanged: (v) => _validarCP(v, coloniasList),
|
||||
onLocationChanged: _fetchStreetName,
|
||||
onWhatsappChanged: (v) =>
|
||||
setState(() => _whatsappNotif = v ?? false),
|
||||
onRegister: _register,
|
||||
),
|
||||
],
|
||||
@@ -386,11 +484,13 @@ class _Step2 extends StatelessWidget {
|
||||
final TextEditingController calleCtrl;
|
||||
final Colonia? selectedColonia;
|
||||
final LatLng? selectedLocation;
|
||||
final int radioAlerta;
|
||||
final String tipoInmueble;
|
||||
final bool whatsappNotif;
|
||||
final bool loading;
|
||||
final ValueChanged<Colonia?> onColoniaChanged;
|
||||
final ValueChanged<String> onTipoChanged;
|
||||
final ValueChanged<String> onCPChanged;
|
||||
final ValueChanged<LatLng> onLocationChanged;
|
||||
final ValueChanged<int> onRadioChanged;
|
||||
final ValueChanged<bool?> onWhatsappChanged;
|
||||
final VoidCallback onRegister;
|
||||
|
||||
const _Step2({
|
||||
@@ -398,17 +498,28 @@ class _Step2 extends StatelessWidget {
|
||||
required this.calleCtrl,
|
||||
required this.selectedColonia,
|
||||
required this.selectedLocation,
|
||||
required this.radioAlerta,
|
||||
required this.tipoInmueble,
|
||||
required this.whatsappNotif,
|
||||
required this.loading,
|
||||
required this.onColoniaChanged,
|
||||
required this.onTipoChanged,
|
||||
required this.onCPChanged,
|
||||
required this.onLocationChanged,
|
||||
required this.onRadioChanged,
|
||||
required this.onWhatsappChanged,
|
||||
required this.onRegister,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapCenter = selectedLocation ?? const LatLng(20.5222, -100.8123);
|
||||
|
||||
// Magia de privacidad: Restringir paneo a 1km a la redonda de la colonia
|
||||
final bounds = selectedColonia != null
|
||||
? LatLngBounds(
|
||||
LatLng(mapCenter.latitude - 0.01, mapCenter.longitude - 0.01),
|
||||
LatLng(mapCenter.latitude + 0.01, mapCenter.longitude + 0.01),
|
||||
)
|
||||
: null;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
@@ -420,17 +531,97 @@ class _Step2 extends StatelessWidget {
|
||||
title: 'Dirección de tu casa',
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Tipo de inmueble',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile<String>(
|
||||
title: const Text(
|
||||
'Casa',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
value: 'Casa',
|
||||
groupValue: tipoInmueble,
|
||||
onChanged: (v) => onTipoChanged(v!),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RadioListTile<String>(
|
||||
title: const Text(
|
||||
'Negocio',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
value: 'Negocio',
|
||||
groupValue: tipoInmueble,
|
||||
onChanged: (v) => onTipoChanged(v!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
AppFormField(
|
||||
label: 'Código Postal',
|
||||
hint: 'Ej. 38000',
|
||||
controller: cpCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: onCPChanged,
|
||||
),
|
||||
|
||||
if (selectedColonia != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
ColoniasSelector(
|
||||
labelText: 'Colonia',
|
||||
initialValue: selectedColonia,
|
||||
onChanged: onColoniaChanged,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryLight.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||
border: Border.all(color: AppTheme.primaryMid),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppTheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Colonia: ${selectedColonia!.nombre}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Horario ${selectedColonia!.turno?.toLowerCase() ?? 'asignado'}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
selectedColonia!.horarioEstimado ??
|
||||
'Sin horario especificado',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
AppFormField(
|
||||
@@ -460,6 +651,9 @@ class _Step2 extends StatelessWidget {
|
||||
options: MapOptions(
|
||||
initialCenter: mapCenter,
|
||||
initialZoom: 15.0,
|
||||
cameraConstraint: bounds != null
|
||||
? CameraConstraint.contain(bounds: bounds)
|
||||
: const CameraConstraint.unconstrained(),
|
||||
onTap: (_, latlng) => onLocationChanged(latlng),
|
||||
),
|
||||
children: [
|
||||
@@ -486,18 +680,34 @@ class _Step2 extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 24),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Ingresa un código postal con servicio\npara asignar tu colonia.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Sección OCR (Privacidad por diseño) ──
|
||||
AppFormCard(
|
||||
icon: Icons.notifications_outlined,
|
||||
title: 'Distancia de alerta',
|
||||
icon: Icons.document_scanner_outlined,
|
||||
title: 'Verificación de Domicilio',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Te avisamos cuando el camión esté a esta distancia de tu casa.',
|
||||
'Para prevenir abusos, requerimos validar tu dirección con un recibo (luz o agua). '
|
||||
'Por privacidad, la imagen será borrada inmediatamente después de la lectura.',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSecondary,
|
||||
@@ -505,17 +715,46 @@ class _Step2 extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
...[200, 400, 600].map(
|
||||
(dist) => _RadioOption(
|
||||
value: dist,
|
||||
groupValue: radioAlerta,
|
||||
label: '$dist metros',
|
||||
sublabel: dist == 200
|
||||
? '~2-3 min de anticipación'
|
||||
: dist == 400
|
||||
? '~4-5 min de anticipación'
|
||||
: '~6-8 min de anticipación',
|
||||
onChanged: onRadioChanged,
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(
|
||||
Icons.upload_file,
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
label: const Text(
|
||||
'Escanear recibo (OCR)',
|
||||
style: TextStyle(color: AppTheme.primary),
|
||||
),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Abriendo cámara... (Próximamente)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Sección WhatsApp ──
|
||||
AppFormCard(
|
||||
icon: Icons.chat_outlined,
|
||||
title: 'Notificaciones Externas',
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
activeColor: AppTheme.primary,
|
||||
value: whatsappNotif,
|
||||
onChanged: onWhatsappChanged,
|
||||
title: const Text(
|
||||
'Recibir alertas del camión vía WhatsApp (Próximamente)',
|
||||
style: TextStyle(fontSize: 14, color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,15 +1,314 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class CitizenHomeScreen extends StatelessWidget {
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/models/ui_models.dart';
|
||||
import 'colonias_data.dart';
|
||||
|
||||
class CitizenHomeScreen extends StatefulWidget {
|
||||
const CitizenHomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CitizenHomeScreen> createState() => _CitizenHomeScreenState();
|
||||
}
|
||||
|
||||
class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
|
||||
bool _isLoading = true;
|
||||
List<UIHouseModel> _casas = [];
|
||||
Map<String, String> _etas = {};
|
||||
Map<String, String> _horarios = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
try {
|
||||
const storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'token') ?? '';
|
||||
|
||||
if (token.isEmpty) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: const String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://localhost:8000',
|
||||
),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
),
|
||||
);
|
||||
|
||||
// 1. Obtener horarios de las colonias
|
||||
try {
|
||||
final colRes = await dio.get('/colonias');
|
||||
if (colRes.data is List) {
|
||||
for (var c in colRes.data) {
|
||||
final nombre = c['nombre'] ?? c['colonia'] ?? '';
|
||||
final horario = c['horario_estimado'] ?? c['schedule'] ?? 'Horario no definido';
|
||||
if (nombre.isNotEmpty) {
|
||||
_horarios[nombre] = horario;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
debugPrint('Aviso: No se pudieron cargar los horarios.');
|
||||
}
|
||||
|
||||
// 2. Obtener los domicilios del ciudadano
|
||||
final res = await dio.get('/addresses');
|
||||
List<UIHouseModel> loadedCasas = [];
|
||||
if (res.data is List) {
|
||||
loadedCasas = (res.data as List).map((e) => UIHouseModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
// 3. Obtener ETA (Tiempo Estimado) para cada domicilio
|
||||
Map<String, String> loadedEtas = {};
|
||||
for (var casa in loadedCasas) {
|
||||
try {
|
||||
final etaRes = await dio.get('/eta', queryParameters: {'address_id': casa.id});
|
||||
loadedEtas[casa.id] = etaRes.data['mensaje'] ?? 'Estado desconocido';
|
||||
} catch (e) {
|
||||
loadedEtas[casa.id] = 'Calculando...';
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_casas = loadedCasas;
|
||||
_etas = loadedEtas;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error en CitizenHomeScreen: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Inicio')),
|
||||
body: const Center(
|
||||
child: Text('TODO: Citizen Home Screen - Mostrar tarjeta ETA'),
|
||||
backgroundColor: AppTheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Estado del Servicio'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Actualizar tiempos',
|
||||
onPressed: () {
|
||||
setState(() => _isLoading = true);
|
||||
_loadData();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _casas.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'No tienes domicilios registrados.',
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _casas.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 24),
|
||||
itemBuilder: (context, index) {
|
||||
final casa = _casas[index];
|
||||
final eta = _etas[casa.id] ?? 'Actualizando...';
|
||||
final horario = _horarios[casa.colonia] ?? 'Horario asignado a la ruta';
|
||||
return _HouseEtaCard(casa: casa, etaMsg: eta, horario: horario);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Widget para la Tarjeta de Mapa y ETA ─────────────────────────────────────
|
||||
class _HouseEtaCard extends StatelessWidget {
|
||||
final UIHouseModel casa;
|
||||
final String etaMsg;
|
||||
final String horario;
|
||||
|
||||
const _HouseEtaCard({
|
||||
required this.casa,
|
||||
required this.etaMsg,
|
||||
required this.horario,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final center = kColoniasCoordinates[casa.colonia] ?? const LatLng(20.5222, -100.8123);
|
||||
|
||||
// Restricción del mapa a la colonia (Privacidad por Diseño)
|
||||
final bounds = LatLngBounds(
|
||||
LatLng(center.latitude - 0.01, center.longitude - 0.01),
|
||||
LatLng(center.latitude + 0.01, center.longitude + 0.01),
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||
border: Border.all(color: AppTheme.border),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// ── Mapa Restringido ──
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCameraFit: CameraFit.bounds(bounds: bounds),
|
||||
cameraConstraint: CameraConstraint.contain(bounds: bounds),
|
||||
interactionOptions: const InteractionOptions(
|
||||
flags: InteractiveFlag.drag | InteractiveFlag.pinchZoom,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.onlineshack.recolecta',
|
||||
),
|
||||
CircleLayer(
|
||||
circles: [
|
||||
CircleMarker(
|
||||
point: center,
|
||||
color: AppTheme.primary.withValues(alpha: 0.15),
|
||||
borderColor: AppTheme.primary,
|
||||
borderStrokeWidth: 2,
|
||||
radius: 400,
|
||||
useRadiusInMeter: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Recuadro de Información ──
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.home, color: AppTheme.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
casa.alias.isNotEmpty ? casa.alias : 'Mi Domicilio',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_InfoRow(icon: Icons.location_on_outlined, title: 'Dirección', value: casa.direccionCompleta),
|
||||
const SizedBox(height: 12),
|
||||
_InfoRow(icon: Icons.schedule_outlined, title: 'Horario Habitual', value: horario),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// ── Alerta de ETA en Tiempo Real ──
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryLight,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||
border: Border.all(color: AppTheme.primaryMid),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.local_shipping_outlined, color: AppTheme.primaryDark),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Estado del Camión',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
etaMsg,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fila auxiliar de info ────────────────────────────────────────────────────
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String value;
|
||||
|
||||
const _InfoRow({required this.icon, required this.title, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: AppTheme.textSecondary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary, height: 1.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
45
views_v1/.gitignore
vendored
Normal file
45
views_v1/.gitignore
vendored
Normal 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
30
views_v1/.metadata
Normal 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
3
views_v1/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# rutaverde
|
||||
|
||||
A new Flutter project.
|
||||
1
views_v1/analysis_options.yaml
Normal file
1
views_v1/analysis_options.yaml
Normal file
@@ -0,0 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
770
views_v1/pubspec.lock
Normal file
770
views_v1/pubspec.lock
Normal 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
33
views_v1/pubspec.yaml
Normal 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
BIN
views_v1/web/favicon.png
Normal file
Binary file not shown.
BIN
views_v1/web/icons/Icon-192.png
Normal file
BIN
views_v1/web/icons/Icon-192.png
Normal file
Binary file not shown.
BIN
views_v1/web/icons/Icon-512.png
Normal file
BIN
views_v1/web/icons/Icon-512.png
Normal file
Binary file not shown.
BIN
views_v1/web/icons/Icon-maskable-192.png
Normal file
BIN
views_v1/web/icons/Icon-maskable-192.png
Normal file
Binary file not shown.
BIN
views_v1/web/icons/Icon-maskable-512.png
Normal file
BIN
views_v1/web/icons/Icon-maskable-512.png
Normal file
Binary file not shown.
46
views_v1/web/index.html
Normal file
46
views_v1/web/index.html
Normal 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>
|
||||
35
views_v1/web/manifest.json
Normal file
35
views_v1/web/manifest.json
Normal 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
17
views_v1/windows/.gitignore
vendored
Normal 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/
|
||||
108
views_v1/windows/CMakeLists.txt
Normal file
108
views_v1/windows/CMakeLists.txt
Normal 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)
|
||||
109
views_v1/windows/flutter/CMakeLists.txt
Normal file
109
views_v1/windows/flutter/CMakeLists.txt
Normal 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}
|
||||
)
|
||||
20
views_v1/windows/flutter/generated_plugin_registrant.cc
Normal file
20
views_v1/windows/flutter/generated_plugin_registrant.cc
Normal 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"));
|
||||
}
|
||||
15
views_v1/windows/flutter/generated_plugin_registrant.h
Normal file
15
views_v1/windows/flutter/generated_plugin_registrant.h
Normal 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_
|
||||
26
views_v1/windows/flutter/generated_plugins.cmake
Normal file
26
views_v1/windows/flutter/generated_plugins.cmake
Normal 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)
|
||||
40
views_v1/windows/runner/CMakeLists.txt
Normal file
40
views_v1/windows/runner/CMakeLists.txt
Normal 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)
|
||||
121
views_v1/windows/runner/Runner.rc
Normal file
121
views_v1/windows/runner/Runner.rc
Normal 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
|
||||
71
views_v1/windows/runner/flutter_window.cpp
Normal file
71
views_v1/windows/runner/flutter_window.cpp
Normal 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);
|
||||
}
|
||||
33
views_v1/windows/runner/flutter_window.h
Normal file
33
views_v1/windows/runner/flutter_window.h
Normal 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_
|
||||
43
views_v1/windows/runner/main.cpp
Normal file
43
views_v1/windows/runner/main.cpp
Normal 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;
|
||||
}
|
||||
16
views_v1/windows/runner/resource.h
Normal file
16
views_v1/windows/runner/resource.h
Normal 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
|
||||
BIN
views_v1/windows/runner/resources/app_icon.ico
Normal file
BIN
views_v1/windows/runner/resources/app_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
14
views_v1/windows/runner/runner.exe.manifest
Normal file
14
views_v1/windows/runner/runner.exe.manifest
Normal 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>
|
||||
69
views_v1/windows/runner/utils.cpp
Normal file
69
views_v1/windows/runner/utils.cpp
Normal 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;
|
||||
}
|
||||
19
views_v1/windows/runner/utils.h
Normal file
19
views_v1/windows/runner/utils.h
Normal 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_
|
||||
288
views_v1/windows/runner/win32_window.cpp
Normal file
288
views_v1/windows/runner/win32_window.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
102
views_v1/windows/runner/win32_window.h
Normal file
102
views_v1/windows/runner/win32_window.h
Normal 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
144
views_v2/app.dart
Normal 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
53
views_v2/dio_client.dart
Normal 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());
|
||||
39
views_v2/feedback_model.dart
Normal file
39
views_v2/feedback_model.dart
Normal 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_*
|
||||
};
|
||||
}
|
||||
104
views_v2/feedback_provider.dart
Normal file
104
views_v2/feedback_provider.dart
Normal 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,
|
||||
);
|
||||
354
views_v2/feedback_screen.dart
Normal file
354
views_v2/feedback_screen.dart
Normal 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
25
views_v2/main.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
378
views_v2/notifications_screen.dart
Normal file
378
views_v2/notifications_screen.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
51
views_v2/prevention_banner.dart
Normal file
51
views_v2/prevention_banner.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
174
views_v2/progress_steps.dart
Normal file
174
views_v2/progress_steps.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user