287 lines
8.0 KiB
Dart
287 lines
8.0 KiB
Dart
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../core/network/api_client.dart';
|
|
import '../core/services/auth_controller.dart';
|
|
import '../core/storage/secure_storage.dart';
|
|
import 'bootstrap.dart' as bootstrap;
|
|
import '../features/auth/login_page.dart';
|
|
import '../features/auth/register_page.dart';
|
|
import '../features/addresses/new_address_page.dart';
|
|
|
|
final routerProvider = Provider<GoRouter>((ref) {
|
|
final authSnapshot = ref.watch(authControllerProvider);
|
|
final isAuthenticated = authSnapshot.asData?.value.isAuthenticated ?? false;
|
|
|
|
return GoRouter(
|
|
initialLocation: '/home',
|
|
redirect: (context, state) {
|
|
final location = state.matchedLocation;
|
|
final isAuthRoute = location == '/login' || location == '/register';
|
|
|
|
if (authSnapshot.isLoading) {
|
|
return location == '/login' ? null : '/login';
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return isAuthRoute ? null : '/login';
|
|
}
|
|
|
|
if (isAuthenticated && isAuthRoute) {
|
|
return '/home';
|
|
}
|
|
|
|
return null;
|
|
},
|
|
routes: <RouteBase>[
|
|
GoRoute(
|
|
path: '/login',
|
|
name: 'login',
|
|
builder: (context, state) => const LoginPage(),
|
|
),
|
|
GoRoute(
|
|
path: '/register',
|
|
name: 'register',
|
|
builder: (context, state) => const RegisterPage(),
|
|
),
|
|
GoRoute(
|
|
path: '/home',
|
|
name: 'home',
|
|
builder: (context, state) => const HomePage(),
|
|
),
|
|
GoRoute(
|
|
path: '/addresses/new',
|
|
name: 'addresses-new',
|
|
builder: (context, state) => const NewAddressPage(),
|
|
),
|
|
GoRoute(
|
|
path: '/status',
|
|
name: 'status',
|
|
builder: (context, state) => const StatusPage(),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
class RecolectaApp extends ConsumerWidget {
|
|
const RecolectaApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final bootstrapState = ref.watch(bootstrap.bootstrapProvider);
|
|
|
|
return bootstrapState.when(
|
|
loading: () => const MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
home: BootstrapLoadingPage(),
|
|
),
|
|
error: (error, stackTrace) => MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
home: BootstrapErrorPage(error: error),
|
|
),
|
|
data: (_) => MaterialApp.router(
|
|
debugShowCheckedModeBanner: false,
|
|
title: 'Recolecta',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6F78)),
|
|
scaffoldBackgroundColor: const Color(0xFFF4F7F6),
|
|
useMaterial3: true,
|
|
),
|
|
routerConfig: ref.watch(routerProvider),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class BootstrapLoadingPage extends StatelessWidget {
|
|
const BootstrapLoadingPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
}
|
|
}
|
|
|
|
class BootstrapErrorPage extends StatelessWidget {
|
|
const BootstrapErrorPage({super.key, required this.error});
|
|
|
|
final Object error;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error_outline, size: 48),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'No se pudo cargar la configuración inicial.',
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
error.toString(),
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomePage extends ConsumerWidget {
|
|
const HomePage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final dio = ref.read(apiClientProvider);
|
|
final storage = ref.read(secureStorageProvider);
|
|
final baseUrl = dio.options.baseUrl;
|
|
final authState = ref.watch(authControllerProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Recolecta'),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: authState.isLoading
|
|
? null
|
|
: () async {
|
|
await ref.read(authControllerProvider.notifier).logout();
|
|
if (context.mounted) {
|
|
context.go('/login');
|
|
}
|
|
},
|
|
icon: const Icon(Icons.logout),
|
|
tooltip: 'Salir',
|
|
),
|
|
IconButton(
|
|
onPressed: () => context.goNamed('status'),
|
|
icon: const Icon(Icons.route),
|
|
tooltip: 'Estado',
|
|
),
|
|
],
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: ListView(
|
|
children: [
|
|
const Text(
|
|
'Bootstrap listo',
|
|
style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'La app ya carga .env, Riverpod y GoRouter para la base del MVP.',
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextButton(
|
|
onPressed: () => context.go('/addresses/new'),
|
|
child: const Text('Agregar domicilio'),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_InfoCard(title: 'API base URL', value: baseUrl, icon: Icons.cloud),
|
|
const SizedBox(height: 16),
|
|
_InfoCard(
|
|
title: 'Secure Storage',
|
|
value: storage.runtimeType.toString(),
|
|
icon: Icons.lock,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_ImageCard(
|
|
title: 'Widget listo para caché de imágenes',
|
|
imageUrl:
|
|
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=800&q=80',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class StatusPage extends StatelessWidget {
|
|
const StatusPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Estado')),
|
|
body: const Padding(
|
|
padding: EdgeInsets.all(24),
|
|
child: Text(
|
|
'Aquí después se conectarán ETA, notificaciones, ruta asignada y métricas de privacidad.',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _InfoCard extends StatelessWidget {
|
|
const _InfoCard({
|
|
required this.title,
|
|
required this.value,
|
|
required this.icon,
|
|
});
|
|
|
|
final String title;
|
|
final String value;
|
|
final IconData icon;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
elevation: 0,
|
|
child: ListTile(
|
|
leading: Icon(icon),
|
|
title: Text(title),
|
|
subtitle: Text(value),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ImageCard extends StatelessWidget {
|
|
const _ImageCard({required this.title, required this.imageUrl});
|
|
|
|
final String title;
|
|
final String imageUrl;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
CachedNetworkImage(
|
|
imageUrl: imageUrl,
|
|
height: 180,
|
|
width: double.infinity,
|
|
fit: BoxFit.cover,
|
|
placeholder: (context, url) => const SizedBox(
|
|
height: 180,
|
|
child: Center(child: CircularProgressIndicator()),
|
|
),
|
|
errorWidget: (context, url, error) => const SizedBox(
|
|
height: 180,
|
|
child: Center(child: Icon(Icons.image_not_supported)),
|
|
),
|
|
),
|
|
Padding(padding: const EdgeInsets.all(16), child: Text(title)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|