import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/notifications/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_selection_sheet.dart'; import '../../../../shared/widgets/app_sheet_input_field.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import 'date_time_picker_sheet.dart'; import '../../data/models/schedule_item_model.dart'; import '../../data/services/calendar_service.dart'; class CreateEventSheet extends StatefulWidget { final DateTime? initialDate; final ScheduleItemModel? editingEvent; final VoidCallback? onSaved; final bool pageMode; const CreateEventSheet({ super.key, this.initialDate, this.editingEvent, this.onSaved, this.pageMode = false, }); static Future show( BuildContext context, { DateTime? initialDate, VoidCallback? onSaved, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => CreateEventSheet(initialDate: initialDate, onSaved: onSaved), ); } static Future edit( BuildContext context, ScheduleItemModel event, { VoidCallback? onSaved, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => CreateEventSheet(editingEvent: event, onSaved: onSaved), ); } @override State createState() => _CreateEventSheetState(); } class _CreateEventSheetState extends State with SingleTickerProviderStateMixin { static const List _defaultReminderOptions = [ null, 0, 5, 10, 15, 30, 60, 120, ]; late TabController _tabController; final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _locationController = TextEditingController(); final _notesController = TextEditingController(); late DateTime _startDate; late DateTime _startTime; DateTime? _endDate; DateTime? _endTime; String _selectedColor = '#3B82F6'; int? _reminderMinutes = 15; bool _saving = false; bool get _isEditing => widget.editingEvent != null; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); if (_isEditing) { final event = widget.editingEvent!; _titleController.text = event.title; _descriptionController.text = event.description ?? ''; _locationController.text = event.metadata?.location ?? ''; _notesController.text = event.metadata?.notes ?? ''; _startDate = event.startAt; _startTime = event.startAt; _endDate = event.endAt; _endTime = event.endAt; _selectedColor = event.metadata?.color ?? '#3B82F6'; _reminderMinutes = _sanitizeReminderMinutes( event.metadata?.reminderMinutes, ); } else { final now = DateTime.now(); final initial = widget.initialDate; final rounded = _roundToNearestMinute(now, 5); _startDate = initial != null ? DateTime( initial.year, initial.month, initial.day, rounded.hour, rounded.minute, ) : rounded; _startTime = _startDate; _endDate = _startDate; _endTime = _startDate.add(const Duration(hours: 1)); } } @override void dispose() { _tabController.dispose(); _titleController.dispose(); _descriptionController.dispose(); _locationController.dispose(); _notesController.dispose(); super.dispose(); } DateTime _roundToNearestMinute(DateTime dt, int interval) { final totalMinutes = dt.hour * 60 + dt.minute; final rounded = ((totalMinutes / interval).round() * interval); final hours = rounded ~/ 60; final minutes = rounded % 60; final dayOffset = hours >= 24 ? 1 : 0; final newHour = hours % 24; return DateTime(dt.year, dt.month, dt.day + dayOffset, newHour, minutes); } @override Widget build(BuildContext context) { if (widget.pageMode) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusScope.of(context).unfocus(), child: Container( color: AppColors.background, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildPageHeader(), _buildTabBar(), Expanded(child: _buildTabContent()), ], ), ), ); } return AnimatedPadding( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( height: MediaQuery.of(context).size.height * 0.85, decoration: const BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: AppSpacing.sm), Center( child: Container( width: AppSpacing.xl + AppSpacing.sm, height: AppSpacing.xs, decoration: BoxDecoration( color: AppColors.slate200, borderRadius: BorderRadius.circular(AppRadius.full), ), ), ), const SizedBox(height: AppSpacing.sm), _buildHeader(), _buildTabBar(), Expanded(child: _buildTabContent()), ], ), ), ); } Widget _buildPageHeader() { return BackTitlePageHeader( title: _isEditing ? '编辑日程' : '新建日程', onBack: () => Navigator.of(context).pop(), trailing: ValueListenableBuilder( valueListenable: _titleController, builder: (context, value, child) { final enabled = value.text.trim().isNotEmpty && !_saving; return SizedBox( height: AppSpacing.xxl * 2, child: TextButton( onPressed: enabled ? _saveEvent : null, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), minimumSize: const Size(AppSpacing.none, AppSpacing.none), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: _saving ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, trackColor: AppColors.blue200, ) : Text( '保存', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: enabled ? AppColors.blue600 : AppColors.slate400, ), ), ), ); }, ), ); } Widget _buildHeader() { return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: AppSpacing.xxl * 2, height: AppSpacing.xxl * 2, child: TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom( padding: const EdgeInsets.all(AppSpacing.none), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.full), ), ), child: const Icon( LucideIcons.x, size: AppSpacing.xxl, color: AppColors.slate700, ), ), ), Text( _isEditing ? '编辑日程' : '新建日程', style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.slate900, ), ), ValueListenableBuilder( valueListenable: _titleController, builder: (context, value, child) { final enabled = value.text.trim().isNotEmpty && !_saving; return SizedBox( height: AppSpacing.xxl * 2, child: TextButton( onPressed: enabled ? _saveEvent : null, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, ), minimumSize: const Size(AppSpacing.none, AppSpacing.none), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: _saving ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, trackColor: AppColors.blue200, ) : Text( '保存', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: enabled ? AppColors.blue600 : AppColors.slate400, ), ), ), ); }, ), ], ), ); } Widget _buildTabBar() { return Container( decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: AppColors.border)), ), child: TabBar( controller: _tabController, labelColor: AppColors.blue600, unselectedLabelColor: AppColors.slate600, indicatorColor: AppColors.blue600, tabs: const [ Tab(text: '基础'), Tab(text: '进阶'), ], ), ); } Widget _buildTabContent() { return TabBarView( controller: _tabController, children: [_buildBasicTab(), _buildAdvancedTab()], ); } Widget _buildBasicTab() { return SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField('标题', _titleController, '请输入日程标题'), const SizedBox(height: 20), _buildDateTimePicker('开始', _startDate, _startTime, (date, time) { setState(() { _startDate = date; _startTime = time; if (_endDate != null && _endTime != null) { final endDateTime = DateTime( _endDate!.year, _endDate!.month, _endDate!.day, _endTime!.hour, _endTime!.minute, ); final startDateTime = DateTime( date.year, date.month, date.day, time.hour, time.minute, ); if (endDateTime.isBefore(startDateTime) || endDateTime.isAtSameMomentAs(startDateTime)) { _endDate = date; _endTime = time.add(const Duration(hours: 1)); } } }); }), const SizedBox(height: 20), _buildDateTimePicker( '结束', _endDate ?? _startDate, _endTime ?? _startTime, (date, time) { setState(() { final startDateTime = DateTime( _startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute, ); final endDateTime = DateTime( date.year, date.month, date.day, time.hour, time.minute, ); if (endDateTime.isBefore(startDateTime) || endDateTime.isAtSameMomentAs(startDateTime)) { _endDate = _startDate; _endTime = _startTime.add(const Duration(hours: 1)); } else { _endDate = date; _endTime = time; } }); }, isOptional: true, minTime: DateTime( _startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute, ), ), ], ), ); } Widget _buildAdvancedTab() { return SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField('描述', _descriptionController, '请输入描述'), const SizedBox(height: 20), _buildTextField('地点', _locationController, '请输入地点'), const SizedBox(height: 20), _buildReminderPicker(), const SizedBox(height: 20), _buildColorPicker(), const SizedBox(height: 20), _buildTextField('备注', _notesController, '请输入备注', maxLines: 3), ], ), ); } Widget _buildTextField( String label, TextEditingController controller, String hint, { int maxLines = 1, bool autofocus = false, }) { return AppSheetInputField( controller: controller, label: label, hint: hint, maxLines: maxLines, autofocus: autofocus, ); } Widget _buildDateTimePicker( String label, DateTime date, DateTime time, Function(DateTime, DateTime) onChanged, { bool isOptional = false, DateTime? minTime, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label + (isOptional ? '(可选)' : ''), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), const SizedBox(height: 8), InkWell( onTap: () async { final picked = await _pickDateTime(date, time, minTime: minTime); if (picked == null) { return; } onChanged(picked.$1, picked.$2); }, borderRadius: BorderRadius.circular(AppRadius.md), child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.slate50, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.borderSecondary), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon( LucideIcons.calendar, size: 16, color: AppColors.slate600, ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( _formatDateTimeLabel(date, time), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.slate900, ), ), ), const Icon( LucideIcons.chevronRight, size: 16, color: AppColors.slate400, ), ], ), ), ), ], ); } String _formatDateTimeLabel(DateTime date, DateTime time) { return '${date.year}年${date.month}月${date.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } Future<(DateTime, DateTime)?> _pickDateTime( DateTime date, DateTime time, { DateTime? minTime, }) async { final result = await showModalBottomSheet<(DateTime, DateTime)>( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => DateTimePickerSheet( initialDate: date, initialTime: time, minTime: minTime, ), ); return result; } Widget _buildColorPicker() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '颜色', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), const SizedBox(height: 8), Row( children: defaultColors.map((color) { final colorHex = '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}'; final isSelected = _selectedColor == colorHex; return GestureDetector( onTap: () => setState(() => _selectedColor = colorHex), child: Container( margin: const EdgeInsets.only(right: 12), width: 32, height: 32, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: isSelected ? Border.all(color: AppColors.slate900, width: 2) : null, ), child: isSelected ? const Icon(Icons.check, size: 16, color: Colors.white) : null, ), ); }).toList(), ), ], ); } Widget _buildReminderPicker() { String labelOf(int? value) { if (value == null) { return '无提醒'; } if (value == 0) { return '准时提醒'; } return '开始前$value分钟'; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '提醒时间', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.slate700, ), ), const SizedBox(height: 8), InkWell( onTap: () async { final options = _buildReminderOptions(); final selected = await showAppSelectionSheet( context, title: '选择提醒时间', items: options .map((v) => AppSelectionItem(value: v, label: labelOf(v))) .toList(), selectedValue: _reminderMinutes, ); if (selected != null) { setState(() { _reminderMinutes = selected; }); } }, borderRadius: BorderRadius.circular(AppRadius.md), child: Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.slate50, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColors.borderSecondary), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Text( labelOf(_reminderMinutes), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.slate900, ), ), ), const Icon( LucideIcons.chevronRight, size: 16, color: AppColors.slate400, ), ], ), ), ), ], ); } int? _sanitizeReminderMinutes(int? minutes) { return (minutes != null && minutes >= 0) ? minutes : null; } List _buildReminderOptions() { final current = _sanitizeReminderMinutes(_reminderMinutes); final nonNull = _defaultReminderOptions.whereType().toSet(); if (current != null) { nonNull.add(current); } final sorted = nonNull.toList()..sort(); return [null, ...sorted]; } Future _saveEvent() async { if (_titleController.text.trim().isEmpty || _saving) return; setState(() { _saving = true; }); final startAt = DateTime( _startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute, ); DateTime? endAt; if (_endDate != null && _endTime != null) { endAt = DateTime( _endDate!.year, _endDate!.month, _endDate!.day, _endTime!.hour, _endTime!.minute, ); } final metadata = ScheduleMetadata( color: _selectedColor, location: _locationController.text.trim().isNotEmpty ? _locationController.text.trim() : null, notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null, reminderMinutes: _reminderMinutes, attachments: const [], version: widget.editingEvent?.metadata?.version ?? 1, ); final event = ScheduleItemModel( id: _isEditing ? widget.editingEvent!.id : 'evt_${DateTime.now().millisecondsSinceEpoch}', ownerId: widget.editingEvent?.ownerId ?? '', title: _titleController.text.trim(), description: _descriptionController.text.trim().isNotEmpty ? _descriptionController.text.trim() : null, startAt: startAt, endAt: endAt, metadata: metadata, ); try { final service = sl(); late final ScheduleItemModel saved; if (_isEditing) { saved = await service.updateEvent(event); } else { saved = await service.addEvent(event); } try { final notificationService = sl(); await notificationService.upsertEventReminder(saved); } catch (_) { if (mounted) { Toast.show(context, '提醒创建失败,请检查通知权限', type: ToastType.warning); } } widget.onSaved?.call(); if (mounted) { Navigator.pop(context, true); } } catch (e) { if (mounted) { Toast.show(context, '保存失败: $e', type: ToastType.error); } } finally { if (mounted) { setState(() { _saving = false; }); } } } }