m3mo 5cd79e096d Improve touch targets with 48dp minimum size for mobile
- Enlarge calendar priority dots to 10px with pulsing animation
- Increase task tile checkbox and menu button containers to 48x48
- Add proper minVerticalPadding to task tiles (72dp height)
- Update filter chips with MaterialTapTargetSize.padded
- Increase navigation buttons to 48x48 with 28px icons
- Update task form with 48dp height segmented button
2026-02-02 22:57:17 +01:00

178 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:agenda_tasks/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../../../core/di/injection_container.dart';
import '../../domain/enums/priority.dart';
import '../viewmodels/task_form_viewmodel.dart';
class TaskFormPage extends StatelessWidget {
final String? taskId;
final String? initialDate;
const TaskFormPage({
super.key,
this.taskId,
this.initialDate,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) {
final vm = getIt<TaskFormViewModel>();
if (taskId != null) {
vm.initForEdit(taskId!);
} else {
final date = initialDate != null
? DateTime.tryParse(initialDate!)
: null;
vm.initForCreate(date);
}
return vm;
},
child: const _TaskFormView(),
);
}
}
class _TaskFormView extends StatelessWidget {
const _TaskFormView();
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final vm = context.watch<TaskFormViewModel>();
final locale = Localizations.localeOf(context).languageCode;
return Scaffold(
appBar: AppBar(
title: Text(vm.isEditing ? l10n.editTask : l10n.newTask),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => context.pop(),
),
actions: [
TextButton(
onPressed: vm.status == FormStatus.loading
? null
: () async {
final success = await vm.save();
if (success && context.mounted) {
context.pop(true);
}
},
child: Text(l10n.save),
),
],
),
body: vm.status == FormStatus.loading && vm.isEditing
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
decoration: InputDecoration(
labelText: '${l10n.title} *',
errorText: vm.titleError != null
? l10n.titleRequired
: null,
border: const OutlineInputBorder(),
),
controller: TextEditingController(text: vm.title)
..selection = TextSelection.collapsed(offset: vm.title.length),
onChanged: vm.setTitle,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
TextField(
decoration: InputDecoration(
labelText: l10n.description,
border: const OutlineInputBorder(),
),
controller: TextEditingController(text: vm.description)
..selection = TextSelection.collapsed(offset: vm.description.length),
onChanged: vm.setDescription,
maxLines: 3,
),
const SizedBox(height: 20),
ConstrainedBox(
constraints: const BoxConstraints(minHeight: 72),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(l10n.date),
subtitle: Text(DateFormat.yMMMd(locale).format(vm.date)),
trailing: const Icon(Icons.calendar_today, size: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Theme.of(context).colorScheme.outline),
),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: vm.date,
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null) {
vm.setDate(picked);
}
},
),
),
const SizedBox(height: 20),
Text(
l10n.priority,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 12),
SizedBox(
height: 48,
child: SegmentedButton<Priority>(
segments: [
ButtonSegment(
value: Priority.low,
label: Text(l10n.priorityLow),
),
ButtonSegment(
value: Priority.medium,
label: Text(l10n.priorityMedium),
),
ButtonSegment(
value: Priority.high,
label: Text(l10n.priorityHigh),
),
],
selected: {vm.priority},
onSelectionChanged: (selection) {
vm.setPriority(selection.first);
},
),
),
if (vm.status == FormStatus.error && vm.failure != null) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
vm.failure!.message,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
],
),
),
);
}
}