feat(09-01): rework frequency picker with shortcut chips and freeform picker
- Replace 10-chip grid + hidden Custom mode with 4 shortcut chips (Täglich, Wöchentlich, Alle 2 Wochen, Monatlich) - Always-visible freeform 'Alle [N] [Tage/Wochen/Monate]' picker row below chips - Bidirectional sync: tapping chip populates picker; editing picker recalculates chip highlight - _resolveFrequency() now reads exclusively from picker (single source of truth) - Edit mode correctly loads all 8 IntervalType values including quarterly and yearly - Add l10n keys frequencyShortcutDaily/Weekly/Biweekly/Monthly to app_de.arb
This commit is contained in:
@@ -32,11 +32,10 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _customIntervalController = TextEditingController(text: '2');
|
||||
final _customIntervalController = TextEditingController(text: '1');
|
||||
|
||||
FrequencyInterval? _selectedPreset;
|
||||
bool _isCustomFrequency = false;
|
||||
_CustomUnit _customUnit = _CustomUnit.days;
|
||||
_ShortcutFrequency? _activeShortcut;
|
||||
_CustomUnit _customUnit = _CustomUnit.weeks;
|
||||
EffortLevel _effortLevel = EffortLevel.medium;
|
||||
DateTime _dueDate = DateTime.now();
|
||||
Task? _existingTask;
|
||||
@@ -45,7 +44,9 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedPreset = FrequencyInterval.presets[3]; // Default: weekly
|
||||
_activeShortcut = _ShortcutFrequency.weekly;
|
||||
_customIntervalController.text = '1';
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_dueDate = _dateOnly(DateTime.now());
|
||||
if (widget.isEditing) {
|
||||
_loadExistingTask();
|
||||
@@ -65,38 +66,45 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
_effortLevel = task.effortLevel;
|
||||
_dueDate = task.nextDueDate;
|
||||
|
||||
// Find matching preset
|
||||
_selectedPreset = null;
|
||||
_isCustomFrequency = true;
|
||||
for (final preset in FrequencyInterval.presets) {
|
||||
if (preset.intervalType == task.intervalType &&
|
||||
preset.days == task.intervalDays) {
|
||||
_selectedPreset = preset;
|
||||
_isCustomFrequency = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isCustomFrequency) {
|
||||
// Determine custom unit from stored interval
|
||||
switch (task.intervalType) {
|
||||
case IntervalType.everyNMonths:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = task.intervalDays.toString();
|
||||
case IntervalType.monthly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.weekly:
|
||||
// Populate picker from stored interval
|
||||
switch (task.intervalType) {
|
||||
case IntervalType.daily:
|
||||
_customUnit = _CustomUnit.days;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.everyNDays:
|
||||
// Check if it's a clean week multiple
|
||||
if (task.intervalDays % 7 == 0) {
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.biweekly:
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = '2';
|
||||
default:
|
||||
_customIntervalController.text = (task.intervalDays ~/ 7).toString();
|
||||
} else {
|
||||
_customUnit = _CustomUnit.days;
|
||||
_customIntervalController.text = task.intervalDays.toString();
|
||||
}
|
||||
}
|
||||
case IntervalType.weekly:
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.biweekly:
|
||||
_customUnit = _CustomUnit.weeks;
|
||||
_customIntervalController.text = '2';
|
||||
case IntervalType.monthly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '1';
|
||||
case IntervalType.everyNMonths:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = task.intervalDays.toString();
|
||||
case IntervalType.quarterly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '3';
|
||||
case IntervalType.yearly:
|
||||
_customUnit = _CustomUnit.months;
|
||||
_customIntervalController.text = '12';
|
||||
}
|
||||
|
||||
// Detect matching shortcut chip
|
||||
_activeShortcut = _ShortcutFrequency.fromPickerValues(
|
||||
int.tryParse(_customIntervalController.text) ?? 1,
|
||||
_customUnit,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,59 +232,52 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
}
|
||||
|
||||
Widget _buildFrequencySelector(AppLocalizations l10n, ThemeData theme) {
|
||||
final items = <Widget>[];
|
||||
|
||||
// Preset intervals
|
||||
for (final preset in FrequencyInterval.presets) {
|
||||
items.add(
|
||||
ChoiceChip(
|
||||
label: Text(preset.label()),
|
||||
selected: !_isCustomFrequency && _selectedPreset == preset,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
setState(() {
|
||||
_selectedPreset = preset;
|
||||
_isCustomFrequency = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Custom option
|
||||
items.add(
|
||||
ChoiceChip(
|
||||
label: Text(l10n.taskFormFrequencyCustom),
|
||||
selected: _isCustomFrequency,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
setState(() {
|
||||
_isCustomFrequency = true;
|
||||
_selectedPreset = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Shortcut chips row (always visible)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: items,
|
||||
children: [
|
||||
for (final shortcut in _ShortcutFrequency.values)
|
||||
ChoiceChip(
|
||||
label: Text(_shortcutLabel(shortcut, l10n)),
|
||||
selected: _activeShortcut == shortcut,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
final values = shortcut.toPickerValues();
|
||||
setState(() {
|
||||
_activeShortcut = shortcut;
|
||||
_customIntervalController.text = values.number.toString();
|
||||
_customUnit = values.unit;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isCustomFrequency) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildCustomFrequencyInput(l10n, theme),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
// Freeform picker row (ALWAYS visible — not conditional)
|
||||
_buildFrequencyPickerRow(l10n, theme),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomFrequencyInput(AppLocalizations l10n, ThemeData theme) {
|
||||
String _shortcutLabel(_ShortcutFrequency shortcut, AppLocalizations l10n) {
|
||||
switch (shortcut) {
|
||||
case _ShortcutFrequency.daily:
|
||||
return l10n.frequencyShortcutDaily;
|
||||
case _ShortcutFrequency.weekly:
|
||||
return l10n.frequencyShortcutWeekly;
|
||||
case _ShortcutFrequency.biweekly:
|
||||
return l10n.frequencyShortcutBiweekly;
|
||||
case _ShortcutFrequency.monthly:
|
||||
return l10n.frequencyShortcutMonthly;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFrequencyPickerRow(AppLocalizations l10n, ThemeData theme) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
@@ -294,6 +295,14 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_activeShortcut = _ShortcutFrequency.fromPickerValues(
|
||||
int.tryParse(value) ?? 1,
|
||||
_customUnit,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -315,8 +324,13 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
],
|
||||
selected: {_customUnit},
|
||||
onSelectionChanged: (newSelection) {
|
||||
final newUnit = newSelection.first;
|
||||
setState(() {
|
||||
_customUnit = newSelection.first;
|
||||
_customUnit = newUnit;
|
||||
_activeShortcut = _ShortcutFrequency.fromPickerValues(
|
||||
int.tryParse(_customIntervalController.text) ?? 1,
|
||||
newUnit,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -374,53 +388,32 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the frequency from either selected preset or custom input.
|
||||
/// Resolve the frequency from the freeform picker (single source of truth).
|
||||
/// The picker is always the source of truth; shortcut chips just populate it.
|
||||
({IntervalType type, int days, int? anchorDay}) _resolveFrequency() {
|
||||
if (!_isCustomFrequency && _selectedPreset != null) {
|
||||
final preset = _selectedPreset!;
|
||||
// For calendar-anchored intervals, set anchorDay to due date's day
|
||||
int? anchorDay;
|
||||
if (_isCalendarAnchored(preset.intervalType)) {
|
||||
anchorDay = _dueDate.day;
|
||||
}
|
||||
return (
|
||||
type: preset.intervalType,
|
||||
days: preset.days,
|
||||
anchorDay: anchorDay,
|
||||
);
|
||||
}
|
||||
|
||||
// Custom frequency
|
||||
final number = int.tryParse(_customIntervalController.text) ?? 1;
|
||||
switch (_customUnit) {
|
||||
case _CustomUnit.days:
|
||||
return (
|
||||
type: IntervalType.everyNDays,
|
||||
days: number,
|
||||
anchorDay: null,
|
||||
);
|
||||
if (number == 1) {
|
||||
return (type: IntervalType.daily, days: 1, anchorDay: null);
|
||||
}
|
||||
return (type: IntervalType.everyNDays, days: number, anchorDay: null);
|
||||
case _CustomUnit.weeks:
|
||||
return (
|
||||
type: IntervalType.everyNDays,
|
||||
days: number * 7,
|
||||
anchorDay: null,
|
||||
);
|
||||
if (number == 1) {
|
||||
return (type: IntervalType.weekly, days: 1, anchorDay: null);
|
||||
}
|
||||
if (number == 2) {
|
||||
return (type: IntervalType.biweekly, days: 14, anchorDay: null);
|
||||
}
|
||||
return (type: IntervalType.everyNDays, days: number * 7, anchorDay: null);
|
||||
case _CustomUnit.months:
|
||||
return (
|
||||
type: IntervalType.everyNMonths,
|
||||
days: number,
|
||||
anchorDay: _dueDate.day,
|
||||
);
|
||||
if (number == 1) {
|
||||
return (type: IntervalType.monthly, days: 1, anchorDay: _dueDate.day);
|
||||
}
|
||||
return (type: IntervalType.everyNMonths, days: number, anchorDay: _dueDate.day);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isCalendarAnchored(IntervalType type) {
|
||||
return type == IntervalType.monthly ||
|
||||
type == IntervalType.everyNMonths ||
|
||||
type == IntervalType.quarterly ||
|
||||
type == IntervalType.yearly;
|
||||
}
|
||||
|
||||
Future<void> _onSave() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
@@ -507,5 +500,36 @@ class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unit options for custom frequency input.
|
||||
/// Shortcut frequency options for quick selection chips.
|
||||
enum _ShortcutFrequency {
|
||||
daily,
|
||||
weekly,
|
||||
biweekly,
|
||||
monthly;
|
||||
|
||||
/// Returns the picker values (number + unit) that this shortcut represents.
|
||||
({int number, _CustomUnit unit}) toPickerValues() {
|
||||
switch (this) {
|
||||
case _ShortcutFrequency.daily:
|
||||
return (number: 1, unit: _CustomUnit.days);
|
||||
case _ShortcutFrequency.weekly:
|
||||
return (number: 1, unit: _CustomUnit.weeks);
|
||||
case _ShortcutFrequency.biweekly:
|
||||
return (number: 2, unit: _CustomUnit.weeks);
|
||||
case _ShortcutFrequency.monthly:
|
||||
return (number: 1, unit: _CustomUnit.months);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the matching shortcut for given picker values, or null if no match.
|
||||
static _ShortcutFrequency? fromPickerValues(int number, _CustomUnit unit) {
|
||||
if (number == 1 && unit == _CustomUnit.days) return _ShortcutFrequency.daily;
|
||||
if (number == 1 && unit == _CustomUnit.weeks) return _ShortcutFrequency.weekly;
|
||||
if (number == 2 && unit == _CustomUnit.weeks) return _ShortcutFrequency.biweekly;
|
||||
if (number == 1 && unit == _CustomUnit.months) return _ShortcutFrequency.monthly;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unit options for freeform frequency picker.
|
||||
enum _CustomUnit { days, weeks, months }
|
||||
|
||||
Reference in New Issue
Block a user