|
|
|
|
@@ -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 }
|
|
|
|
|
|