Initial commit: SIBU 2.0 MISSION
This commit is contained in:
210
old/lib/widgets/custom_app_bar.dart
Normal file
210
old/lib/widgets/custom_app_bar.dart
Normal file
@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Custom app bar implementing clean, minimal header design
|
||||
/// with contextual actions for the transit app
|
||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final List<Widget>? actions;
|
||||
final Widget? leading;
|
||||
final bool showBackButton;
|
||||
final VoidCallback? onBackPressed;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final double elevation;
|
||||
final bool centerTitle;
|
||||
|
||||
const CustomAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.leading,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.elevation = 0,
|
||||
this.centerTitle = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
|
||||
void _handleBackPress(BuildContext context) {
|
||||
// Haptic feedback for back navigation
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (onBackPressed != null) {
|
||||
onBackPressed!();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLeading(BuildContext context) {
|
||||
if (leading != null) {
|
||||
return leading!;
|
||||
}
|
||||
|
||||
if (showBackButton && Navigator.of(context).canPop()) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new),
|
||||
onPressed: () => _handleBackPress(context),
|
||||
tooltip: 'Atrás',
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
final List<Widget> actionWidgets = [];
|
||||
|
||||
// Add custom actions if provided
|
||||
if (actions != null) {
|
||||
actionWidgets.addAll(actions!);
|
||||
}
|
||||
|
||||
// Add contextual actions based on current route
|
||||
final currentRoute = ModalRoute.of(context)?.settings.name;
|
||||
|
||||
switch (currentRoute) {
|
||||
case '/map-screen':
|
||||
actionWidgets.add(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Handle location centering
|
||||
},
|
||||
tooltip: 'Mi ubicación',
|
||||
),
|
||||
);
|
||||
actionWidgets.add(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Handle filter options
|
||||
},
|
||||
tooltip: 'Filtros',
|
||||
),
|
||||
);
|
||||
break;
|
||||
case '/schedules-screen':
|
||||
actionWidgets.add(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Handle schedule refresh
|
||||
},
|
||||
tooltip: 'Actualizar',
|
||||
),
|
||||
);
|
||||
break;
|
||||
case '/coupons-screen':
|
||||
actionWidgets.add(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Handle QR code scanning
|
||||
},
|
||||
tooltip: 'Escanear',
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return actionWidgets;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AppBar(
|
||||
title: Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: foregroundColor ?? theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
leading: _buildLeading(context),
|
||||
actions: _buildActions(context),
|
||||
backgroundColor: backgroundColor ?? theme.colorScheme.surface,
|
||||
foregroundColor: foregroundColor ?? theme.colorScheme.onSurface,
|
||||
elevation: elevation,
|
||||
shadowColor: theme.shadowColor.withValues(alpha: 0.1),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
centerTitle: centerTitle,
|
||||
systemOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: theme.brightness == Brightness.light
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarBrightness: theme.brightness,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Specialized app bar for map screen with location and filter controls
|
||||
class CustomMapAppBar extends CustomAppBar {
|
||||
const CustomMapAppBar({
|
||||
super.key,
|
||||
super.title = 'Mapa de Rutas',
|
||||
super.showBackButton = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Specialized app bar for schedules screen with refresh functionality
|
||||
class CustomSchedulesAppBar extends CustomAppBar {
|
||||
const CustomSchedulesAppBar({
|
||||
super.key,
|
||||
super.title = 'Horarios',
|
||||
});
|
||||
}
|
||||
|
||||
/// Specialized app bar for coupons screen with QR scanner
|
||||
class CustomCouponsAppBar extends CustomAppBar {
|
||||
const CustomCouponsAppBar({
|
||||
super.key,
|
||||
super.title = 'Cupones',
|
||||
});
|
||||
}
|
||||
|
||||
/// Specialized app bar for bus stop details with contextual actions
|
||||
class CustomBusStopAppBar extends CustomAppBar {
|
||||
final String stopName;
|
||||
|
||||
const CustomBusStopAppBar({
|
||||
super.key,
|
||||
required this.stopName,
|
||||
}) : super(title: stopName);
|
||||
|
||||
@override
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Handle adding to favorites
|
||||
},
|
||||
tooltip: 'Agregar a favoritos',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
// Handle sharing bus stop
|
||||
},
|
||||
tooltip: 'Compartir',
|
||||
),
|
||||
...super._buildActions(context),
|
||||
];
|
||||
}
|
||||
}
|
||||
202
old/lib/widgets/custom_bottom_bar.dart
Normal file
202
old/lib/widgets/custom_bottom_bar.dart
Normal file
@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Custom bottom navigation bar implementing adaptive tab persistence
|
||||
/// with contextual badge indicators for the transit app
|
||||
class CustomBottomBar extends StatefulWidget {
|
||||
final int currentIndex;
|
||||
final Function(int) onTap;
|
||||
|
||||
const CustomBottomBar({
|
||||
super.key,
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomBottomBar> createState() => _CustomBottomBarState();
|
||||
}
|
||||
|
||||
class _CustomBottomBarState extends State<CustomBottomBar> {
|
||||
// Navigation items with routes and icons
|
||||
final List<BottomNavigationBarItem> _navigationItems = [
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.map_outlined),
|
||||
activeIcon: Icon(Icons.map),
|
||||
label: 'Mapa',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.schedule_outlined),
|
||||
activeIcon: Icon(Icons.schedule),
|
||||
label: 'Horarios',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.local_offer_outlined),
|
||||
activeIcon: Icon(Icons.local_offer),
|
||||
label: 'Cupones',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.local_taxi_outlined),
|
||||
activeIcon: Icon(Icons.local_taxi),
|
||||
label: 'Taxi',
|
||||
),
|
||||
];
|
||||
|
||||
final List<String> _routes = [
|
||||
'/map-screen',
|
||||
'/schedules-screen',
|
||||
'/coupons-screen',
|
||||
'/taxi-screen',
|
||||
];
|
||||
|
||||
void _handleTap(int index) {
|
||||
if (index != widget.currentIndex) {
|
||||
// Haptic feedback for tab selection
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
// Navigate to selected route
|
||||
Navigator.pushNamed(context, _routes[index]);
|
||||
|
||||
// Call the onTap callback
|
||||
widget.onTap(index);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildNavigationItem(BottomNavigationBarItem item, int index) {
|
||||
final bool isSelected = index == widget.currentIndex;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => _handleTap(index),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icon with micro-interaction confirmation
|
||||
AnimatedScale(
|
||||
scale: isSelected ? 1.1 : 1.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? theme.colorScheme.secondary.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: _buildIconWithBadge(
|
||||
isSelected ? item.activeIcon : item.icon,
|
||||
index,
|
||||
isSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Label with smooth transition
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: theme.textTheme.labelSmall!.copyWith(
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
child: Text(item.label!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconWithBadge(Widget icon, int index, bool isSelected) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// Smart notification badges - contextual indicators
|
||||
bool showBadge = false;
|
||||
String? badgeText;
|
||||
|
||||
switch (index) {
|
||||
case 0: // Map
|
||||
// Show badge when there are nearby bus updates
|
||||
showBadge = false; // Would be dynamic based on app state
|
||||
break;
|
||||
case 1: // Schedules
|
||||
// Show badge when there are schedule changes
|
||||
showBadge = false; // Would be dynamic based on app state
|
||||
break;
|
||||
case 2: // Coupons
|
||||
// Show badge when there are new coupons available
|
||||
showBadge = true; // Example: new coupons available
|
||||
badgeText = '3';
|
||||
break;
|
||||
case 3: // Taxi
|
||||
// Show badge for new taxi services or favorites
|
||||
showBadge = false; // Would be dynamic based on app state
|
||||
break;
|
||||
}
|
||||
|
||||
if (!showBadge) {
|
||||
return IconTheme(
|
||||
data: IconThemeData(
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
size: 24,
|
||||
),
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
|
||||
return Badge(
|
||||
label: badgeText != null ? Text(badgeText) : null,
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
textColor: theme.colorScheme.onError,
|
||||
smallSize: badgeText == null ? 8 : null,
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
size: 24,
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withValues(alpha: 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
height: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
_navigationItems.length,
|
||||
(index) => _buildNavigationItem(_navigationItems[index], index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
84
old/lib/widgets/custom_error_widget.dart
Normal file
84
old/lib/widgets/custom_error_widget.dart
Normal file
@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../core/app_export.dart';
|
||||
|
||||
// custom_error_widget.dart
|
||||
|
||||
class CustomErrorWidget extends StatelessWidget {
|
||||
final FlutterErrorDetails? errorDetails;
|
||||
final String? errorMessage;
|
||||
|
||||
const CustomErrorWidget({
|
||||
Key? key,
|
||||
this.errorDetails,
|
||||
this.errorMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFFAFAFA),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/images/sad_face.svg',
|
||||
height: 42,
|
||||
width: 42,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Something went wrong",
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF262626),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
child: const Text(
|
||||
'We encountered an unexpected error while processing your request.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF525252), // neutral-600
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
bool canBeBack = Navigator.canPop(context);
|
||||
if (canBeBack) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
Navigator.pushNamed(context, AppRoutes.initial);
|
||||
}
|
||||
},
|
||||
icon:
|
||||
const Icon(Icons.arrow_back, size: 18, color: Colors.white),
|
||||
label: const Text('Back'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.lightTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
2189
old/lib/widgets/custom_icon_widget.dart
Normal file
2189
old/lib/widgets/custom_icon_widget.dart
Normal file
File diff suppressed because it is too large
Load Diff
182
old/lib/widgets/custom_image_widget.dart
Normal file
182
old/lib/widgets/custom_image_widget.dart
Normal file
@ -0,0 +1,182 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../core/app_export.dart';
|
||||
|
||||
extension ImageTypeExtension on String {
|
||||
ImageType get imageType {
|
||||
if (this.startsWith('http') || this.startsWith('https')) {
|
||||
return ImageType.network;
|
||||
} else if (this.endsWith('.svg')) {
|
||||
return ImageType.svg;
|
||||
} else if (this.startsWith('file: //')) {
|
||||
return ImageType.file;
|
||||
} else {
|
||||
return ImageType.png;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ImageType { svg, png, network, file, unknown }
|
||||
|
||||
// ignore_for_file: must_be_immutable
|
||||
class CustomImageWidget extends StatelessWidget {
|
||||
CustomImageWidget({
|
||||
this.imageUrl,
|
||||
this.height,
|
||||
this.width,
|
||||
this.color,
|
||||
this.fit,
|
||||
this.alignment,
|
||||
this.onTap,
|
||||
this.radius,
|
||||
this.margin,
|
||||
this.border,
|
||||
this.placeHolder = 'assets/images/no-image.jpg',
|
||||
this.errorWidget,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
///[imageUrl] is required parameter for showing image
|
||||
final String? imageUrl;
|
||||
|
||||
final double? height;
|
||||
|
||||
final double? width;
|
||||
|
||||
final BoxFit? fit;
|
||||
|
||||
final String placeHolder;
|
||||
|
||||
final Color? color;
|
||||
|
||||
final Alignment? alignment;
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
final BorderRadius? radius;
|
||||
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
final BoxBorder? border;
|
||||
|
||||
/// Optional widget to show when the image fails to load.
|
||||
/// If null, a default asset image is shown.
|
||||
final Widget? errorWidget;
|
||||
|
||||
/// Semantic label for the image to improve accessibility
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return alignment != null
|
||||
? Align(alignment: alignment!, child: _buildWidget())
|
||||
: _buildWidget();
|
||||
}
|
||||
|
||||
Widget _buildWidget() {
|
||||
return Padding(
|
||||
padding: margin ?? EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: _buildCircleImage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///build the image with border radius
|
||||
_buildCircleImage() {
|
||||
if (radius != null) {
|
||||
return ClipRRect(
|
||||
borderRadius: radius ?? BorderRadius.zero,
|
||||
child: _buildImageWithBorder(),
|
||||
);
|
||||
} else {
|
||||
return _buildImageWithBorder();
|
||||
}
|
||||
}
|
||||
|
||||
///build the image with border and border radius style
|
||||
_buildImageWithBorder() {
|
||||
if (border != null) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: border,
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: _buildImageView(),
|
||||
);
|
||||
} else {
|
||||
return _buildImageView();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImageView() {
|
||||
if (imageUrl != null) {
|
||||
switch (imageUrl!.imageType) {
|
||||
case ImageType.svg:
|
||||
return Container(
|
||||
height: height,
|
||||
width: width,
|
||||
child: SvgPicture.asset(
|
||||
imageUrl!,
|
||||
height: height,
|
||||
width: width,
|
||||
fit: fit ?? BoxFit.contain,
|
||||
colorFilter: this.color != null
|
||||
? ColorFilter.mode(
|
||||
this.color ?? Colors.transparent, BlendMode.srcIn)
|
||||
: null,
|
||||
semanticsLabel: semanticLabel,
|
||||
),
|
||||
);
|
||||
case ImageType.file:
|
||||
return Image.file(
|
||||
File(imageUrl!),
|
||||
height: height,
|
||||
width: width,
|
||||
fit: fit ?? BoxFit.cover,
|
||||
color: color,
|
||||
semanticLabel: semanticLabel,
|
||||
);
|
||||
case ImageType.network:
|
||||
return CachedNetworkImage(
|
||||
height: height,
|
||||
width: width,
|
||||
fit: fit,
|
||||
imageUrl: imageUrl!,
|
||||
color: color,
|
||||
placeholder: (context, url) => Container(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: LinearProgressIndicator(
|
||||
color: Colors.grey.shade200,
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
errorWidget ??
|
||||
Image.asset(
|
||||
placeHolder,
|
||||
height: height,
|
||||
width: width,
|
||||
fit: fit ?? BoxFit.cover,
|
||||
semanticLabel: semanticLabel,
|
||||
),
|
||||
);
|
||||
case ImageType.png:
|
||||
default:
|
||||
return Image.asset(
|
||||
imageUrl!,
|
||||
height: height,
|
||||
width: width,
|
||||
fit: fit ?? BoxFit.cover,
|
||||
color: color,
|
||||
semanticLabel: semanticLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
188
old/lib/widgets/debug_banner_widget.dart
Normal file
188
old/lib/widgets/debug_banner_widget.dart
Normal file
@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
|
||||
class DebugBannerWidget extends StatefulWidget {
|
||||
final String? lastError;
|
||||
final int routeCount;
|
||||
final bool isConnected;
|
||||
final String connectionStatus;
|
||||
|
||||
const DebugBannerWidget({
|
||||
super.key,
|
||||
this.lastError,
|
||||
this.routeCount = 0,
|
||||
this.isConnected = false,
|
||||
this.connectionStatus = 'Not checked',
|
||||
});
|
||||
|
||||
@override
|
||||
State<DebugBannerWidget> createState() => _DebugBannerWidgetState();
|
||||
}
|
||||
|
||||
class _DebugBannerWidgetState extends State<DebugBannerWidget> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFEE715), width: 2),
|
||||
),
|
||||
child: _isExpanded ? _buildExpandedView() : _buildCollapsedView(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollapsedView() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bug_report,
|
||||
color: widget.isConnected ? Colors.green : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'DEBUG',
|
||||
style: TextStyle(
|
||||
color: widget.isConnected ? Colors.green : Colors.red,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Color(0xFFFEE715),
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandedView() {
|
||||
final truncatedUrl = SupabaseService.getTruncatedUrl();
|
||||
final maskedKey = SupabaseService.getMaskedAnonKey();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bug_report,
|
||||
color: widget.isConnected ? Colors.green : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'DEBUG INFO',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFEE715),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: Color(0xFFFEE715),
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Color(0xFFFEE715), height: 16, thickness: 1),
|
||||
|
||||
// Connection Status
|
||||
_buildDebugRow(
|
||||
'Status',
|
||||
widget.isConnected ? 'Connected ✓' : 'Connection Failed',
|
||||
isError: !widget.isConnected,
|
||||
),
|
||||
|
||||
// Supabase URL (first/last 8 chars)
|
||||
_buildDebugRow(
|
||||
'SUPABASE_URL',
|
||||
truncatedUrl,
|
||||
),
|
||||
|
||||
// Supabase Anon Key (first/last 6 chars, masked)
|
||||
_buildDebugRow(
|
||||
'SUPABASE_ANON_KEY',
|
||||
maskedKey,
|
||||
),
|
||||
|
||||
// Route Count
|
||||
_buildDebugRow(
|
||||
'Routes Count',
|
||||
widget.isConnected ? '${widget.routeCount} routes' : 'N/A',
|
||||
),
|
||||
|
||||
// Last Error
|
||||
if (widget.lastError != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildDebugRow(
|
||||
'Last Error',
|
||||
widget.lastError!,
|
||||
isError: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDebugRow(String label, String value, {bool isError = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label:',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFEE715),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: isError ? Colors.red : Colors.white,
|
||||
fontSize: 9,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
maxLines: isError ? 3 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
284
old/lib/widgets/route_selection_bottom_sheet.dart
Normal file
284
old/lib/widgets/route_selection_bottom_sheet.dart
Normal file
@ -0,0 +1,284 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/app_state_service.dart';
|
||||
|
||||
class RouteSelectionBottomSheet extends StatefulWidget {
|
||||
final String title;
|
||||
final VoidCallback? onRouteChanged;
|
||||
|
||||
const RouteSelectionBottomSheet({
|
||||
super.key,
|
||||
this.title = 'Seleccionar Ruta',
|
||||
this.onRouteChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RouteSelectionBottomSheet> createState() =>
|
||||
_RouteSelectionBottomSheetState();
|
||||
}
|
||||
|
||||
class _RouteSelectionBottomSheetState extends State<RouteSelectionBottomSheet> {
|
||||
final AppStateService _appStateService = AppStateService();
|
||||
bool _isRefreshing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appStateService.addListener(_onStateChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_appStateService.removeListener(_onStateChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onStateChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRouteSelected(String routeId) async {
|
||||
try {
|
||||
await _appStateService.selectRoute(routeId);
|
||||
|
||||
// Notify parent widget that route changed
|
||||
widget.onRouteChanged?.call();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Ruta cambiada: ${_appStateService.selectedRouteName}'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error seleccionando ruta: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshRoutes() async {
|
||||
setState(() {
|
||||
_isRefreshing = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await _appStateService.refreshRoutes();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Rutas actualizadas'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error actualizando rutas: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isRefreshing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routes = _appStateService.allRoutes;
|
||||
final selectedRouteId = _appStateService.selectedRouteId;
|
||||
final isLoading = _appStateService.isLoadingRoutes || _isRefreshing;
|
||||
final error = _appStateService.error;
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF101820),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _refreshRoutes,
|
||||
icon: Icon(
|
||||
Icons.refresh,
|
||||
color: isLoading ? Colors.grey : const Color(0xFF101820),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Content
|
||||
Flexible(
|
||||
child: isLoading
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Color(0xFFFEE715)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: error != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red[600],
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
error,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.red[600],
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _refreshRoutes,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Reintentar'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFEE715),
|
||||
foregroundColor: const Color(0xFF101820),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: routes.isEmpty
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.route_outlined,
|
||||
color: Colors.grey,
|
||||
size: 48,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No hay rutas disponibles',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: routes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final route = routes[index];
|
||||
final isSelected = selectedRouteId == route.id;
|
||||
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(route.color
|
||||
.replaceFirst('#', '0xFF'))),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
route.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: route.direction.isNotEmpty
|
||||
? Text(
|
||||
route.direction,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: isSelected
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0xFFFEE715),
|
||||
)
|
||||
: null,
|
||||
onTap: () => _onRouteSelected(route.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user