Files
IKAR/lib/frontend/widgets/schedule_time_picker.dart

246 lines
7.2 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../core/utils/format.dart';
import '../../l10n/app_localizations.dart';
import 'hint_bubble.dart';
import 'sheet_helpers.dart';
import '../../core/config/app_fonts.dart';
import '../../core/config/app_shape.dart';
/// Барабан выбора времени отправки («Отправить позже»): три колонки —
/// день, час, минута. Возвращает выбранный момент в будущем или null.
Future<DateTime?> showScheduleTimePicker(
BuildContext context, {
DateTime? initial,
String? title,
}) {
final sheetTitle =
title ?? AppLocalizations.of(context)!.scheduleTimePickerTitle;
return showModalBottomSheet<DateTime>(
context: context,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
shape: kSheetShape,
builder: (_) => _ScheduleSheet(initial: initial, title: sheetTitle),
);
}
class _ScheduleSheet extends StatefulWidget {
final DateTime? initial;
final String title;
const _ScheduleSheet({required this.initial, required this.title});
@override
State<_ScheduleSheet> createState() => _ScheduleSheetState();
}
class _ScheduleSheetState extends State<_ScheduleSheet> {
static const int _dayCount = 366;
late final DateTime _today;
late final FixedExtentScrollController _dayCtrl;
late final FixedExtentScrollController _hourCtrl;
late final FixedExtentScrollController _minuteCtrl;
late int _dayIndex;
late int _hour;
late int _minute;
@override
void initState() {
super.initState();
final now = DateTime.now();
_today = DateTime(now.year, now.month, now.day);
final base = (widget.initial != null && widget.initial!.isAfter(now))
? widget.initial!
: now.add(const Duration(minutes: 1));
_dayIndex = DateTime(
base.year,
base.month,
base.day,
).difference(_today).inDays.clamp(0, _dayCount - 1);
_hour = base.hour;
_minute = base.minute;
_dayCtrl = FixedExtentScrollController(initialItem: _dayIndex);
_hourCtrl = FixedExtentScrollController(initialItem: _hour);
_minuteCtrl = FixedExtentScrollController(initialItem: _minute);
}
@override
void dispose() {
_dayCtrl.dispose();
_hourCtrl.dispose();
_minuteCtrl.dispose();
super.dispose();
}
String _dayLabel(int index) {
final l10n = AppLocalizations.of(context)!;
if (index == 0) return l10n.scheduleTimePickerToday;
if (index == 1) return l10n.scheduleTimePickerTomorrow;
final d = _today.add(Duration(days: index));
return l10n.scheduleTimePickerDayLabel(
formatWeekdayShort(l10n, d.weekday),
formatDayMonth(l10n, d),
);
}
DateTime get _selected => DateTime(
_today.year,
_today.month,
_today.day + _dayIndex,
_hour,
_minute,
);
String get _buttonLabel {
final l10n = AppLocalizations.of(context)!;
final s = _selected;
final day = _dayIndex == 0
? l10n.scheduleTimePickerTodayLower
: _dayIndex == 1
? l10n.scheduleTimePickerTomorrowLower
: formatDayMonth(l10n, s);
return l10n.scheduleTimePickerSendAt(day, formatClock(s));
}
void _confirm(BuildContext buttonContext) {
final result = _selected;
if (!result.isAfter(DateTime.now())) {
showHintBubble(
buttonContext,
AppLocalizations.of(context)!.scheduleTimePickerPastTime,
);
return;
}
Navigator.of(context).pop(result);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: cs.onSurfaceVariant.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
Text(
widget.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 8),
SizedBox(
height: 190,
child: Row(
children: [
_wheel(
cs: cs,
controller: _dayCtrl,
count: _dayCount,
flex: 3,
align: Alignment.centerLeft,
onChanged: (i) => _dayIndex = i,
label: _dayLabel,
),
_wheel(
cs: cs,
controller: _hourCtrl,
count: 24,
onChanged: (i) => _hour = i,
label: pad2,
),
_wheel(
cs: cs,
controller: _minuteCtrl,
count: 60,
onChanged: (i) => _minute = i,
label: pad2,
),
],
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: Builder(
builder: (buttonContext) => FilledButton(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: AppShape.buttonBorder,
),
onPressed: () => _confirm(buttonContext),
child: Text(
_buttonLabel,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
),
);
}
Widget _wheel({
required ColorScheme cs,
required FixedExtentScrollController controller,
required int count,
required void Function(int) onChanged,
required String Function(int) label,
int flex = 1,
Alignment align = Alignment.center,
}) {
return Expanded(
flex: flex,
child: CupertinoPicker(
scrollController: controller,
itemExtent: 40,
squeeze: 1.1,
diameterRatio: 1.5,
backgroundColor: Colors.transparent,
selectionOverlay: CupertinoPickerDefaultSelectionOverlay(
background: cs.primary.withValues(alpha: 0.07),
),
onSelectedItemChanged: (i) => setState(() => onChanged(i)),
children: List.generate(
count,
(i) => Align(
alignment: align,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Text(
label(i),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: cs.onSurface, fontSize: 18),
),
),
),
),
),
);
}
}