fix: unify date limits, fix ISO week calculation, refactor home screen plan button
- Fix _isoWeek: correct Sunday shift (-3 instead of +4), use floor+1 formula, match jan1 timezone to input — planned meals now appear correctly for UTC+ users - Add kPlanningHorizonDays=28 / kMenuPastWeeks=8 constants; apply to home date strip, plan picker (strip + calendar), and menu screen prev/next navigation - Menu screen week nav: disable arrows at min/max limits using compareTo - Home screen: replace _GenerateActionCard/_WeekPlannedChip conditional with always-visible _FutureDayPlanButton(dateString); show _DayPlannedChip only when the specific day has planned meals; remove standalone _PlanMenuButton - _FutureDayPlanButton uses selected date as defaultStart instead of lastPlanned+1 - Rename weekPlannedLabel -> dayPlannedLabel across all 12 locales Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import '../../core/storage/local_preferences_provider.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../shared/models/diary_entry.dart';
|
||||
import '../../shared/models/home_summary.dart';
|
||||
import '../../shared/constants/date_limits.dart';
|
||||
import '../../shared/models/meal_type.dart';
|
||||
import '../../shared/models/menu.dart';
|
||||
import '../diary/food_search_sheet.dart';
|
||||
@@ -128,8 +129,6 @@ class HomeScreen extends ConsumerWidget {
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_QuickActionsRow(),
|
||||
const SizedBox(height: 8),
|
||||
_PlanMenuButton(),
|
||||
if (!isFutureDate && recommendations.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
_SectionTitle(l10n.recommendCook),
|
||||
@@ -191,10 +190,10 @@ class _DateSelector extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DateSelectorState extends State<_DateSelector> {
|
||||
// Strip covers 7 future days + today + 364 past days = 372 items total.
|
||||
// Strip covers kPlanningHorizonDays future days + today + 364 past days.
|
||||
// With reverse: true, index 0 is rendered at the RIGHT edge (newest).
|
||||
// index 0 = today + 7, index 7 = today, index 371 = today - 364.
|
||||
static const _futureDays = 7;
|
||||
// index 0 = today + kPlanningHorizonDays, index kPlanningHorizonDays = today.
|
||||
static const _futureDays = kPlanningHorizonDays;
|
||||
static const _pastDays = 364;
|
||||
static const _totalDays = _futureDays + 1 + _pastDays; // 372
|
||||
static const _pillWidth = 48.0;
|
||||
@@ -1156,125 +1155,26 @@ class _FutureDayHeader extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final date = DateTime.parse(dateString);
|
||||
final weekString = isoWeekString(date);
|
||||
final menuState = ref.watch(menuProvider(weekString));
|
||||
final plannedMeals = ref.watch(plannedMealsProvider(dateString));
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_PlanningBanner(dateString: dateString),
|
||||
const SizedBox(height: 8),
|
||||
menuState.when(
|
||||
loading: () => _GenerateLoadingCard(l10n: l10n),
|
||||
error: (_, __) => _GenerateActionCard(
|
||||
l10n: l10n,
|
||||
onGenerate: () =>
|
||||
ref.read(menuProvider(weekString).notifier).generate(),
|
||||
),
|
||||
data: (plan) => plan == null
|
||||
? _GenerateActionCard(
|
||||
l10n: l10n,
|
||||
onGenerate: () =>
|
||||
ref.read(menuProvider(weekString).notifier).generate(),
|
||||
)
|
||||
: _WeekPlannedChip(l10n: l10n),
|
||||
),
|
||||
_FutureDayPlanButton(dateString: dateString),
|
||||
if (plannedMeals.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_DayPlannedChip(l10n: l10n),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GenerateActionCard extends StatelessWidget {
|
||||
class _DayPlannedChip extends StatelessWidget {
|
||||
final AppLocalizations l10n;
|
||||
final VoidCallback onGenerate;
|
||||
const _GenerateActionCard({required this.l10n, required this.onGenerate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.auto_awesome,
|
||||
color: theme.colorScheme.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.generateWeekLabel,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.generateWeekSubtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: onGenerate,
|
||||
child: Text(l10n.generateWeekLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GenerateLoadingCard extends StatelessWidget {
|
||||
final AppLocalizations l10n;
|
||||
const _GenerateLoadingCard({required this.l10n});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.generatingMenu,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WeekPlannedChip extends StatelessWidget {
|
||||
final AppLocalizations l10n;
|
||||
const _WeekPlannedChip({required this.l10n});
|
||||
const _DayPlannedChip({required this.l10n});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1292,7 +1192,7 @@ class _WeekPlannedChip extends StatelessWidget {
|
||||
color: theme.colorScheme.onSecondaryContainer, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.weekPlannedLabel,
|
||||
l10n.dayPlannedLabel,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -1647,8 +1547,9 @@ class _ActionButton extends StatelessWidget {
|
||||
|
||||
// ── Plan menu button ──────────────────────────────────────────
|
||||
|
||||
class _PlanMenuButton extends ConsumerWidget {
|
||||
const _PlanMenuButton();
|
||||
class _FutureDayPlanButton extends ConsumerWidget {
|
||||
final String dateString;
|
||||
const _FutureDayPlanButton({required this.dateString});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -1682,16 +1583,13 @@ class _PlanMenuButton extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _openPlanSheet(BuildContext context, WidgetRef ref) {
|
||||
final defaultStart = DateTime.parse(dateString);
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (_) => PlanMenuSheet(
|
||||
onModeSelected: (mode) {
|
||||
final lastPlanned = ref.read(lastPlannedDateProvider);
|
||||
final defaultStart = lastPlanned != null
|
||||
? lastPlanned.add(const Duration(days: 1))
|
||||
: DateTime.now().add(const Duration(days: 1));
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
||||
Reference in New Issue
Block a user