feat: My Tasks widget, event countdown redesign, completed section
- My Tasks widget: today's tasks with interactive check-off (ToggleTaskIntent) and a "+" that opens the app's add-task sheet; writes preserve all task fields and sync via the App Group. - Event Countdown: AZURE/AWS-style header (accent name + countdown label + percent); tap the label to cycle days → weeks → time remaining. - Today: collapsible "Completed" archive section below Upcoming. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,14 @@ struct ContentView: View {
|
||||
|
||||
private var isIPad: Bool { UIDevice.current.userInterfaceIdiom == .pad }
|
||||
|
||||
/// Picks up the "+" tapped on the My Tasks widget and opens the add-task sheet.
|
||||
private func consumeWidgetAddTask() {
|
||||
guard UserDefaults.kisani.bool(forKey: "kisani.widget.openAddTask") else { return }
|
||||
UserDefaults.kisani.removeObject(forKey: "kisani.widget.openAddTask")
|
||||
selectedTab = 0
|
||||
showQuickAddTask = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isIPad {
|
||||
@@ -89,6 +97,7 @@ struct ContentView: View {
|
||||
CloudSyncManager.shared.backupSettings()
|
||||
HealthKitManager.shared.bootstrap()
|
||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||
consumeWidgetAddTask()
|
||||
// Pull workouts from Health, then reschedule so a detected workout updates reminders.
|
||||
Task {
|
||||
await workoutVM.syncFromHealthKit()
|
||||
@@ -98,6 +107,7 @@ struct ContentView: View {
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .active {
|
||||
taskVM.reload()
|
||||
consumeWidgetAddTask()
|
||||
workoutVM.checkPendingHealthConfirm()
|
||||
CloudSyncManager.shared.backupSettings()
|
||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||
|
||||
@@ -200,6 +200,12 @@ class TaskViewModel: ObservableObject {
|
||||
.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= startOfDay }
|
||||
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
||||
}
|
||||
/// All completed tasks, most-recent first (for the Completed archive section).
|
||||
var completedTasks: [TaskItem] {
|
||||
tasks
|
||||
.filter { $0.isComplete }
|
||||
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
||||
}
|
||||
|
||||
func toggle(_ task: TaskItem) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
|
||||
@@ -123,6 +123,16 @@ struct TodayView: View {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Completed (all, collapsible) ──
|
||||
if !taskVM.completedTasks.isEmpty {
|
||||
CompletedSection(
|
||||
tasks: taskVM.completedTasks,
|
||||
onUndo: { taskVM.toggle($0) }
|
||||
)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
|
||||
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
|
||||
|| !taskVM.upcomingTasks.isEmpty || !taskVM.completedTodayTasks.isEmpty
|
||||
|| !taskVM.next3DaysTasks.isEmpty || !todayCalEvents.isEmpty
|
||||
@@ -1009,6 +1019,74 @@ struct CompletedTodaySection: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Completed Section (all completed, collapsible archive)
|
||||
struct CompletedSection: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let tasks: [TaskItem]
|
||||
let onUndo: (TaskItem) -> Void
|
||||
|
||||
@State private var expanded = false
|
||||
|
||||
private let dateFmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "MMM d"; return f
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
HStack {
|
||||
Text("Completed")
|
||||
.font(AppFonts.sans(15, weight: .bold))
|
||||
.foregroundColor(AppColors.text2(cs))
|
||||
Spacer()
|
||||
Text("\(tasks.count)")
|
||||
.font(AppFonts.mono(11))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.rotationEffect(.degrees(expanded ? 0 : -90))
|
||||
.animation(KisaniSpring.micro, value: expanded)
|
||||
}
|
||||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { withAnimation(KisaniSpring.snappy) { expanded.toggle() } }
|
||||
|
||||
if expanded {
|
||||
ForEach(tasks) { task in
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
withAnimation(KisaniSpring.snappy) { onUndo(task) }
|
||||
} label: {
|
||||
Image(systemName: "checkmark.square.fill")
|
||||
.font(.system(size: 22))
|
||||
.foregroundColor(AppColors.text3(cs).opacity(0.6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text(task.title)
|
||||
.font(AppFonts.sans(14))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.strikethrough(true, color: AppColors.text3(cs).opacity(0.7))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 8)
|
||||
if let d = task.completedAt {
|
||||
Text(dateFmt.string(from: d))
|
||||
.font(AppFonts.sans(13))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14).padding(.vertical, 10)
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||||
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Task Row
|
||||
struct TaskRowView: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
|
||||
Reference in New Issue
Block a user