feat: floating tab bar, NLP date parsing, and task date picker

- Tab bar: TickTick-style floating pill (cornerRadius 28, shadow, systemGray5 active highlight)
- NLP: month+day parsing (8th June, June 8), recurrence (every year/week/etc), intent stripping (remind me of)
- Recurrence chip in add-task toolbar; isRecurring saved with task
- Date chip defaults to Today on new tasks; tapping opens full date picker
- Date picker: calendar grid, inline time wheel, Repeat menu (Date tab)
- Duration tab: Start/End column selector, live duration counter, All Day toggle
- TaskItem: added endDate and isAllDay fields
- Fixed hidden Unicode character corrupting + operator in TodayView

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
Robin Kutesa
2026-05-30 23:49:30 +03:00
parent a11258dea5
commit 68efa02803
6 changed files with 1601 additions and 300 deletions

View File

@@ -12,6 +12,9 @@ struct TaskItem: Identifiable, Codable, Equatable {
var category: TaskCategory = .reminder
var taskColor: TaskColor = .orange
var isRecurring: Bool = false
var isPinned: Bool = false
var endDate: Date? = nil
var isAllDay: Bool = false
}
enum Quadrant: String, CaseIterable, Identifiable, Codable {
@@ -84,12 +87,26 @@ class TaskViewModel: ObservableObject {
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
}
var upcomingTasks: [TaskItem] {
let tomorrow = Calendar.current.date(byAdding: .day, value: 1,
to: Calendar.current.startOfDay(for: Date()))!
let cal = Calendar.current
let fourDays = cal.date(byAdding: .day, value: 4, to: cal.startOfDay(for: Date()))!
return tasks
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= tomorrow }
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= fourDays }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
}
var next3DaysTasks: [(date: Date, tasks: [TaskItem])] {
let cal = Calendar.current
let today = cal.startOfDay(for: Date())
return (1...3).compactMap { offset -> (Date, [TaskItem])? in
guard let day = cal.date(byAdding: .day, value: offset, to: today),
let nextDay = cal.date(byAdding: .day, value: 1, to: day) else { return nil }
let dayTasks = tasks.filter {
!$0.isComplete &&
($0.dueDate ?? .distantFuture) >= day &&
($0.dueDate ?? .distantFuture) < nextDay
}.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
return dayTasks.isEmpty ? nil : (day, dayTasks)
}
}
var completedTodayTasks: [TaskItem] {
let startOfDay = Calendar.current.startOfDay(for: Date())
return tasks
@@ -104,15 +121,42 @@ class TaskViewModel: ObservableObject {
save()
}
func tasks(for quadrant: Quadrant) -> [TaskItem] {
tasks.filter { $0.quadrant == quadrant }
tasks
.filter { $0.quadrant == quadrant }
.sorted { $0.isPinned && !$1.isPinned }
}
func pin(_ task: TaskItem) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[idx].isPinned.toggle()
save()
}
func setDate(_ task: TaskItem, date: Date?) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[idx].dueDate = date
save()
}
func setCategory(_ task: TaskItem, category: TaskCategory) {
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[idx].category = category
save()
}
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
tasks[idx].quadrant = quadrant
save()
}
func addTask(title: String, dueDate: Date?, hasTime: Bool = false,
quadrant: Quadrant = .urgent, category: TaskCategory = .reminder,
taskColor: TaskColor = .orange, isRecurring: Bool = false) {
taskColor: TaskColor = .orange, isRecurring: Bool = false,
endDate: Date? = nil, isAllDay: Bool = false) {
tasks.append(TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
quadrant: quadrant, category: category,
taskColor: taskColor, isRecurring: isRecurring))
taskColor: taskColor, isRecurring: isRecurring,
endDate: endDate, isAllDay: isAllDay))
save()
}