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
This commit is contained in:
parent
da873afae0
commit
5cd79e096d
@ -2,7 +2,6 @@ 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/entities/task_entity.dart';
|
||||
@ -35,7 +34,6 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
final repository = getIt<TaskRepository>();
|
||||
final Map<String, List<TaskEntity>> tasks = {};
|
||||
|
||||
final firstDay = DateTime(_focusedMonth.year, _focusedMonth.month, 1);
|
||||
final lastDay = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0);
|
||||
|
||||
for (int day = 1; day <= lastDay.day; day++) {
|
||||
@ -60,19 +58,24 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Priority? _getHighestPriority(List<TaskEntity>? tasks) {
|
||||
if (tasks == null || tasks.isEmpty) return null;
|
||||
List<Priority> _getUniquePriorities(List<TaskEntity>? tasks) {
|
||||
if (tasks == null || tasks.isEmpty) return [];
|
||||
|
||||
final incompleteTasks = tasks.where((t) => !t.isDone).toList();
|
||||
if (incompleteTasks.isEmpty) return null;
|
||||
if (incompleteTasks.isEmpty) return [];
|
||||
|
||||
// Get unique priorities, sorted by importance (high first)
|
||||
final priorities = <Priority>[];
|
||||
if (incompleteTasks.any((t) => t.priority == Priority.high)) {
|
||||
return Priority.high;
|
||||
priorities.add(Priority.high);
|
||||
}
|
||||
if (incompleteTasks.any((t) => t.priority == Priority.medium)) {
|
||||
return Priority.medium;
|
||||
priorities.add(Priority.medium);
|
||||
}
|
||||
return Priority.low;
|
||||
if (incompleteTasks.any((t) => t.priority == Priority.low)) {
|
||||
priorities.add(Priority.low);
|
||||
}
|
||||
return priorities;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -114,7 +117,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
focusedMonth: _focusedMonth,
|
||||
selectedDate: _selectedDate,
|
||||
tasksByDate: _tasksByDate,
|
||||
getHighestPriority: _getHighestPriority,
|
||||
getUniquePriorities: _getUniquePriorities,
|
||||
onDateSelected: (date) {
|
||||
setState(() => _selectedDate = date);
|
||||
},
|
||||
@ -126,12 +129,15 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
? SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(_selectedDate!);
|
||||
context.go('/?date=$dateStr');
|
||||
},
|
||||
child: Text(l10n.goToDay),
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(_selectedDate!);
|
||||
context.go('/?date=$dateStr');
|
||||
},
|
||||
child: Text(l10n.goToDay),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -158,21 +164,29 @@ class _MonthNavigation extends StatelessWidget {
|
||||
final monthFormat = DateFormat.yMMMM(locale);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: onPrevious,
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.chevron_left, size: 28),
|
||||
onPressed: onPrevious,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
monthFormat.format(month),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: onNext,
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.chevron_right, size: 28),
|
||||
onPressed: onNext,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -216,14 +230,14 @@ class _CalendarGrid extends StatelessWidget {
|
||||
final DateTime focusedMonth;
|
||||
final DateTime? selectedDate;
|
||||
final Map<String, List<TaskEntity>> tasksByDate;
|
||||
final Priority? Function(List<TaskEntity>?) getHighestPriority;
|
||||
final List<Priority> Function(List<TaskEntity>?) getUniquePriorities;
|
||||
final ValueChanged<DateTime> onDateSelected;
|
||||
|
||||
const _CalendarGrid({
|
||||
required this.focusedMonth,
|
||||
required this.selectedDate,
|
||||
required this.tasksByDate,
|
||||
required this.getHighestPriority,
|
||||
required this.getUniquePriorities,
|
||||
required this.onDateSelected,
|
||||
});
|
||||
|
||||
@ -237,10 +251,12 @@ class _CalendarGrid extends StatelessWidget {
|
||||
final today = DateTime.now();
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 7,
|
||||
childAspectRatio: 1,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
),
|
||||
itemCount: rowCount * 7,
|
||||
itemBuilder: (context, index) {
|
||||
@ -260,12 +276,11 @@ class _CalendarGrid extends StatelessWidget {
|
||||
date.day == selectedDate!.day;
|
||||
|
||||
final tasks = tasksByDate[dateStr];
|
||||
final highestPriority = getHighestPriority(tasks);
|
||||
final priorities = getUniquePriorities(tasks);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onDateSelected(date),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
@ -288,10 +303,10 @@ class _CalendarGrid extends StatelessWidget {
|
||||
fontWeight: isToday || isSelected ? FontWeight.bold : null,
|
||||
),
|
||||
),
|
||||
if (highestPriority != null)
|
||||
if (priorities.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
child: _PulsingDot(priority: highestPriority),
|
||||
bottom: 6,
|
||||
child: _PriorityDots(priorities: priorities),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -302,6 +317,25 @@ class _CalendarGrid extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PriorityDots extends StatelessWidget {
|
||||
final List<Priority> priorities;
|
||||
|
||||
const _PriorityDots({required this.priorities});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: priorities
|
||||
.map((priority) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: _PulsingDot(priority: priority),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PulsingDot extends StatefulWidget {
|
||||
final Priority priority;
|
||||
|
||||
@ -354,15 +388,15 @@ class _PulsingDotState extends State<_PulsingDot>
|
||||
return Opacity(
|
||||
opacity: _animation.value,
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: _getPriorityColor(),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _getPriorityColor().withOpacity(0.5),
|
||||
blurRadius: 2,
|
||||
color: _getPriorityColor().withValues(alpha: 0.5),
|
||||
blurRadius: 3,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
|
||||
@ -79,9 +79,12 @@ class _DailyAgendaView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(vm.selectedDate);
|
||||
context.push('/task/new?date=$dateStr');
|
||||
final result = await context.push('/task/new?date=$dateStr');
|
||||
if (result == true) {
|
||||
vm.loadTasks();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
@ -140,7 +143,12 @@ class _DailyAgendaView extends StatelessWidget {
|
||||
return TaskTile(
|
||||
task: task,
|
||||
onToggle: () => vm.toggleTask(task.id),
|
||||
onTap: () => context.push('/task/${task.id}/edit'),
|
||||
onTap: () async {
|
||||
final result = await context.push('/task/${task.id}/edit');
|
||||
if (result == true) {
|
||||
vm.loadTasks();
|
||||
}
|
||||
},
|
||||
onDelete: () => vm.deleteTask(task.id),
|
||||
onReschedule: () => vm.rescheduleToTomorrow(task.id),
|
||||
);
|
||||
@ -169,13 +177,17 @@ class _DateNavigation extends StatelessWidget {
|
||||
final dateFormat = DateFormat.yMMMMEEEEd(locale);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: onPrevious,
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.chevron_left, size: 28),
|
||||
onPressed: onPrevious,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@ -184,9 +196,13 @@ class _DateNavigation extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: onNext,
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.chevron_right, size: 28),
|
||||
onPressed: onNext,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -99,52 +99,59 @@ class _TaskFormView extends StatelessWidget {
|
||||
onChanged: vm.setDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: Text(l10n.date),
|
||||
subtitle: Text(DateFormat.yMMMd(locale).format(vm.date)),
|
||||
trailing: const Icon(Icons.calendar_today),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
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: 16),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.priority,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
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);
|
||||
},
|
||||
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),
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/logging/app_logger.dart';
|
||||
import '../../domain/entities/task_entity.dart';
|
||||
import '../../domain/enums/priority.dart';
|
||||
import '../../domain/repositories/task_repository.dart';
|
||||
|
||||
enum TasksStatus { initial, loading, success, error }
|
||||
@ -31,13 +32,31 @@ class DailyTasksViewModel extends ChangeNotifier {
|
||||
TaskFilter get filter => _filter;
|
||||
|
||||
List<TaskEntity> get _filteredTasks {
|
||||
List<TaskEntity> filtered;
|
||||
switch (_filter) {
|
||||
case TaskFilter.all:
|
||||
return _tasks;
|
||||
filtered = List.from(_tasks);
|
||||
break;
|
||||
case TaskFilter.active:
|
||||
return _tasks.where((t) => !t.isDone).toList();
|
||||
filtered = _tasks.where((t) => !t.isDone).toList();
|
||||
break;
|
||||
case TaskFilter.completed:
|
||||
return _tasks.where((t) => t.isDone).toList();
|
||||
filtered = _tasks.where((t) => t.isDone).toList();
|
||||
break;
|
||||
}
|
||||
// Sort by priority: high first, then medium, then low
|
||||
filtered.sort((a, b) => _priorityOrder(a.priority).compareTo(_priorityOrder(b.priority)));
|
||||
return filtered;
|
||||
}
|
||||
|
||||
int _priorityOrder(Priority priority) {
|
||||
switch (priority) {
|
||||
case Priority.high:
|
||||
return 0;
|
||||
case Priority.medium:
|
||||
return 1;
|
||||
case Priority.low:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -26,18 +26,21 @@ class FilterChips extends StatelessWidget {
|
||||
label: Text(l10n.filterAll),
|
||||
selected: currentFilter == TaskFilter.all,
|
||||
onSelected: (_) => onFilterChanged(TaskFilter.all),
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilterChip(
|
||||
label: Text(l10n.filterActive),
|
||||
selected: currentFilter == TaskFilter.active,
|
||||
onSelected: (_) => onFilterChanged(TaskFilter.active),
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilterChip(
|
||||
label: Text(l10n.filterCompleted),
|
||||
selected: currentFilter == TaskFilter.completed,
|
||||
onSelected: (_) => onFilterChanged(TaskFilter.completed),
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -69,28 +69,33 @@ class TaskTile extends StatelessWidget {
|
||||
},
|
||||
onDismissed: (_) => onDelete(),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: priorityColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 72),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 5,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: priorityColor,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Checkbox(
|
||||
value: task.isDone,
|
||||
onChanged: (_) => onToggle(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Checkbox(
|
||||
value: task.isDone,
|
||||
onChanged: (_) => onToggle(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -118,8 +123,12 @@ class TaskTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: (value) async {
|
||||
if (value == 'reschedule') {
|
||||
onReschedule();
|
||||
} else if (value == 'delete') {
|
||||
@ -145,33 +154,37 @@ class TaskTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'reschedule',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.schedule),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.rescheduleToTomorrow),
|
||||
],
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
height: 56,
|
||||
value: 'reschedule',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.schedule, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Text(l10n.rescheduleToTomorrow),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.delete, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
],
|
||||
PopupMenuItem(
|
||||
height: 56,
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 24, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(width: 12),
|
||||
Text(l10n.delete, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user