Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,632 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../core/app_export.dart';
import '../../models/route_model.dart';
import '../../services/app_state_service.dart';
import '../../services/transportation_service.dart';
import '../../widgets/custom_app_bar.dart';
import '../../widgets/custom_bottom_bar.dart';
import '../../widgets/route_selection_bottom_sheet.dart';
import './widgets/empty_state_widget.dart';
import './widgets/notification_badge_widget.dart';
import './widgets/route_selection_card.dart';
import './widgets/schedule_card.dart';
import './widgets/search_bar_widget.dart';
class SchedulesScreen extends StatefulWidget {
const SchedulesScreen({super.key});
@override
State<SchedulesScreen> createState() => _SchedulesScreenState();
}
class _SchedulesScreenState extends State<SchedulesScreen>
with TickerProviderStateMixin {
int _currentBottomIndex = 1; // Schedules tab
String _searchQuery = '';
final ScrollController _scrollController = ScrollController();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
// Animation controllers
late AnimationController _fadeAnimationController;
late Animation<double> _fadeAnimation;
// Service and data
final TransportationService _transportationService = TransportationService();
final AppStateService _appStateService = AppStateService();
List<RouteModel> _routes = [];
List<Map<String, dynamic>> _schedules = [];
bool _isLoadingSchedules = false;
String _currentScheduleType = 'weekday';
// Notification state
final Map<String, bool> _notificationStates = {};
int _activeNotifications = 0;
@override
void initState() {
super.initState();
_initializeAnimations();
_appStateService.addListener(_onGlobalStateChanged);
_loadInitialData();
_determineCurrentScheduleType();
}
@override
void dispose() {
_appStateService.removeListener(_onGlobalStateChanged);
_fadeAnimationController.dispose();
_scrollController.dispose();
super.dispose();
}
void _initializeAnimations() {
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
));
}
void _onGlobalStateChanged() {
// React to global route selection changes
if (mounted) {
_loadSchedulesForSelectedRoute();
}
}
void _determineCurrentScheduleType() {
// Use Panama timezone as specified in requirements
final panamaNow =
DateTime.now().toUtc().add(Duration(hours: -5)); // Panama is UTC-5
final dayOfWeek = panamaNow.weekday;
// Monday = 1, Sunday = 7
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
_currentScheduleType = 'weekday';
} else if (dayOfWeek == 6) {
_currentScheduleType = 'saturday';
} else {
_currentScheduleType = 'sunday';
}
}
Future<void> _loadInitialData() async {
// Check if we have routes loaded globally
if (_appStateService.allRoutes.isEmpty &&
!_appStateService.isLoadingRoutes) {
await _appStateService.loadRoutes();
}
// Show toast if no routes found
if (_appStateService.allRoutes.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_appStateService.showNoRoutesToast(context);
});
}
// Update local routes reference
setState(() {
_routes = _appStateService.allRoutes;
});
// Load schedules for currently selected route
await _loadSchedulesForSelectedRoute();
}
Future<void> _loadSchedulesForSelectedRoute() async {
final selectedRouteId = _appStateService.selectedRouteId;
if (selectedRouteId == null) {
setState(() {
_schedules = [];
_isLoadingSchedules = false;
});
return;
}
setState(() {
_isLoadingSchedules = true;
});
try {
final schedules =
await _transportationService.getRouteTimetablesByScheduleType(
selectedRouteId,
_currentScheduleType,
);
setState(() {
_schedules = schedules;
_isLoadingSchedules = false;
});
_fadeAnimationController.forward();
} catch (e) {
setState(() {
_isLoadingSchedules = false;
_schedules = [];
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading schedules: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
List<RouteModel> get _filteredRoutes {
if (_searchQuery.isEmpty) return _routes;
return _routes.where((route) {
final name = route.name.toLowerCase();
final query = _searchQuery.toLowerCase();
return name.contains(query);
}).toList();
}
List<Map<String, dynamic>> get _filteredSchedules {
if (_searchQuery.isEmpty) return _schedules;
return _schedules.where((schedule) {
final departureTime =
(schedule['departure_time'] as String).toLowerCase();
final query = _searchQuery.toLowerCase();
return departureTime.contains(query);
}).toList();
}
void _onBottomNavTap(int index) {
setState(() {
_currentBottomIndex = index;
});
}
Future<void> _onRouteSelected(String routeId) async {
await _appStateService.selectRoute(routeId);
setState(() {
_searchQuery = '';
});
}
void _onSearchChanged(String query) {
setState(() {
_searchQuery = query;
});
}
void _onSearchClear() {
setState(() {
_searchQuery = '';
});
}
void _toggleNotification(String scheduleId) {
setState(() {
final currentState = _notificationStates[scheduleId] ?? false;
_notificationStates[scheduleId] = !currentState;
if (!currentState) {
_activeNotifications++;
} else {
_activeNotifications--;
}
});
}
Future<void> _onRefresh() async {
await _appStateService.refreshRoutes();
setState(() {
_routes = _appStateService.allRoutes;
});
await _loadSchedulesForSelectedRoute();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Horarios actualizados'),
backgroundColor: AppTheme.lightTheme.colorScheme.tertiary,
duration: const Duration(seconds: 2),
),
);
}
}
void _showRouteSelector() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => RouteSelectionBottomSheet(
title: 'Seleccionar Ruta',
onRouteChanged: () {
// The global state listener will handle the reload
},
),
);
}
void _showScheduleContextMenu(String scheduleId, String departureTime) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12.w,
height: 0.5.h,
margin: EdgeInsets.symmetric(vertical: 2.h),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.outline,
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Text(
'Opciones para $departureTime',
style: AppTheme.lightTheme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 2.h),
_buildContextMenuItem(
icon: 'alarm_add',
title: 'Configurar Recordatorio',
onTap: () {
Navigator.pop(context);
_showReminderDialog(scheduleId, departureTime);
},
),
_buildContextMenuItem(
icon: 'share',
title: 'Compartir Horario',
onTap: () {
Navigator.pop(context);
_shareSchedule(departureTime);
},
),
_buildContextMenuItem(
icon: 'report_problem',
title: 'Reportar Problema',
onTap: () {
Navigator.pop(context);
_reportIssue(scheduleId);
},
),
SizedBox(height: 2.h),
],
),
),
),
);
}
Widget _buildContextMenuItem({
required String icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: CustomIconWidget(
iconName: icon,
color: AppTheme.lightTheme.colorScheme.onSurface,
size: 24,
),
title: Text(
title,
style: AppTheme.lightTheme.textTheme.bodyLarge,
),
onTap: onTap,
);
}
void _showReminderDialog(String scheduleId, String departureTime) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Configurar Recordatorio'),
content: Text(
'¿Deseas recibir una notificación antes de la salida de las $departureTime?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancelar'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_toggleNotification(scheduleId);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recordatorio configurado'),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Confirmar'),
),
],
),
);
}
void _shareSchedule(String departureTime) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Compartiendo horario de $departureTime'),
duration: const Duration(seconds: 2),
),
);
}
void _reportIssue(String scheduleId) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Reporte enviado. Gracias por tu feedback.'),
duration: Duration(seconds: 2),
),
);
}
String _getScheduleTypeDisplayName() {
switch (_currentScheduleType) {
case 'weekday':
return 'Lunes a Viernes';
case 'saturday':
return 'Sábado';
case 'sunday':
return 'Domingo';
default:
return 'Horario';
}
}
Widget _buildRouteSelectionView() {
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomIconWidget(
iconName: 'schedule',
color: AppTheme.lightTheme.colorScheme.secondary,
size: 20,
),
SizedBox(width: 2.w),
Text(
'Horarios para ${_getScheduleTypeDisplayName()}',
style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith(
color: AppTheme.lightTheme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
SearchBarWidget(
hintText: 'Buscar rutas...',
onChanged: _onSearchChanged,
onClear: _onSearchClear,
),
Expanded(
child: _appStateService.isLoadingRoutes
? const Center(child: CircularProgressIndicator())
: _filteredRoutes.isEmpty
? const EmptyStateWidget(
title: 'No se encontraron rutas',
subtitle: 'Intenta con otros términos de búsqueda',
)
: ListView.builder(
controller: _scrollController,
itemCount: _filteredRoutes.length,
itemBuilder: (context, index) {
final route = _filteredRoutes[index];
return RouteSelectionCard(
routeName: route.displayName,
duration: route.direction,
onTap: () => _onRouteSelected(route.id),
);
},
),
),
],
);
}
Widget _buildScheduleView() {
final selectedRoute = _appStateService.getSelectedRoute();
final selectedRouteName = _appStateService.selectedRouteName;
if (selectedRoute == null || selectedRouteName == null) {
return const Center(
child: Text('No route selected'),
);
}
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.surface,
border: Border(
bottom: BorderSide(
color: AppTheme.lightTheme.colorScheme.outline,
width: 1,
),
),
),
child: Row(
children: [
GestureDetector(
onTap: () {
_appStateService.clearSelectedRoute();
setState(() {
_searchQuery = '';
_schedules.clear();
});
},
child: Container(
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.secondary
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: CustomIconWidget(
iconName: 'arrow_back',
color: AppTheme.lightTheme.colorScheme.primary,
size: 20,
),
),
),
SizedBox(width: 3.w),
Expanded(
child: GestureDetector(
onTap: _showRouteSelector,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedRouteName,
style:
AppTheme.lightTheme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
Text(
'${_getScheduleTypeDisplayName()}${selectedRoute.direction}',
style:
AppTheme.lightTheme.textTheme.bodySmall?.copyWith(
color:
AppTheme.lightTheme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
],
),
),
SearchBarWidget(
hintText: 'Buscar horarios...',
onChanged: _onSearchChanged,
onClear: _onSearchClear,
),
Expanded(
child: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _onRefresh,
color: AppTheme.lightTheme.colorScheme.secondary,
child: _isLoadingSchedules
? const Center(child: CircularProgressIndicator())
: _filteredSchedules.isEmpty
? const EmptyStateWidget(
title: 'No hay horarios disponibles',
subtitle: 'Intenta actualizar o selecciona otra ruta',
)
: FadeTransition(
opacity: _fadeAnimation,
child: ListView.builder(
controller: _scrollController,
itemCount: _filteredSchedules.length,
itemBuilder: (context, index) {
final schedule = _filteredSchedules[index];
final scheduleId = schedule['id'].toString();
final departureTime =
schedule['departure_time'] as String;
final timeParts = departureTime.split(':');
final hour = timeParts[0];
final minute = timeParts[1];
final formattedTime = '$hour:$minute';
return ScheduleCard(
departureTime: formattedTime,
duration: selectedRoute.direction,
arrivalTime: '',
isNotificationEnabled:
_notificationStates[scheduleId] ?? false,
onNotificationToggle: () =>
_toggleNotification(scheduleId),
onLongPress: () => _showScheduleContextMenu(
scheduleId,
formattedTime,
),
);
},
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final hasSelectedRoute = _appStateService.hasSelectedRoute;
return Scaffold(
backgroundColor: AppTheme.lightTheme.scaffoldBackgroundColor,
appBar: CustomAppBar(
title: 'Horarios',
actions: [
NotificationBadgeWidget(
count: _activeNotifications,
child: IconButton(
icon: CustomIconWidget(
iconName: 'notifications',
color: AppTheme.lightTheme.colorScheme.onSurface,
size: 24,
),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Tienes $_activeNotifications notificaciones activas'),
duration: const Duration(seconds: 2),
),
);
},
),
),
],
),
body: SafeArea(
child: !hasSelectedRoute
? _buildRouteSelectionView()
: _buildScheduleView(),
),
bottomNavigationBar: CustomBottomBar(
currentIndex: _currentBottomIndex,
onTap: _onBottomNavTap,
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
class EmptyStateWidget extends StatelessWidget {
final String title;
final String subtitle;
final String? actionText;
final VoidCallback? onActionPressed;
const EmptyStateWidget({
super.key,
required this.title,
required this.subtitle,
this.actionText,
this.onActionPressed,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 30.w,
height: 30.w,
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.secondary
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(15.w),
),
child: Center(
child: CustomIconWidget(
iconName: 'schedule',
color: AppTheme.lightTheme.colorScheme.secondary,
size: 15.w,
),
),
),
SizedBox(height: 4.h),
Text(
title,
style: AppTheme.lightTheme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: AppTheme.lightTheme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
SizedBox(height: 2.h),
Text(
subtitle,
style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith(
color: AppTheme.lightTheme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
if (actionText != null && onActionPressed != null) ...[
SizedBox(height: 4.h),
ElevatedButton(
onPressed: onActionPressed,
child: Text(actionText!),
),
],
],
),
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
import '../../../theme/app_theme.dart';
class NotificationBadgeWidget extends StatelessWidget {
final int count;
final Widget child;
const NotificationBadgeWidget({
super.key,
required this.count,
required this.child,
});
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
child,
if (count > 0)
Positioned(
right: -1.w,
top: -0.5.h,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: count > 9 ? 1.5.w : 1.w,
vertical: 0.5.h,
),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.error,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppTheme.lightTheme.colorScheme.surface,
width: 2,
),
),
constraints: BoxConstraints(
minWidth: 5.w,
minHeight: 2.5.h,
),
child: Text(
count > 99 ? '99+' : count.toString(),
style: AppTheme.lightTheme.textTheme.labelSmall?.copyWith(
color: AppTheme.lightTheme.colorScheme.onError,
fontWeight: FontWeight.w600,
fontSize: 10.sp,
),
textAlign: TextAlign.center,
),
),
),
],
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
class RouteSelectionCard extends StatelessWidget {
final String routeName;
final String duration;
final VoidCallback onTap;
final bool isSelected;
const RouteSelectionCard({
super.key,
required this.routeName,
required this.duration,
required this.onTap,
this.isSelected = false,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
decoration: BoxDecoration(
color: isSelected
? AppTheme.lightTheme.colorScheme.secondary
.withValues(alpha: 0.1)
: AppTheme.lightTheme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? AppTheme.lightTheme.colorScheme.secondary
: AppTheme.lightTheme.colorScheme.outline,
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: AppTheme.lightTheme.shadowColor.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
routeName,
style:
AppTheme.lightTheme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: isSelected
? AppTheme.lightTheme.colorScheme.primary
: AppTheme.lightTheme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 0.5.h),
Text(
'Duración estimada: $duration',
style:
AppTheme.lightTheme.textTheme.bodySmall?.copyWith(
color:
AppTheme.lightTheme.colorScheme.onSurfaceVariant,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
SizedBox(width: 2.w),
CustomIconWidget(
iconName: 'chevron_right',
color: isSelected
? AppTheme.lightTheme.colorScheme.secondary
: AppTheme.lightTheme.colorScheme.onSurfaceVariant,
size: 24,
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
class ScheduleCard extends StatefulWidget {
final String departureTime;
final String duration;
final String arrivalTime;
final bool isNotificationEnabled;
final VoidCallback onNotificationToggle;
final VoidCallback? onLongPress;
const ScheduleCard({
super.key,
required this.departureTime,
required this.duration,
required this.arrivalTime,
required this.isNotificationEnabled,
required this.onNotificationToggle,
this.onLongPress,
});
@override
State<ScheduleCard> createState() => _ScheduleCardState();
}
class _ScheduleCardState extends State<ScheduleCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _handleTapDown(TapDownDetails details) {
_animationController.forward();
}
void _handleTapUp(TapUpDetails details) {
_animationController.reverse();
}
void _handleTapCancel() {
_animationController.reverse();
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTapCancel: _handleTapCancel,
onLongPress: widget.onLongPress != null
? () {
HapticFeedback.mediumImpact();
widget.onLongPress!();
}
: null,
child: Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.lightTheme.colorScheme.outline,
width: 1,
),
boxShadow: [
BoxShadow(
color: AppTheme.lightTheme.shadowColor
.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Salida',
style: AppTheme
.lightTheme.textTheme.labelSmall
?.copyWith(
color: AppTheme.lightTheme.colorScheme
.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 0.5.h),
Text(
widget.departureTime,
style: AppTheme
.lightTheme.textTheme.titleLarge
?.copyWith(
fontWeight: FontWeight.w600,
color: AppTheme
.lightTheme.colorScheme.onSurface,
),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 2.w, vertical: 0.5.h),
decoration: BoxDecoration(
color: AppTheme
.lightTheme.colorScheme.secondary
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.duration,
style: AppTheme
.lightTheme.textTheme.labelMedium
?.copyWith(
color:
AppTheme.lightTheme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Llegada',
style: AppTheme
.lightTheme.textTheme.labelSmall
?.copyWith(
color: AppTheme.lightTheme.colorScheme
.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 0.5.h),
Text(
widget.arrivalTime,
style: AppTheme
.lightTheme.textTheme.titleLarge
?.copyWith(
fontWeight: FontWeight.w600,
color: AppTheme
.lightTheme.colorScheme.onSurface,
),
),
],
),
),
],
),
],
),
),
SizedBox(width: 3.w),
GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
widget.onNotificationToggle();
},
child: Container(
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: widget.isNotificationEnabled
? AppTheme.lightTheme.colorScheme.secondary
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.isNotificationEnabled
? AppTheme.lightTheme.colorScheme.secondary
: AppTheme.lightTheme.colorScheme.outline,
width: 1,
),
),
child: CustomIconWidget(
iconName: widget.isNotificationEnabled
? 'notifications'
: 'notifications_none',
color: widget.isNotificationEnabled
? AppTheme.lightTheme.colorScheme.onSecondary
: AppTheme
.lightTheme.colorScheme.onSurfaceVariant,
size: 20,
),
),
),
],
),
),
),
);
},
),
);
}
}

