feat: redesign home screen date selector with navigation controls
Replace static 7-day ListView with a scrollable strip (365 days back,
today at the right edge) and a header row with:
- chevron buttons to step one day at a time
- selected date label ("Вт, 18 марта")
- "Сегодня" shortcut that appears when a past date is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,7 +135,7 @@ class _AppBar extends StatelessWidget {
|
|||||||
|
|
||||||
// ── Date selector ─────────────────────────────────────────────
|
// ── Date selector ─────────────────────────────────────────────
|
||||||
|
|
||||||
class _DateSelector extends StatelessWidget {
|
class _DateSelector extends StatefulWidget {
|
||||||
final DateTime selectedDate;
|
final DateTime selectedDate;
|
||||||
final ValueChanged<DateTime> onDateSelected;
|
final ValueChanged<DateTime> onDateSelected;
|
||||||
|
|
||||||
@@ -144,7 +144,101 @@ class _DateSelector extends StatelessWidget {
|
|||||||
required this.onDateSelected,
|
required this.onDateSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DateSelector> createState() => _DateSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateSelectorState extends State<_DateSelector> {
|
||||||
static const _weekDayShort = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
static const _weekDayShort = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||||||
|
static const _monthNames = [
|
||||||
|
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||||
|
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря',
|
||||||
|
];
|
||||||
|
// Total days available in the past (index 0 = today, index N-1 = oldest)
|
||||||
|
static const _totalDays = 365;
|
||||||
|
static const _pillWidth = 48.0;
|
||||||
|
static const _pillSpacing = 6.0;
|
||||||
|
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
|
||||||
|
String _formatSelectedDate(DateTime date) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final dayName = _weekDayShort[date.weekday - 1];
|
||||||
|
final month = _monthNames[date.month - 1];
|
||||||
|
final yearSuffix = date.year != now.year ? ' ${date.year}' : '';
|
||||||
|
return '$dayName, ${date.day} $month$yearSuffix';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index in the reversed list: 0 = today, 1 = yesterday, …
|
||||||
|
int _indexForDate(DateTime date) {
|
||||||
|
final today = DateTime.now();
|
||||||
|
final todayNormalized = DateTime(today.year, today.month, today.day);
|
||||||
|
final dateNormalized = DateTime(date.year, date.month, date.day);
|
||||||
|
return todayNormalized.difference(dateNormalized).inDays.clamp(0, _totalDays - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _offsetForIndex(int index) => index * (_pillWidth + _pillSpacing);
|
||||||
|
|
||||||
|
void _selectPreviousDay() {
|
||||||
|
final previousDay = widget.selectedDate.subtract(const Duration(days: 1));
|
||||||
|
final previousDayNormalized =
|
||||||
|
DateTime(previousDay.year, previousDay.month, previousDay.day);
|
||||||
|
final today = DateTime.now();
|
||||||
|
final oldestAllowed = DateTime(today.year, today.month, today.day)
|
||||||
|
.subtract(const Duration(days: _totalDays - 1));
|
||||||
|
if (!previousDayNormalized.isBefore(oldestAllowed)) {
|
||||||
|
widget.onDateSelected(previousDayNormalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectNextDay() {
|
||||||
|
final today = DateTime.now();
|
||||||
|
final todayNormalized = DateTime(today.year, today.month, today.day);
|
||||||
|
final nextDay = widget.selectedDate.add(const Duration(days: 1));
|
||||||
|
final nextDayNormalized =
|
||||||
|
DateTime(nextDay.year, nextDay.month, nextDay.day);
|
||||||
|
if (!nextDayNormalized.isAfter(todayNormalized)) {
|
||||||
|
widget.onDateSelected(nextDayNormalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _jumpToToday() {
|
||||||
|
final today = DateTime.now();
|
||||||
|
widget.onDateSelected(DateTime(today.year, today.month, today.day));
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController = ScrollController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_DateSelector oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final oldIndex = _indexForDate(oldWidget.selectedDate);
|
||||||
|
final newIndex = _indexForDate(widget.selectedDate);
|
||||||
|
if (oldIndex != newIndex && _scrollController.hasClients) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_offsetForIndex(newIndex),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -152,24 +246,75 @@ class _DateSelector extends StatelessWidget {
|
|||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
final todayNormalized = DateTime(today.year, today.month, today.day);
|
final todayNormalized = DateTime(today.year, today.month, today.day);
|
||||||
final selectedNormalized = DateTime(
|
final selectedNormalized = DateTime(
|
||||||
selectedDate.year, selectedDate.month, selectedDate.day);
|
widget.selectedDate.year, widget.selectedDate.month, widget.selectedDate.day);
|
||||||
|
final isToday = selectedNormalized == todayNormalized;
|
||||||
|
|
||||||
return SizedBox(
|
return Column(
|
||||||
height: 64,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// ── Header row ──────────────────────────────────────────
|
||||||
|
SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_left),
|
||||||
|
iconSize: 20,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: _selectPreviousDay,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_formatSelectedDate(widget.selectedDate),
|
||||||
|
style: theme.textTheme.labelMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isToday)
|
||||||
|
TextButton(
|
||||||
|
onPressed: _jumpToToday,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Сегодня',
|
||||||
|
style: theme.textTheme.labelMedium
|
||||||
|
?.copyWith(color: theme.colorScheme.primary),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
iconSize: 20,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// ── Day strip ────────────────────────────────────────────
|
||||||
|
SizedBox(
|
||||||
|
height: 56,
|
||||||
|
// reverse: true → index 0 (today) sits at the right edge
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
controller: _scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: 7,
|
reverse: true,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
itemCount: _totalDays,
|
||||||
itemBuilder: (context, index) {
|
separatorBuilder: (_, __) => const SizedBox(width: _pillSpacing),
|
||||||
final date = todayNormalized.subtract(Duration(days: 6 - index));
|
itemBuilder: (listContext, index) {
|
||||||
|
final date = todayNormalized.subtract(Duration(days: index));
|
||||||
final isSelected = date == selectedNormalized;
|
final isSelected = date == selectedNormalized;
|
||||||
final isToday = date == todayNormalized;
|
final isDayToday = date == todayNormalized;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => onDateSelected(date),
|
onTap: () => widget.onDateSelected(date),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
width: 44,
|
width: _pillWidth,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? theme.colorScheme.primary
|
? theme.colorScheme.primary
|
||||||
@@ -191,11 +336,12 @@ class _DateSelector extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'${date.day}',
|
'${date.day}',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight:
|
fontWeight: isSelected || isDayToday
|
||||||
isSelected || isToday ? FontWeight.w700 : FontWeight.normal,
|
? FontWeight.w700
|
||||||
|
: FontWeight.normal,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? theme.colorScheme.onPrimary
|
? theme.colorScheme.onPrimary
|
||||||
: isToday
|
: isDayToday
|
||||||
? theme.colorScheme.primary
|
? theme.colorScheme.primary
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -206,6 +352,8 @@ class _DateSelector extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,10 +125,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -668,26 +668,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -993,10 +993,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.10"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user