feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持
This commit is contained in:
@@ -0,0 +1,754 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../home/presentation/navigation/home_return_policy.dart';
|
||||
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../../data/services/calendar_repository.dart';
|
||||
import '../calendar_state_manager.dart';
|
||||
import '../calendar_time_utils.dart';
|
||||
import '../utils/event_color_resolver.dart';
|
||||
import '../dayweek/day_event_layout_engine.dart';
|
||||
import '../dayweek/day_timeline_metrics.dart';
|
||||
import '../dayweek/day_view_scale.dart';
|
||||
import '../widgets/bottom_dock.dart';
|
||||
|
||||
class CalendarDayWeekScreen extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
final bool resetToToday;
|
||||
|
||||
const CalendarDayWeekScreen({
|
||||
super.key,
|
||||
this.initialDate,
|
||||
this.resetToToday = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CalendarDayWeekScreen> createState() => _CalendarDayWeekScreenState();
|
||||
}
|
||||
|
||||
class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
with WidgetsBindingObserver {
|
||||
static const double _dayItemWidth = 44;
|
||||
static const double _dayItemGap = 12;
|
||||
static const double _minEventTapHeight = 32;
|
||||
|
||||
final DayEventLayoutEngine _layoutEngine = const DayEventLayoutEngine();
|
||||
final Map<int, Offset> _activePointers = {};
|
||||
final ScrollController _dayStripController = ScrollController();
|
||||
final ScrollController _timelineController = ScrollController();
|
||||
|
||||
DayViewScale _scale = DayViewScale.defaultScale();
|
||||
DayViewScale _pinchStartScale = DayViewScale.defaultScale();
|
||||
double? _pinchStartDistance;
|
||||
|
||||
late final CalendarStateManager _calendarManager;
|
||||
late DateTime _selectedDate;
|
||||
late List<DateTime> _monthDates;
|
||||
List<ScheduleItemModel> _events = const [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_calendarManager = sl<CalendarStateManager>();
|
||||
|
||||
if (widget.resetToToday) {
|
||||
_calendarManager.resetToToday();
|
||||
}
|
||||
|
||||
_selectedDate = widget.initialDate ?? _calendarManager.selectedDate;
|
||||
_updateMonthDates();
|
||||
_loadEvents();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToSelectedDate();
|
||||
_scrollTimelineToNow();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateMonthDates() {
|
||||
_monthDates = monthDatesFor(_selectedDate);
|
||||
}
|
||||
|
||||
Future<void> _loadEvents({bool forceRefresh = false}) async {
|
||||
final events = await sl<CalendarRepository>().getDayEvents(
|
||||
_selectedDate,
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_events = events;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_dayStripController.dispose();
|
||||
_timelineController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_loadEvents(forceRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.todoBg,
|
||||
body: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop) {
|
||||
returnToHomePreserveState(context, forceGoHome: true);
|
||||
}
|
||||
},
|
||||
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(
|
||||
controller: _timelineController,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: AppSpacing.lg,
|
||||
right: AppSpacing.lg,
|
||||
top: 2,
|
||||
),
|
||||
child: RepaintBoundary(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;
|
||||
_scale = DayViewScale.defaultScale();
|
||||
});
|
||||
_calendarManager.setSelectedDate(today);
|
||||
_updateMonthDates();
|
||||
_scrollToSelectedDate(animate: true);
|
||||
_scrollTimelineToNow(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;
|
||||
_pinchStartScale = _scale;
|
||||
}
|
||||
}
|
||||
|
||||
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 nextScale = _pinchStartScale.zoomByFactor(
|
||||
currentDistance / startDistance,
|
||||
);
|
||||
if ((nextScale.hourHeight - _scale.hourHeight).abs() < 0.1) {
|
||||
return;
|
||||
}
|
||||
if (nextScale.hourHeight < _scale.hourHeight &&
|
||||
_scale.hourHeight <= DayViewScale.minHourHeight) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_scale = nextScale;
|
||||
});
|
||||
}
|
||||
|
||||
void _handlePointerUp(PointerUpEvent event) {
|
||||
_handlePointerRemove(event.pointer);
|
||||
}
|
||||
|
||||
void _handlePointerCancel(PointerCancelEvent event) {
|
||||
_handlePointerRemove(event.pointer);
|
||||
}
|
||||
|
||||
void _handlePointerRemove(int pointer) {
|
||||
_activePointers.remove(pointer);
|
||||
if (_activePointers.length < 2) {
|
||||
_pinchStartDistance = null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final monthLabel = context.l10n.calendarDayWeekMonthYearLabel(
|
||||
_selectedDate.year,
|
||||
_selectedDate.month,
|
||||
);
|
||||
final isNotToday = !isSameDay(_selectedDate, DateTime.now());
|
||||
|
||||
return SizedBox(
|
||||
height: 68,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, top: 12, bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
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,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.chevronLeft,
|
||||
size: 16,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.12),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
monthLabel,
|
||||
key: ValueKey(monthLabel),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isNotToday)
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
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: Center(
|
||||
child: Text(
|
||||
context.l10n.calendarToday,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isNotToday) const SizedBox(width: 8),
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: () async {
|
||||
final changed = await context.push<bool>(
|
||||
'${AppRoutes.calendarEventCreate}?date=${formatYmd(_selectedDate)}',
|
||||
);
|
||||
if (changed == true) {
|
||||
await _loadEvents(forceRefresh: true);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blue600,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.plus,
|
||||
size: 20,
|
||||
color: AppColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekStrip() {
|
||||
final stripKey = ValueKey('${_selectedDate.year}-${_selectedDate.month}');
|
||||
|
||||
return SizedBox(
|
||||
height: 86,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
child: ListView.separated(
|
||||
key: stripKey,
|
||||
controller: _dayStripController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
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 AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
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);
|
||||
}
|
||||
|
||||
void _scrollTimelineToNow({bool animate = false}) {
|
||||
if (!_timelineController.hasClients) {
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final minuteOfDay = now.hour * 60 + now.minute;
|
||||
final targetY = _scale.pixelsForMinutes(minuteOfDay);
|
||||
final viewportHeight = _timelineController.position.viewportDimension;
|
||||
final offset = targetY - (viewportHeight / 3);
|
||||
final max = _timelineController.position.maxScrollExtent;
|
||||
final clampedOffset = offset.clamp(0.0, max);
|
||||
|
||||
if (animate) {
|
||||
_timelineController.animateTo(
|
||||
clampedOffset,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_timelineController.jumpTo(clampedOffset);
|
||||
}
|
||||
|
||||
Widget _buildDayItem(DateTime date, bool isSelected, bool isWeekend) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_weekdayLabel(date),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isWeekend ? AppColors.slate400 : AppColors.slate600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 140),
|
||||
curve: Curves.easeOut,
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.blue100 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${date.day}',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
|
||||
color: isSelected
|
||||
? AppColors.blue600
|
||||
: (isWeekend ? AppColors.slate400 : AppColors.slate900),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _weekdayLabel(DateTime date) {
|
||||
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
return labels[date.weekday % 7];
|
||||
}
|
||||
|
||||
Widget _buildTimelineBoard() {
|
||||
final now = DateTime.now();
|
||||
final showCurrent = shouldShowCurrentMarker(_selectedDate, now);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final boardWidth = constraints.maxWidth;
|
||||
final boardHeight = DayTimelineMetrics.timelineHeight(_scale);
|
||||
final eventAreaLeft = DayTimelineMetrics.eventAreaLeft();
|
||||
final eventAreaWidth = DayTimelineMetrics.eventAreaWidth(boardWidth);
|
||||
|
||||
final layouts = _layoutEngine.layout(
|
||||
events: _events,
|
||||
scale: _scale,
|
||||
eventAreaLeft: eventAreaLeft,
|
||||
eventAreaWidth: eventAreaWidth,
|
||||
viewDate: _selectedDate,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: boardHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
RepaintBoundary(
|
||||
child: _buildTimelineGrid(
|
||||
boardHeight: boardHeight,
|
||||
eventAreaLeft: eventAreaLeft,
|
||||
),
|
||||
),
|
||||
if (showCurrent)
|
||||
_buildCurrentTimeMarker(now: now, boardHeight: boardHeight),
|
||||
RepaintBoundary(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
for (final layout in layouts)
|
||||
_buildEventCard(layout: layout, boardHeight: boardHeight),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimelineGrid({
|
||||
required double boardHeight,
|
||||
required double eventAreaLeft,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: boardHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (var hour = 0; hour <= DayTimelineMetrics.hoursInDay; hour++)
|
||||
_buildHourTick(
|
||||
hour: hour,
|
||||
boardHeight: boardHeight,
|
||||
eventAreaLeft: eventAreaLeft,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHourTick({
|
||||
required int hour,
|
||||
required double boardHeight,
|
||||
required double eventAreaLeft,
|
||||
}) {
|
||||
final minute = hour * DayTimelineMetrics.minutesInHour;
|
||||
final y = _scale.pixelsForMinutes(minute);
|
||||
final isLastHour = hour == DayTimelineMetrics.hoursInDay;
|
||||
final adjustedY = isLastHour ? boardHeight - 1 : y;
|
||||
final labelTop = (adjustedY - 7).clamp(0.0, boardHeight - 14);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: adjustedY,
|
||||
left: eventAreaLeft,
|
||||
right: 0,
|
||||
child: Container(height: 1, color: AppColors.border),
|
||||
),
|
||||
Positioned(
|
||||
top: labelTop,
|
||||
left: 0,
|
||||
width: DayTimelineMetrics.timeLabelWidth,
|
||||
child: Text(
|
||||
formatHour(hour),
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentTimeMarker({
|
||||
required DateTime now,
|
||||
required double boardHeight,
|
||||
}) {
|
||||
final minute = now.hour * DayTimelineMetrics.minutesInHour + now.minute;
|
||||
final top = _scale.pixelsForMinutes(minute).clamp(0.0, boardHeight);
|
||||
|
||||
return Positioned(
|
||||
top: top - 9,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: DayTimelineMetrics.timeLabelWidth,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.red500,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
formatHm(now),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DayTimelineMetrics.timeLabelGap),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.red500,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCard({
|
||||
required DayEventLayout layout,
|
||||
required double boardHeight,
|
||||
}) {
|
||||
final isArchived = layout.event.status == ScheduleStatus.archived;
|
||||
Color eventColor;
|
||||
if (isArchived) {
|
||||
eventColor = AppColors.slate400;
|
||||
} else {
|
||||
eventColor = resolveEventColor(
|
||||
status: layout.event.status,
|
||||
colorHex: layout.event.metadata?.color,
|
||||
);
|
||||
}
|
||||
final isCompact = layout.visualHeight < 20;
|
||||
final tapHeight = layout.visualHeight < _minEventTapHeight
|
||||
? _minEventTapHeight
|
||||
: layout.visualHeight;
|
||||
final top = (layout.top - ((tapHeight - layout.visualHeight) / 2)).clamp(
|
||||
0.0,
|
||||
boardHeight - tapHeight,
|
||||
);
|
||||
final visualTop = layout.top - top;
|
||||
|
||||
return Positioned(
|
||||
top: top,
|
||||
left: layout.left,
|
||||
width: layout.width,
|
||||
height: tapHeight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
final changed = await context.push<bool>(
|
||||
AppRoutes.calendarEventDetail(layout.event.id),
|
||||
);
|
||||
if (changed == true) {
|
||||
await _loadEvents(forceRefresh: true);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: visualTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: layout.visualHeight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
right: DayTimelineMetrics.eventColumnGap,
|
||||
),
|
||||
padding: isCompact
|
||||
? const EdgeInsets.symmetric(horizontal: 4, vertical: 2)
|
||||
: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: eventColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: eventColor, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: eventColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
if (!isCompact) const SizedBox(width: 4),
|
||||
if (!isCompact)
|
||||
Expanded(
|
||||
child: Text(
|
||||
layout.event.title,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: eventColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomDock() {
|
||||
return BottomDock(
|
||||
activeTab: DockTab.calendar,
|
||||
onTodoTap: () {
|
||||
_calendarManager.setViewType(CalendarViewType.day);
|
||||
context.push(AppRoutes.todoList);
|
||||
},
|
||||
onCalendarTap: () {
|
||||
_calendarManager.setViewType(CalendarViewType.day);
|
||||
context.push(AppRoutes.calendarMonth);
|
||||
},
|
||||
onHomeTap: () => returnToHomePreserveState(context, forceGoHome: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../widgets/create_event_sheet.dart';
|
||||
|
||||
class CalendarEventCreateScreen extends StatelessWidget {
|
||||
final DateTime? initialDate;
|
||||
|
||||
const CalendarEventCreateScreen({super.key, this.initialDate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: CreateEventSheet(initialDate: initialDate, pageMode: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../../features/notification/data/services/local_notification_service.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
import '../../../../shared/widgets/detail_header_action_menu.dart';
|
||||
import '../../../../shared/widgets/destructive_action_sheet.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../data/services/calendar_service.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../utils/event_color_resolver.dart';
|
||||
|
||||
enum _CalendarHeaderAction { edit, delete, share, archive }
|
||||
|
||||
class CalendarEventDetailScreen extends StatefulWidget {
|
||||
final String eventId;
|
||||
|
||||
const CalendarEventDetailScreen({super.key, required this.eventId});
|
||||
|
||||
@override
|
||||
State<CalendarEventDetailScreen> createState() =>
|
||||
_CalendarEventDetailScreenState();
|
||||
}
|
||||
|
||||
class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
ScheduleItemModel? _event;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEvent();
|
||||
}
|
||||
|
||||
Future<void> _loadEvent() async {
|
||||
try {
|
||||
_event = await sl<CalendarService>().getEventById(widget.eventId);
|
||||
} catch (e) {
|
||||
_event = null;
|
||||
} finally {
|
||||
_loading = false;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))),
|
||||
);
|
||||
}
|
||||
if (_event == null) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.homeBackgroundTop, AppColors.background],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
BackTitlePageHeader(title: context.l10n.calendarDetailTitle),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.calendarDetailNotFoundTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
context.l10n.calendarDetailNotFoundDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final event = _event!;
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.homeBackgroundTop, AppColors.background],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(event),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg,
|
||||
AppSpacing.sm,
|
||||
AppSpacing.lg,
|
||||
AppSpacing.xl,
|
||||
),
|
||||
children: [
|
||||
_buildHeroSurface(event),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildMetaSurface(event),
|
||||
if (_hasExtraInfo(event)) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildExtraSurface(event),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ScheduleItemModel event) {
|
||||
return BackTitlePageHeader(
|
||||
title: context.l10n.calendarDetailTitle,
|
||||
onBack: () => context.pop(),
|
||||
trailing: _buildHeaderActions(event),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderActions(ScheduleItemModel event) {
|
||||
final items = <DetailHeaderActionItem<_CalendarHeaderAction>>[];
|
||||
if (event.canEdit) {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.edit,
|
||||
label: context.l10n.commonEdit,
|
||||
icon: LucideIcons.pencil,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (event.canDelete) {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.delete,
|
||||
label: context.l10n.commonDelete,
|
||||
icon: LucideIcons.trash2,
|
||||
isDestructive: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (event.canInvite) {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.share,
|
||||
label: context.l10n.commonShare,
|
||||
icon: LucideIcons.share2,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (event.status != ScheduleStatus.archived && event.canEdit) {
|
||||
final isExpired = _isEventExpired(event);
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.archive,
|
||||
label: context.l10n.commonArchive,
|
||||
icon: LucideIcons.archive,
|
||||
enabled: !isExpired,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DetailHeaderActionMenu<_CalendarHeaderAction>(
|
||||
items: items,
|
||||
onSelected: (action) => _handleHeaderAction(action, event),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isEventExpired(ScheduleItemModel event) {
|
||||
final now = DateTime.now();
|
||||
if (event.endAt != null) {
|
||||
return event.endAt!.isBefore(now);
|
||||
}
|
||||
return event.startAt.isBefore(now);
|
||||
}
|
||||
|
||||
Future<void> _handleHeaderAction(
|
||||
_CalendarHeaderAction action,
|
||||
ScheduleItemModel event,
|
||||
) async {
|
||||
switch (action) {
|
||||
case _CalendarHeaderAction.edit:
|
||||
final changed = await context.push<bool>(
|
||||
AppRoutes.calendarEventEdit(event.id),
|
||||
);
|
||||
if (changed == true) {
|
||||
_loadEvent();
|
||||
if (mounted) context.pop(true);
|
||||
}
|
||||
return;
|
||||
case _CalendarHeaderAction.delete:
|
||||
_showDeleteConfirmation();
|
||||
return;
|
||||
case _CalendarHeaderAction.share:
|
||||
context.push(AppRoutes.calendarEventShare(event.id));
|
||||
return;
|
||||
case _CalendarHeaderAction.archive:
|
||||
_archiveEvent();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeroSurface(ScheduleItemModel event) {
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
);
|
||||
final timeRange = _formatRangeLabel(event.startAt, event.endAt);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderTertiary),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.slate200.withValues(alpha: 0.38),
|
||||
blurRadius: AppRadius.lg,
|
||||
offset: const Offset(0, AppSpacing.xs),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: AppSpacing.sm,
|
||||
height: AppSpacing.xxl,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
_buildStatusBadge(event.status),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceInfoLight,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderQuaternary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.calendarDetailTimeArrangement,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
timeRange,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetaSurface(ScheduleItemModel event) {
|
||||
final startAt = event.startAt;
|
||||
final dateStr = context.l10n.calendarDetailDateLabel(
|
||||
startAt.year,
|
||||
startAt.month,
|
||||
startAt.day,
|
||||
_getWeekday(startAt.weekday),
|
||||
);
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.calendarDetailBasicInfo,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(context.l10n.calendarDetailDate, dateStr),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
context.l10n.calendarDetailReminder,
|
||||
_formatReminderText(event.metadata?.reminderMinutes),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: AppSpacing.xxl * 3,
|
||||
child: Text(
|
||||
context.l10n.calendarDetailColor,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: AppSpacing.xl,
|
||||
height: AppSpacing.xl,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasExtraInfo(ScheduleItemModel event) {
|
||||
return (event.metadata?.location?.trim().isNotEmpty ?? false) ||
|
||||
(event.description?.trim().isNotEmpty ?? false) ||
|
||||
(event.metadata?.notes?.trim().isNotEmpty ?? false);
|
||||
}
|
||||
|
||||
Widget _buildExtraSurface(ScheduleItemModel event) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.calendarDetailExtraInfo,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
if (event.metadata?.location?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
context.l10n.calendarDetailLocation,
|
||||
event.metadata!.location!.trim(),
|
||||
),
|
||||
],
|
||||
if (event.description?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
context.l10n.calendarDetailDescription,
|
||||
event.description!.trim(),
|
||||
),
|
||||
],
|
||||
if (event.metadata?.notes?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
context.l10n.calendarDetailNotes,
|
||||
event.metadata!.notes!.trim(),
|
||||
multiline: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatReminderText(int? reminderMinutes) {
|
||||
if (reminderMinutes == null) {
|
||||
return context.l10n.calendarDetailReminderNone;
|
||||
}
|
||||
if (reminderMinutes == 0) {
|
||||
return context.l10n.calendarDetailReminderOnTime;
|
||||
}
|
||||
return context.l10n.calendarDetailReminderBeforeMinutes(reminderMinutes);
|
||||
}
|
||||
|
||||
String _getWeekday(int weekday) {
|
||||
final l10n = context.l10n;
|
||||
final weekdays = [
|
||||
l10n.calendarWeekdayMon,
|
||||
l10n.calendarWeekdayTue,
|
||||
l10n.calendarWeekdayWed,
|
||||
l10n.calendarWeekdayThu,
|
||||
l10n.calendarWeekdayFri,
|
||||
l10n.calendarWeekdaySat,
|
||||
l10n.calendarWeekdaySun,
|
||||
];
|
||||
return weekdays[weekday - 1];
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Future<void> _showDeleteConfirmation() async {
|
||||
final confirmed = await showDestructiveActionSheet(
|
||||
context,
|
||||
title: context.l10n.calendarDetailDeleteTitle,
|
||||
message: context.l10n.calendarDetailDeleteMessage,
|
||||
confirmText: context.l10n.calendarDetailDeleteConfirm,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await sl<CalendarService>().deleteEvent(widget.eventId);
|
||||
try {
|
||||
await sl<LocalNotificationService>().cancelEventReminder(widget.eventId);
|
||||
} catch (_) {}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
context.pop(true);
|
||||
}
|
||||
|
||||
Future<void> _archiveEvent() async {
|
||||
final confirmed = await showDestructiveActionSheet(
|
||||
context,
|
||||
title: context.l10n.calendarDetailArchiveTitle,
|
||||
message: context.l10n.calendarDetailArchiveMessage,
|
||||
confirmText: context.l10n.calendarDetailArchiveConfirm,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await sl<CalendarService>().archiveEvent(widget.eventId);
|
||||
if (mounted) {
|
||||
context.pop(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.calendarDetailArchiveFailed,
|
||||
type: ToastType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatRangeLabel(DateTime startAt, DateTime? endAt) {
|
||||
final startLabel = context.l10n.calendarDetailDateTimeShort(
|
||||
startAt.month,
|
||||
startAt.day,
|
||||
_getWeekday(startAt.weekday),
|
||||
_formatTime(startAt),
|
||||
);
|
||||
if (endAt == null) {
|
||||
return startLabel;
|
||||
}
|
||||
final isSameDay =
|
||||
startAt.year == endAt.year &&
|
||||
startAt.month == endAt.month &&
|
||||
startAt.day == endAt.day;
|
||||
if (isSameDay) {
|
||||
return '$startLabel - ${_formatTime(endAt)}';
|
||||
}
|
||||
final endLabel = context.l10n.calendarDetailDateTimeShort(
|
||||
endAt.month,
|
||||
endAt.day,
|
||||
_getWeekday(endAt.weekday),
|
||||
_formatTime(endAt),
|
||||
);
|
||||
return context.l10n.calendarDetailRangeWithStartEnd(startLabel, endLabel);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(ScheduleStatus status) {
|
||||
final isArchived = status == ScheduleStatus.archived;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isArchived
|
||||
? AppColors.feedbackWarningSurface
|
||||
: AppColors.feedbackSuccessSurface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(
|
||||
color: isArchived
|
||||
? AppColors.feedbackWarningBorder
|
||||
: AppColors.feedbackSuccessBorder,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isArchived
|
||||
? context.l10n.calendarDetailStatusExpired
|
||||
: context.l10n.settingsJobStatusEnabled,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isArchived
|
||||
? AppColors.feedbackWarningText
|
||||
: AppColors.feedbackSuccessText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value, {bool multiline = false}) {
|
||||
return Row(
|
||||
crossAxisAlignment: multiline
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: AppSpacing.xxl * 3,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: multiline ? 1.4 : 1.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../widgets/create_event_sheet.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../../data/services/calendar_service.dart';
|
||||
|
||||
class CalendarEventEditScreen extends StatefulWidget {
|
||||
final String eventId;
|
||||
|
||||
const CalendarEventEditScreen({super.key, required this.eventId});
|
||||
|
||||
@override
|
||||
State<CalendarEventEditScreen> createState() =>
|
||||
_CalendarEventEditScreenState();
|
||||
}
|
||||
|
||||
class _CalendarEventEditScreenState extends State<CalendarEventEditScreen> {
|
||||
ScheduleItemModel? _event;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
_event = await sl<CalendarService>().getEventById(widget.eventId);
|
||||
} catch (_) {
|
||||
_event = null;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))),
|
||||
);
|
||||
}
|
||||
|
||||
if (_event == null) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Text(context.l10n.calendarEventNoAccessOrMissing),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: CreateEventSheet(editingEvent: _event, pageMode: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../../data/services/calendar_service.dart';
|
||||
import '../widgets/calendar_share_dialog.dart';
|
||||
|
||||
class CalendarEventShareScreen extends StatefulWidget {
|
||||
final String eventId;
|
||||
|
||||
const CalendarEventShareScreen({super.key, required this.eventId});
|
||||
|
||||
@override
|
||||
State<CalendarEventShareScreen> createState() =>
|
||||
_CalendarEventShareScreenState();
|
||||
}
|
||||
|
||||
class _CalendarEventShareScreenState extends State<CalendarEventShareScreen> {
|
||||
ScheduleItemModel? _event;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
_event = await sl<CalendarService>().getEventById(widget.eventId);
|
||||
} catch (_) {
|
||||
_event = null;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: AppLoadingIndicator(size: 22))),
|
||||
);
|
||||
}
|
||||
|
||||
final event = _event;
|
||||
if (event == null) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Text(context.l10n.calendarEventNoAccessOrMissing),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
BackTitlePageHeader(title: context.l10n.calendarShareTitle),
|
||||
Expanded(
|
||||
child: CalendarShareDialog(
|
||||
eventId: event.id,
|
||||
eventTitle: event.title,
|
||||
canInvite: event.canInvite,
|
||||
canEdit: event.canEdit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../../home/presentation/navigation/home_return_policy.dart';
|
||||
import '../calendar_state_manager.dart';
|
||||
import '../calendar_time_utils.dart';
|
||||
import '../utils/event_color_resolver.dart';
|
||||
import '../widgets/bottom_dock.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
import '../../data/services/calendar_repository.dart';
|
||||
|
||||
class CalendarMonthScreen extends StatefulWidget {
|
||||
final bool resetToToday;
|
||||
|
||||
const CalendarMonthScreen({super.key, this.resetToToday = false});
|
||||
|
||||
@override
|
||||
State<CalendarMonthScreen> createState() => _CalendarMonthScreenState();
|
||||
}
|
||||
|
||||
class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
with WidgetsBindingObserver {
|
||||
late final CalendarStateManager _calendarManager;
|
||||
late DateTime _currentMonth;
|
||||
late DateTime _selectedDate;
|
||||
final Map<String, List<ScheduleItemModel>> _eventsByDay = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_calendarManager = sl<CalendarStateManager>();
|
||||
|
||||
if (widget.resetToToday) {
|
||||
_calendarManager.resetToToday();
|
||||
}
|
||||
|
||||
final savedDate = _calendarManager.selectedDate;
|
||||
_selectedDate = savedDate;
|
||||
_currentMonth = DateTime(savedDate.year, savedDate.month, 1);
|
||||
_loadMonthEvents();
|
||||
}
|
||||
|
||||
Future<void> _loadMonthEvents({bool forceRefresh = false}) async {
|
||||
final events = await sl<CalendarRepository>().getMonthEvents(
|
||||
_currentMonth,
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_eventsByDay.clear();
|
||||
for (final event in events) {
|
||||
final key = formatYmd(event.startAt);
|
||||
_eventsByDay[key] = [...(_eventsByDay[key] ?? const []), event];
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_loadMonthEvents(forceRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.todoBg,
|
||||
body: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (!didPop) {
|
||||
returnToHomePreserveState(context, forceGoHome: true);
|
||||
}
|
||||
},
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 84),
|
||||
child: _buildMonthContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildBottomDock(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final l10n = context.l10n;
|
||||
final today = DateTime.now();
|
||||
final isNotToday = !isSameDay(_selectedDate, today);
|
||||
|
||||
return SizedBox(
|
||||
height: 76,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
onTap: _showMonthPicker,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
child: Text(
|
||||
l10n.calendarMonthHeader(_currentMonth.month),
|
||||
key: ValueKey(_currentMonth.month),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Icon(
|
||||
LucideIcons.chevronDown,
|
||||
size: 16,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isNotToday)
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
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: Center(
|
||||
child: Text(
|
||||
l10n.calendarMonthToday,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isNotToday) const SizedBox(width: 8),
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: () async {
|
||||
final changed = await context.push<bool>(
|
||||
AppRoutes.calendarEventCreate,
|
||||
);
|
||||
if (changed == true) {
|
||||
await _loadMonthEvents(forceRefresh: true);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blue600,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.plus,
|
||||
size: 20,
|
||||
color: AppColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _goToToday() {
|
||||
final today = DateTime.now();
|
||||
final targetMonth = DateTime(today.year, today.month, 1);
|
||||
setState(() {
|
||||
_selectedDate = today;
|
||||
if (_currentMonth.year != targetMonth.year ||
|
||||
_currentMonth.month != targetMonth.month) {
|
||||
_currentMonth = targetMonth;
|
||||
}
|
||||
});
|
||||
_calendarManager.setSelectedDate(today);
|
||||
_loadMonthEvents(forceRefresh: true);
|
||||
}
|
||||
|
||||
Widget _buildMonthContent() {
|
||||
return Column(
|
||||
children: [
|
||||
_buildWeekdayHeader(),
|
||||
Container(height: 1, color: AppColors.border),
|
||||
..._buildWeeks(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekdayHeader() {
|
||||
final l10n = context.l10n;
|
||||
final List<String> weekdays = [
|
||||
l10n.calendarMonthWeekdaySunShort,
|
||||
l10n.calendarMonthWeekdayMonShort,
|
||||
l10n.calendarMonthWeekdayTueShort,
|
||||
l10n.calendarMonthWeekdayWedShort,
|
||||
l10n.calendarMonthWeekdayThuShort,
|
||||
l10n.calendarMonthWeekdayFriShort,
|
||||
l10n.calendarMonthWeekdaySatShort,
|
||||
];
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: weekdays
|
||||
.map(
|
||||
(day) => SizedBox(
|
||||
width: 36,
|
||||
child: Center(
|
||||
child: Text(
|
||||
day,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildWeeks() {
|
||||
final firstDayOfMonth = DateTime(
|
||||
_currentMonth.year,
|
||||
_currentMonth.month,
|
||||
1,
|
||||
);
|
||||
final lastDayOfMonth = DateTime(
|
||||
_currentMonth.year,
|
||||
_currentMonth.month + 1,
|
||||
0,
|
||||
);
|
||||
final startWeekday = firstDayOfMonth.weekday % 7;
|
||||
|
||||
final daysInMonth = lastDayOfMonth.day;
|
||||
final totalCells = ((daysInMonth + startWeekday) / 7).ceil() * 7;
|
||||
|
||||
final weeks = <Widget>[];
|
||||
for (var weekStart = 0; weekStart < totalCells; weekStart += 7) {
|
||||
weeks.add(_buildWeekRow(weekStart, startWeekday, daysInMonth));
|
||||
if (weekStart + 7 < totalCells) {
|
||||
weeks.add(Container(height: 1, color: AppColors.border));
|
||||
}
|
||||
}
|
||||
|
||||
return weeks;
|
||||
}
|
||||
|
||||
Widget _buildWeekRow(int weekStart, int startWeekday, int daysInMonth) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(7, (index) {
|
||||
final dayIndex = weekStart + index - startWeekday + 1;
|
||||
if (dayIndex < 1 || dayIndex > daysInMonth) {
|
||||
return SizedBox(width: 36, height: 36);
|
||||
}
|
||||
|
||||
final date = DateTime(
|
||||
_currentMonth.year,
|
||||
_currentMonth.month,
|
||||
dayIndex,
|
||||
);
|
||||
final isSelected = isSameDay(_selectedDate, date);
|
||||
|
||||
return AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedDate = date;
|
||||
});
|
||||
_calendarManager.setSelectedDate(date);
|
||||
_calendarManager.setViewType(CalendarViewType.month);
|
||||
context.push(
|
||||
'${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}',
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.blue100 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$dayIndex',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? AppColors.blue600
|
||||
: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildWeekEvents(weekStart, startWeekday, daysInMonth),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekEvents(int weekStart, int startWeekday, int daysInMonth) {
|
||||
final firstDayOfMonth = DateTime(
|
||||
_currentMonth.year,
|
||||
_currentMonth.month,
|
||||
1,
|
||||
);
|
||||
final weekFirstDate = firstDayOfMonth.add(
|
||||
Duration(days: weekStart - startWeekday),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: 70,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(7, (index) {
|
||||
final dayIndex = weekStart + index - startWeekday + 1;
|
||||
if (dayIndex < 1 || dayIndex > daysInMonth) {
|
||||
return const SizedBox(width: 38, height: 1);
|
||||
}
|
||||
|
||||
final date = weekFirstDate.add(Duration(days: index));
|
||||
final events = _eventsByDay[formatYmd(date)] ?? const [];
|
||||
final displayEvents = events.take(2).toList();
|
||||
final remainingCount = events.length - 2;
|
||||
|
||||
return SizedBox(
|
||||
width: 38,
|
||||
height: 70,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...displayEvents.map((event) {
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
);
|
||||
return AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
onTap: () async {
|
||||
_calendarManager.setSelectedDate(date);
|
||||
final changed = await context.push<bool>(
|
||||
'/calendar/events/${event.id}',
|
||||
);
|
||||
if (changed == true) {
|
||||
await _loadMonthEvents(forceRefresh: true);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
event.title,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (remainingCount > 0)
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
onTap: () {
|
||||
_calendarManager.setSelectedDate(date);
|
||||
_calendarManager.setViewType(CalendarViewType.day);
|
||||
context.push(
|
||||
'${AppRoutes.calendarDayWeek}?date=${formatYmd(date)}',
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'+$remainingCount',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: AppColors.slate500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMonthPicker() {
|
||||
final l10n = context.l10n;
|
||||
var selectedYear = _currentMonth.year;
|
||||
var selectedMonth = _currentMonth.month;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.white,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setSheetState) {
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.commonCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
setState(() {
|
||||
_currentMonth = DateTime(
|
||||
selectedYear,
|
||||
selectedMonth,
|
||||
1,
|
||||
);
|
||||
_selectedDate = DateTime(
|
||||
selectedYear,
|
||||
selectedMonth,
|
||||
1,
|
||||
);
|
||||
});
|
||||
_calendarManager.setSelectedDate(_selectedDate);
|
||||
_loadMonthEvents();
|
||||
},
|
||||
child: Text(l10n.commonConfirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: CupertinoPicker(
|
||||
itemExtent: 40,
|
||||
scrollController: FixedExtentScrollController(
|
||||
initialItem: _currentMonth.year - 2020,
|
||||
),
|
||||
onSelectedItemChanged: (index) {
|
||||
setSheetState(() {
|
||||
selectedYear = 2020 + index;
|
||||
});
|
||||
},
|
||||
children: List.generate(20, (index) {
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.calendarMonthYearLabel(2020 + index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoPicker(
|
||||
itemExtent: 40,
|
||||
scrollController: FixedExtentScrollController(
|
||||
initialItem: _currentMonth.month - 1,
|
||||
),
|
||||
onSelectedItemChanged: (index) {
|
||||
setSheetState(() {
|
||||
selectedMonth = index + 1;
|
||||
});
|
||||
},
|
||||
children: List.generate(12, (index) {
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.calendarMonthHeader(index + 1),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomDock() {
|
||||
return BottomDock(
|
||||
activeTab: DockTab.calendar,
|
||||
onTodoTap: () {
|
||||
_calendarManager.setViewType(CalendarViewType.month);
|
||||
context.push(AppRoutes.todoList);
|
||||
},
|
||||
onCalendarTap: () {},
|
||||
onHomeTap: () => returnToHomePreserveState(context, forceGoHome: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user