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 with WidgetsBindingObserver { static const double _dayItemWidth = 44; static const double _dayItemGap = 12; static const double _eventLeftOffset = 52; static const double _defaultHourHeight = 34.0; static const double _minHourHeight = 17.0; static const double _maxHourHeight = 68.0; double _hourHeight = _defaultHourHeight; final Map _activePointers = {}; double? _pinchStartDistance; double _pinchStartHourHeight = _defaultHourHeight; late final CalendarStateManager _calendarManager; late DateTime _selectedDate; late List _monthDates; final ScrollController _dayStripController = ScrollController(); Key _eventsKey = UniqueKey(); List _events = const []; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _calendarManager = sl(); if (widget.resetToToday) { _calendarManager.resetToToday(); } _selectedDate = _calendarManager.selectedDate; _updateMonthDates(); _loadEvents(); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToSelectedDate(); _setupRouteListener(); }); } void _setupRouteListener() { final router = GoRouter.of(context); router.routerDelegate.addListener(_onRouteChange); } void _onRouteChange() { _loadEvents(); } void _updateMonthDates() { _monthDates = monthDatesFor(_selectedDate); } Future _loadEvents() async { final events = await sl().getEventsForDay(_selectedDate); if (!mounted) { return; } setState(() { _events = events; }); } @override void dispose() { try { GoRouter.of(context).routerDelegate.removeListener(_onRouteChange); } catch (_) {} WidgetsBinding.instance.removeObserver(this); _dayStripController.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _loadEvents(); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.todoBg, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (!didPop) { context.go('/home'); } }, child: SafeArea( child: Stack( children: [ Positioned.fill( top: 154, bottom: 84, child: Listener( onPointerDown: _handlePointerDown, onPointerMove: _handlePointerMove, onPointerUp: _handlePointerUp, onPointerCancel: _handlePointerCancel, behavior: HitTestBehavior.translucent, child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only( left: AppSpacing.lg, right: AppSpacing.lg, top: 2, ), child: KeyedSubtree( key: _eventsKey, child: _buildTimelineBoard(), ), ), ), ), ), Positioned(top: 0, left: 0, right: 0, child: _buildHeader()), Positioned(top: 68, left: 0, right: 0, child: _buildWeekStrip()), Positioned( bottom: 0, left: 0, right: 0, child: _buildBottomDock(), ), ], ), ), ), ); } void _goToToday() { final today = DateTime.now(); setState(() { _selectedDate = today; _hourHeight = _defaultHourHeight; }); _calendarManager.setSelectedDate(today); _updateMonthDates(); _scrollToSelectedDate(animate: true); _loadEvents(); } void _handlePointerDown(PointerDownEvent event) { _activePointers[event.pointer] = event.position; if (_activePointers.length == 2) { final pointers = _activePointers.values.toList(growable: false); _pinchStartDistance = (pointers[0] - pointers[1]).distance; _pinchStartHourHeight = _hourHeight; } } void _handlePointerMove(PointerMoveEvent event) { if (!_activePointers.containsKey(event.pointer)) { return; } _activePointers[event.pointer] = event.position; if (_activePointers.length != 2 || _pinchStartDistance == null) { return; } final pointers = _activePointers.values.toList(growable: false); final currentDistance = (pointers[0] - pointers[1]).distance; final startDistance = _pinchStartDistance!; if (startDistance <= 0) { return; } final nextHeight = (_pinchStartHourHeight * (currentDistance / startDistance)).clamp( _minHourHeight, _maxHourHeight, ); if ((nextHeight - _hourHeight).abs() < 0.1) { return; } setState(() { _hourHeight = nextHeight; }); } void _handlePointerUp(PointerUpEvent event) { _activePointers.remove(event.pointer); if (_activePointers.length < 2) { _pinchStartDistance = null; } } void _handlePointerCancel(PointerCancelEvent event) { _activePointers.remove(event.pointer); if (_activePointers.length < 2) { _pinchStartDistance = null; } } 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(), if (!isSameDay(_selectedDate, DateTime.now())) GestureDetector( onTap: _goToToday, child: Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: AppColors.messageBtnWrap, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: AppColors.messageBtnBorder), ), child: const Center( child: Text( '今天', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), ), ), ), if (!isSameDay(_selectedDate, DateTime.now())) const SizedBox(width: 8), GestureDetector( onTap: () => CreateEventSheet.show( context, initialDate: _selectedDate, onSaved: () { setState(() { _eventsKey = UniqueKey(); }); _loadEvents(); }, ), 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); _loadEvents(); }, 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 = _events; 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: _hourHeight, 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'), ); } }