import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../calendar_state_manager.dart'; import '../calendar_time_utils.dart'; import '../widgets/bottom_dock.dart'; import '../widgets/create_event_sheet.dart'; import '../../data/services/mock_calendar_service.dart'; import '../../data/models/schedule_item_model.dart'; class CalendarDayWeekScreen extends StatefulWidget { final DateTime? initialDate; final bool resetToToday; const CalendarDayWeekScreen({ super.key, this.initialDate, this.resetToToday = false, }); @override State createState() => _CalendarDayWeekScreenState(); } class _CalendarDayWeekScreenState extends State { static const double _dayItemWidth = 44; static const double _dayItemGap = 12; static const double _hourHeight = 34; static const double _eventLeftOffset = 52; late final CalendarStateManager _calendarManager; late DateTime _selectedDate; late List _monthDates; final ScrollController _dayStripController = ScrollController(); Key _eventsKey = UniqueKey(); @override void initState() { super.initState(); _calendarManager = sl(); if (widget.resetToToday) { _calendarManager.resetToToday(); } _selectedDate = _calendarManager.selectedDate; _updateMonthDates(); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToSelectedDate(); }); } void _updateMonthDates() { _monthDates = monthDatesFor(_selectedDate); } @override void dispose() { _dayStripController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.todoBg, body: SafeArea( child: Column( children: [ _buildHeader(), Expanded( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only( left: AppSpacing.lg, right: AppSpacing.lg, top: 2, bottom: 104, ), child: Column( children: [ _buildWeekStrip(), const SizedBox(height: 8), KeyedSubtree( key: _eventsKey, child: _buildTimelineBoard(), ), ], ), ), ), ), _buildBottomDock(), ], ), ), ); } Widget _buildHeader() { return SizedBox( height: 68, child: Padding( padding: const EdgeInsets.only(left: 20, right: 20, top: 12, bottom: 8), child: Row( children: [ GestureDetector( onTap: () => context.go('/calendar/month'), child: Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: AppColors.messageBtnWrap, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: AppColors.messageBtnBorder), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( LucideIcons.chevronLeft, size: 16, color: AppColors.slate700, ), const SizedBox(width: 6), Text( '${_selectedDate.year}年${_selectedDate.month}月', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), ], ), ), ), const Spacer(), GestureDetector( onTap: () => CreateEventSheet.show( context, initialDate: _selectedDate, onSaved: () { setState(() { _eventsKey = UniqueKey(); }); }, ), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.blue600, borderRadius: BorderRadius.circular(18), ), child: const Icon( LucideIcons.plus, size: 20, color: Colors.white, ), ), ), ], ), ), ); } Widget _buildWeekStrip() { return SizedBox( height: 86, child: ListView.separated( controller: _dayStripController, scrollDirection: Axis.horizontal, itemCount: _monthDates.length, separatorBuilder: (context, index) => const SizedBox(width: _dayItemGap), itemBuilder: (context, index) { final date = _monthDates[index]; final isSelected = isSameDay(date, _selectedDate); final isWeekend = date.weekday % 7 == 0 || date.weekday % 7 == 6; return GestureDetector( onTap: () { setState(() { _selectedDate = date; }); _calendarManager.setSelectedDate(date); _updateMonthDates(); _scrollToSelectedDate(animate: true); }, child: SizedBox( width: _dayItemWidth, child: _buildDayItem(date, isSelected, isWeekend), ), ); }, ), ); } void _scrollToSelectedDate({bool animate = false}) { if (!_dayStripController.hasClients) { return; } final index = _monthDates.indexWhere( (date) => isSameDay(date, _selectedDate), ); if (index < 0) { return; } final targetCenter = index * (_dayItemWidth + _dayItemGap) + (_dayItemWidth / 2); final viewport = _dayStripController.position.viewportDimension; var offset = targetCenter - (viewport / 2); final max = _dayStripController.position.maxScrollExtent; if (offset < 0) { offset = 0; } if (offset > max) { offset = max; } if (animate) { _dayStripController.animateTo( offset, duration: const Duration(milliseconds: 180), curve: Curves.easeOut, ); return; } _dayStripController.jumpTo(offset); } Widget _buildDayItem(DateTime date, bool isSelected, bool isWeekend) { final dayNames = ['日', '一', '二', '三', '四', '五', '六']; return Column( mainAxisSize: MainAxisSize.min, children: [ Text( dayNames[date.weekday % 7], style: TextStyle( fontSize: 11, color: isWeekend ? AppColors.slate400 : AppColors.slate600, ), ), const SizedBox(height: 2), Text( '${date.day}', style: TextStyle( fontSize: 17, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, color: isSelected ? AppColors.blue600 : (isWeekend ? AppColors.slate400 : AppColors.slate900), ), ), ], ); } Widget _buildTimelineBoard() { final now = DateTime.now(); final showCurrent = shouldShowCurrentMarker(_selectedDate, now); final events = CalendarService().getEventsForDay(_selectedDate); final eventColumns = _calculateEventColumns(events); return SizedBox( child: Stack( clipBehavior: Clip.none, children: [ Column( children: [ for (var hour = 0; hour <= 23; hour++) ...[ _buildTimelineRow(formatHour(hour)), if (showCurrent && now.hour == hour) _buildTimelineRow(formatHm(now), isCurrentTime: true), ], _buildTimelineRow(formatHour(24), isDisabled: true), ], ), ..._buildPositionedEvents(events, eventColumns), ], ), ); } List _calculateEventColumns(List events) { if (events.isEmpty) return []; final columns = List.filled(events.length, -1); final columnHeights = {}; for (var i = 0; i < events.length; i++) { final event = events[i]; final eventStart = event.startAt.hour * 60 + event.startAt.minute; final eventEnd = event.endAt != null ? event.endAt!.hour * 60 + event.endAt!.minute : eventStart + 60; var column = 0; while (true) { final columnEnd = columnHeights[column] ?? 0; if (columnEnd <= eventStart) { columns[i] = column; columnHeights[column] = eventEnd; break; } column++; } } return columns; } List _buildPositionedEvents( List events, List columns, ) { if (events.isEmpty) return []; final maxColumn = columns.reduce((a, b) => a > b ? a : b) + 1; final eventWidgets = []; for (var i = 0; i < events.length; i++) { final event = events[i]; final column = columns[i]; final startMinutes = event.startAt.hour * 60 + event.startAt.minute; final endMinutes = event.endAt != null ? event.endAt!.hour * 60 + event.endAt!.minute : startMinutes + 60; final durationMinutes = endMinutes - startMinutes; final top = (startMinutes / 60) * _hourHeight; final height = (durationMinutes / 60) * _hourHeight; final eventWidth = maxColumn > 1 ? (MediaQuery.of(context).size.width - _eventLeftOffset - 16) / maxColumn : MediaQuery.of(context).size.width - _eventLeftOffset - 16; final left = _eventLeftOffset + column * eventWidth; eventWidgets.add( Positioned( top: top, left: left, right: maxColumn > 1 ? null : 16, width: maxColumn > 1 ? eventWidth - 4 : null, height: height.clamp(24.0, double.infinity), child: Material( color: Colors.transparent, child: InkWell( onTap: () { final path = '/calendar/events/${event.id}'; debugPrint('Navigating to: $path'); context.push(path); }, child: Container( margin: const EdgeInsets.only(right: 4), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _parseColor( event.metadata?.color, ).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), border: Border.all( color: _parseColor(event.metadata?.color), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration( color: _parseColor(event.metadata?.color), shape: BoxShape.circle, ), ), const SizedBox(width: 4), Expanded( child: Text( event.title, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: _parseColor(event.metadata?.color), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ), ), ); } return eventWidgets; } Color _parseColor(String? hex) { if (hex == null || hex.isEmpty) return AppColors.blue600; try { return Color(int.parse(hex.replaceFirst('#', '0xFF'))); } catch (_) { return AppColors.blue600; } } Widget _buildTimelineRow( String time, { bool isCurrentTime = false, bool isDisabled = false, }) { return SizedBox( height: 34, child: Row( children: [ SizedBox( width: 44, child: isCurrentTime ? Container( width: 44, height: 18, decoration: BoxDecoration( color: AppColors.red500, borderRadius: BorderRadius.circular(9), ), child: Center( child: Text( time, style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ) : Text( time, textAlign: TextAlign.right, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: isDisabled ? AppColors.slate300 : AppColors.slate400, ), ), ), const SizedBox(width: 8), Expanded( child: isCurrentTime ? Container( height: 2, decoration: BoxDecoration( color: AppColors.red500, borderRadius: BorderRadius.circular(99), ), ) : Container( height: 1, color: isDisabled ? AppColors.blue50 : AppColors.border, ), ), ], ), ); } Widget _buildBottomDock() { return BottomDock( activeTab: DockTab.calendar, onTodoTap: () { _calendarManager.setViewType(CalendarViewType.day); context.push('/todo'); }, onCalendarTap: () { _calendarManager.setViewType(CalendarViewType.day); context.go('/calendar/month'); }, onHomeTap: () => context.go('/home'), ); } }