import 'package:flutter/material.dart'; import '../../../core/i18n/app_locale.dart'; import '../../../core/theme/tokens.dart'; import '../../../core/ui/app_icons.dart'; import 'campaign_style.dart'; /// 캠페인 진행 기간 시트 — 사본 `333:72497`(시작일만) · `333:72719`(기간 선택 후). /// /// 값은 변형 PNG(scale 2) 실측이다(변형 spec.json 미추출): 시트 위 모서리 24 · #101010, /// 날짜 칸 49×49(행 간격 4) · 선택 원 #FF7DDD · 기간 띠 #19191C, 일요일 #F24949 · 이웃 달 #999999, /// 버튼 56 높이(비활성 #333333 · 글자 #777777 / 활성 강조색 · 검정 글자). /// /// 규칙은 OS 선택기 때와 같다 — 오늘부터 1년(365일) 안에서만 고른다. 시작 · 끝 순서로 누르고, /// 끝을 시작보다 앞에 누르면 그 날이 새 시작이 된다. Future showCampaignPeriodSheet( BuildContext context, { DateTimeRange? initial, DateTime? today, }) => showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: AppColors.cosmicSurface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (_) => CampaignPeriodSheet( initial: initial, today: DateUtils.dateOnly(today ?? DateTime.now()), ), ); class CampaignPeriodSheet extends StatefulWidget { const CampaignPeriodSheet({super.key, this.initial, required this.today}); final DateTimeRange? initial; final DateTime today; @override State createState() => _CampaignPeriodSheetState(); } class _CampaignPeriodSheetState extends State { static const _cell = 49.0; static const _rowGap = 4.0; static const _band = Color(0xFF19191C); static const _sunday = Color(0xFFF24949); static const _outside = Color(0xFF999999); static const _muted = Color(0xFF777777); late DateTime? _start = widget.initial?.start; late DateTime? _end = widget.initial?.end; late DateTime _month = DateTime( (_start ?? widget.today).year, (_start ?? widget.today).month, ); DateTime get _last => widget.today.add(const Duration(days: 365)); bool _selectable(DateTime d) => !d.isBefore(widget.today) && !d.isAfter(_last); void _pick(DateTime d) => setState(() { if (_start == null || _end != null || d.isBefore(_start!)) { _start = d; _end = null; } else { _end = d; } }); bool _canMove(int delta) { final m = DateTime(_month.year, _month.month + delta); final first = DateTime(widget.today.year, widget.today.month); final last = DateTime(_last.year, _last.month); return !m.isBefore(first) && !m.isAfter(last); } @override Widget build(BuildContext context) { final bottom = MediaQuery.paddingOf(context).bottom; final done = _start != null && _end != null; return Padding( padding: EdgeInsets.only(bottom: bottom + 13), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.fromLTRB(0, 12, 4, 0), child: IconButton( tooltip: context.l10n.commonClose, icon: const ImageIcon(AppIcons.x, size: 24), color: AppColors.onCosmic, onPressed: () => Navigator.of(context).pop(), ), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.campaignPeriodTitle, style: campaignText( 20, 600, AppColors.onCosmic, height: 1.4, spacing: 0, ), ), const SizedBox(height: 5), Text( context.l10n.campaignPeriodMax, style: campaignText(16, 400, _muted, height: 1.4), ), ], ), ), const SizedBox(height: 31), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( children: [ Text( context.l10n.campaignYearMonth( '${_month.year}', '${_month.month}', ), style: campaignText(16, 400, AppColors.onCosmic, height: 1.4), ), const Spacer(), _MonthNav( onPrev: _canMove(-1) ? () => setState( () => _month = DateTime(_month.year, _month.month - 1), ) : null, onNext: _canMove(1) ? () => setState( () => _month = DateTime(_month.year, _month.month + 1), ) : null, ), ], ), ), const SizedBox(height: 24), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ for (final w in context.l10n.campaignWeekdays.split(',')) Expanded( child: Text( w, textAlign: TextAlign.center, style: campaignText(12, 400, const Color(0xFF767676)), ), ), ], ), ), const SizedBox(height: 22), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _grid(), ), const SizedBox(height: 24), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: SizedBox( height: 56, child: FilledButton( onPressed: done ? () => Navigator.of( context, ).pop(DateTimeRange(start: _start!, end: _end!)) : null, style: FilledButton.styleFrom( backgroundColor: AppColors.cosmicAccent, foregroundColor: Colors.black, disabledBackgroundColor: const Color(0xFF333333), disabledForegroundColor: _muted, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), textStyle: campaignText(16, 600, Colors.black), ), child: Text( done ? context.l10n.campaignPeriodRange( '${_start!.month}', '${_start!.day}', '${_end!.month}', '${_end!.day}', ) : context.l10n.campaignPeriodSelect, ), ), ), ), ], ), ); } /// 일요일 시작 주 단위 격자. 앞뒤 이웃 달 날짜로 칸을 채운다. Widget _grid() { final first = DateTime(_month.year, _month.month); final lead = first.weekday % 7; final days = DateUtils.getDaysInMonth(_month.year, _month.month); final weeks = ((lead + days) / 7).ceil(); return Column( children: [ for (var w = 0; w < weeks; w++) ...[ if (w > 0) const SizedBox(height: _rowGap), Row( children: [ for (var i = 0; i < 7; i++) _day( DateTime(first.year, first.month, 1 - lead + w * 7 + i), i, ), ], ), ], ], ); } Widget _day(DateTime d, int column) { final inMonth = d.month == _month.month; final isStart = _start != null && DateUtils.isSameDay(d, _start); final isEnd = _end != null && DateUtils.isSameDay(d, _end); final inRange = _start != null && _end != null && !d.isBefore(_start!) && !d.isAfter(_end!) && !DateUtils.isSameDay(_start, _end); // 기간 띠는 시작 · 끝 원의 가운데에서 끊는다. final band = inRange ? Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: ColoredBox(color: isStart ? Colors.transparent : _band), ), Expanded( child: ColoredBox(color: isEnd ? Colors.transparent : _band), ), ], ) : null; final selected = isStart || isEnd; final color = selected ? AppColors.onCosmic : !inMonth ? _outside : column == 0 ? _sunday : AppColors.onCosmic; final enabled = inMonth && _selectable(d); return Expanded( child: SizedBox( height: _cell, child: Stack( alignment: Alignment.center, children: [ if (band != null) Positioned.fill(child: band), if (selected) const DecoratedBox( decoration: BoxDecoration( color: AppColors.cosmicAccent, shape: BoxShape.circle, ), child: SizedBox.square(dimension: _cell), ), Semantics( button: enabled, selected: selected, child: InkResponse( onTap: enabled ? () => _pick(d) : null, radius: _cell / 2, child: SizedBox.square( dimension: _cell, child: Center( child: Text( '${d.day}', style: campaignText(12, 400, color, height: 1), ), ), ), ), ), ], ), ), ); } } /// 월 이동 — 사본 64×28 알약(획 #474747) 안의 ‹ | ›. class _MonthNav extends StatelessWidget { const _MonthNav({this.onPrev, this.onNext}); final VoidCallback? onPrev; final VoidCallback? onNext; @override Widget build(BuildContext context) { Widget arrow(ImageProvider icon, String label, VoidCallback? onTap) => Semantics( button: true, label: label, child: InkResponse( onTap: onTap, radius: 16, child: SizedBox( width: 31.5, height: 28, child: Center( child: ImageIcon(icon, size: 20, color: AppColors.onCosmic), ), ), ), ); return Container( height: 28, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF474747)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ arrow(AppIcons.chevronLeft, context.l10n.calendarPrevMonth, onPrev), Container(width: 1, height: 10, color: const Color(0xFF474747)), arrow(AppIcons.chevronRight, context.l10n.calendarNextMonth, onNext), ], ), ); } }