View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
class SearchBarWidget extends StatefulWidget {
final String hintText;
final ValueChanged<String> onChanged;
final VoidCallback? onClear;
const SearchBarWidget({
super.key,
required this.hintText,
required this.onChanged,
this.onClear,
});
@override
State<SearchBarWidget> createState() => _SearchBarWidgetState();
}
class _SearchBarWidgetState extends State<SearchBarWidget> {
final TextEditingController _controller = TextEditingController();
bool _hasText = false;
@override
void initState() {
super.initState();
_controller.addListener(_onTextChanged);
}
@override
void dispose() {
_controller.removeListener(_onTextChanged);
_controller.dispose();
super.dispose();
}
void _onTextChanged() {
final hasText = _controller.text.isNotEmpty;
if (hasText != _hasText) {
setState(() {
_hasText = hasText;
});
}
widget.onChanged(_controller.text);
}
void _clearSearch() {
_controller.clear();
widget.onClear?.call();
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
child: TextField(
controller: _controller,
style: AppTheme.lightTheme.textTheme.bodyMedium,
decoration: InputDecoration(
hintText: widget.hintText,
prefixIcon: Padding(
padding: EdgeInsets.all(3.w),
child: CustomIconWidget(
iconName: 'search',
color: AppTheme.lightTheme.colorScheme.onSurfaceVariant,
size: 20,
),
),
suffixIcon: _hasText
? IconButton(
icon: CustomIconWidget(
iconName: 'clear',
color: AppTheme.lightTheme.colorScheme.onSurfaceVariant,
size: 20,
),
onPressed: _clearSearch,
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppTheme.lightTheme.colorScheme.outline,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppTheme.lightTheme.colorScheme.outline,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppTheme.lightTheme.colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: AppTheme.lightTheme.colorScheme.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
),
),
);
}
}