Today menu features, Matrix Move trim, widget auto-advance, Wenza logo (KC-23–26)
- Today ⋯ menu: implemented the placeholder actions.
- Background: 5 selectable themes (Default/Warm/Cool/Mono/Paper), persisted.
- Group & Sort: group by Date (default)/Priority/Category/Quadrant and sort by
Smart/Due/Priority/Title; non-date groupings render via UpcomingSection.
- Select: multi-select sheet with Select All/Deselect All and bulk
Complete/Priority/Move/Delete.
New isolated file TodayMenuFeatures.swift; added TaskViewModel.allActiveRows.
- Matrix "Move" submenu now lists only the other three quadrants (excludes the
current one) using clear matrix labels.
- Event Countdown (Bars) widget auto-advances: each timeline entry re-resolves
"nearest to finish" as of its own time and refreshes at the moment the current
event expires, so it rolls to the next event on expiry/completion.
- Splash wordmark rebranded to "wenza." (regular-weight typewriter + orange dot).
- ISSUES.md: logged KC-23 (Pick Date + time-based reminders), KC-24 (Wenza
rebrand), KC-25 (Move trim), KC-26 (widget auto-advance).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -751,8 +751,126 @@ Files: `TaskContextMenu.swift`, `TaskItem.swift`, `TodayView.swift`,
|
||||
### How to test
|
||||
1. Long-press a task in Today → Postpone → 15 Minutes; reminder/due time moves.
|
||||
2. Repeat from Matrix grid, Matrix detail, and Calendar day timeline.
|
||||
3. Open Move from the same menu → all four quadrants are visible, current one checked.
|
||||
3. Open Move from the same menu → only the other three quadrants are listed (see KC-25).
|
||||
4. Trigger a task reminder notification on device → verify 15/30/60/120 minute
|
||||
lock-screen actions appear.
|
||||
5. Tap a lock-screen snooze action → task reminder updates and notification
|
||||
re-delivers after the selected interval.
|
||||
|
||||
---
|
||||
|
||||
## KC-23 — "Pick Date" everywhere + sensible time-based reminders
|
||||
|
||||
**Status:** In Progress (implemented & build-verified, pending device QA)
|
||||
**Reported by:** User
|
||||
**Area:** Tasks / Matrix / Calendar / Reminders
|
||||
|
||||
### Problem
|
||||
The Date submenu offered "Pick Date" only in the Today view — the Matrix grid
|
||||
cards and the Calendar reschedule/move menu were missing it. Separately, the
|
||||
reminder lead-time options were day/week based ("2 days early", "1 week early"),
|
||||
which is unrealistic for timed events; the user wanted the standard time-based
|
||||
set.
|
||||
|
||||
### Fix
|
||||
- "Pick Date" (and Edit) now appear in the Matrix grid cards and the Calendar day
|
||||
timeline menu — both open the shared `TaskEditSheet` via a new `editingTask`
|
||||
sheet (`QuadrantCard` gained an `onEdit`; Calendar passes `onEdit` into the
|
||||
task menu).
|
||||
- Reworked the reminder picker (`TaskDatePickerSheet`, shared by every entry
|
||||
point) from day-offsets to **minutes before the event time**: None, On time,
|
||||
5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom wheel. Existing
|
||||
absolute `reminderDate`s migrate to the nearest minute offset on open.
|
||||
|
||||
Files: `MatrixView.swift`, `CalendarView.swift`, `TodayView.swift`.
|
||||
|
||||
### How to test
|
||||
1. Long-press a task in Matrix and in Calendar → Date → "Pick Date" is present.
|
||||
2. Create/edit a task → reminder options read On time / 5 min / 30 min / 1 hour /
|
||||
1 day / Custom, relative to the event time.
|
||||
3. Custom → Minutes/Hours/Days toggle + count wheel commits the right offset.
|
||||
|
||||
---
|
||||
|
||||
## KC-24 — App rebrand to "Wenza"
|
||||
|
||||
**Status:** In Progress (display name + copy done; App Store name set in ASC)
|
||||
**Reported by:** User
|
||||
**Area:** Branding
|
||||
|
||||
### Problem
|
||||
The app is being rebranded from "Kisani Cal" to **Wenza**.
|
||||
|
||||
### Fix
|
||||
- `CFBundleDisplayName` → "Wenza" (app) and "Wenza Widgets" (widget) in
|
||||
`project.yml`; permission prompts and in-app copy (sidebar, Auth, Onboarding,
|
||||
Settings, tutorial, widget description) updated to Wenza.
|
||||
- **Deliberately unchanged** to preserve App Store identity and user data: bundle
|
||||
ID `com.kutesir.KisaniCal`, App Group `group.com.kutesir.KisaniCal`,
|
||||
`kisani.tasks.v2.*` storage keys, team ID, and internal Xcode target/project
|
||||
names.
|
||||
- App Store listing name set separately in App Store Connect (ships with the
|
||||
next version).
|
||||
|
||||
Files: `project.yml`, `ContentView.swift`, `TutorialManager.swift`, `AuthView.swift`,
|
||||
`OnboardingView.swift`, `SettingsView.swift`, `KisaniCalWidgets.swift`.
|
||||
|
||||
### How to test
|
||||
1. Build & run → home-screen label and all in-app titles read "Wenza".
|
||||
2. Confirm tasks/widget data survive (App Group + keys unchanged).
|
||||
|
||||
---
|
||||
|
||||
## KC-25 — Matrix "Move" lists only the other quadrants
|
||||
|
||||
**Status:** In Progress (implemented & build-verified, pending device QA)
|
||||
**Reported by:** User
|
||||
**Area:** Tasks / Matrix
|
||||
|
||||
### Problem
|
||||
The Move submenu listed all four quadrants with a checkmark on the current one,
|
||||
so the quadrant a task already sat in appeared as a no-op option.
|
||||
|
||||
### Fix
|
||||
- `TaskMenuItems` Move submenu now excludes the task's current quadrant and lists
|
||||
only the other three, using the clear matrix labels (e.g. a task in Urgent &
|
||||
Important shows Not Urgent & Important, Urgent & Unimportant, Not Urgent &
|
||||
Unimportant). Shared across Today, Matrix, and Calendar.
|
||||
|
||||
Files: `TaskContextMenu.swift`.
|
||||
|
||||
### How to test
|
||||
1. Long-press a task in any quadrant → Move shows only the three other quadrants.
|
||||
2. Pick one → task moves (priority/urgency updated per the TickTick model).
|
||||
|
||||
---
|
||||
|
||||
## KC-26 — Bars widget: auto-select advances to the next event
|
||||
|
||||
**Status:** In Progress (implemented & build-verified, pending device QA)
|
||||
**Reported by:** User
|
||||
**Area:** Widgets
|
||||
|
||||
### Problem
|
||||
With "Upcoming queue" on + "Nearest to finish", the Event Countdown (Bars) widget
|
||||
stayed stuck on an event after it expired (e.g. a passed deadline) instead of
|
||||
rolling to the next-nearest. The selection was frozen: the timeline computed the
|
||||
chosen event once and reused it for all 24 pre-rendered entries, only refreshing
|
||||
every 12 hours.
|
||||
|
||||
### Fix
|
||||
- Each timeline entry now re-resolves the nearest-to-finish event *as of that
|
||||
entry's own time* (`upcomingEvents(asOf:)` filters `dueDate > entryDate`), so
|
||||
the moment the current event expires the next entry rolls to the next one.
|
||||
- The timeline refreshes the instant the current selection expires
|
||||
(`currentSelectionExpiry`), not 12 hours later, giving the next event a fresh
|
||||
full pre-render window.
|
||||
- Completing/closing a task already reloads widget timelines, and the recompute
|
||||
drops it and picks the next. Tracked-event and manual-pick paths unchanged.
|
||||
|
||||
Files: `EventCountdownWidget.swift`.
|
||||
|
||||
### How to test
|
||||
1. Queue on + Nearest to finish; let the nearest event's due time pass → widget
|
||||
rolls to the next-nearest without manual action.
|
||||
2. Complete the nearest event → widget advances to the next.
|
||||
|
||||
@@ -222,6 +222,10 @@ class TaskViewModel: ObservableObject {
|
||||
return out
|
||||
}
|
||||
|
||||
/// All incomplete single-occurrence rows (dated and undated) — for Group & Sort
|
||||
/// and multi-select, which regroup the same task set the date sections draw from.
|
||||
var allActiveRows: [TaskItem] { activeRows() }
|
||||
|
||||
/// Active rows whose due date falls in [today+lo, today+hi) days (hi nil = open).
|
||||
private func bucket(from lo: Int, to hi: Int?) -> [TaskItem] {
|
||||
let cal = Calendar.current; let base = cal.startOfDay(for: Date())
|
||||
|
||||
@@ -20,14 +20,11 @@ struct SplashView: View {
|
||||
.scaleEffect(iconScale)
|
||||
.opacity(iconOpacity)
|
||||
|
||||
// kisaniCAL. — single line, orange dot
|
||||
// wenza. — single line, orange dot
|
||||
HStack(spacing: 0) {
|
||||
Text("kisani")
|
||||
Text("wenza")
|
||||
.font(AppFonts.mono(22, weight: .regular))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
Text("CAL")
|
||||
.font(AppFonts.mono(22, weight: .bold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
Text(".")
|
||||
.font(AppFonts.mono(22, weight: .bold))
|
||||
.foregroundColor(AppColors.accent)
|
||||
|
||||
@@ -89,9 +89,10 @@ struct TaskMenuItems: View {
|
||||
} label: { Label("Postpone", systemImage: "clock.arrow.circlepath") }
|
||||
|
||||
Menu {
|
||||
ForEach(Quadrant.allCases) { q in
|
||||
// Only the quadrants the task is NOT currently in — you can't move it to where it sits.
|
||||
ForEach(Quadrant.allCases.filter { $0 != (currentQuadrant ?? task.quadrant) }) { q in
|
||||
Button { onMove(task, q) } label: {
|
||||
Label(q.rawValue, systemImage: q == (currentQuadrant ?? task.quadrant) ? "checkmark" : "square")
|
||||
Label(q.matrixLabel, systemImage: "arrow.right")
|
||||
}
|
||||
}
|
||||
} label: { Label("Move", systemImage: "arrow.right.square") }
|
||||
|
||||
323
KisaniCal/Views/TodayMenuFeatures.swift
Normal file
323
KisaniCal/Views/TodayMenuFeatures.swift
Normal file
@@ -0,0 +1,323 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Today background themes
|
||||
|
||||
enum TodayBackground {
|
||||
static let presets: [(id: String, label: String)] = [
|
||||
("default", "Default"),
|
||||
("warm", "Warm"),
|
||||
("cool", "Cool"),
|
||||
("mono", "Mono"),
|
||||
("paper", "Paper"),
|
||||
]
|
||||
|
||||
/// The full-screen background for a given style. "default" is a flat surface;
|
||||
/// the rest are subtle top-down tints that fade into the base background.
|
||||
static func gradient(_ id: String, _ cs: ColorScheme) -> LinearGradient {
|
||||
let base = AppColors.background(cs)
|
||||
let top: Color
|
||||
switch id {
|
||||
case "warm": top = AppColors.accentSoft
|
||||
case "cool": top = AppColors.blueSoft
|
||||
case "mono": top = Color(.systemGray5)
|
||||
case "paper": top = Color(red: 0.96, green: 0.94, blue: 0.89)
|
||||
default: top = base
|
||||
}
|
||||
return LinearGradient(colors: [top, base], startPoint: .top, endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Group & Sort model
|
||||
|
||||
enum TaskGrouping: String, CaseIterable, Identifiable {
|
||||
case date, priority, category, quadrant
|
||||
var id: String { rawValue }
|
||||
var label: String {
|
||||
switch self {
|
||||
case .date: return "Date"
|
||||
case .priority: return "Priority"
|
||||
case .category: return "Category"
|
||||
case .quadrant: return "Quadrant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TaskSort: String, CaseIterable, Identifiable {
|
||||
case smart, due, priority, title
|
||||
var id: String { rawValue }
|
||||
var label: String {
|
||||
switch self {
|
||||
case .smart: return "Smart (pinned, then date)"
|
||||
case .due: return "Due date"
|
||||
case .priority: return "Priority"
|
||||
case .title: return "Title"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TaskOrganizer {
|
||||
private static func priorityRank(_ p: Priority) -> Int {
|
||||
switch p { case .high: return 0; case .medium: return 1; case .low: return 2; case .none: return 3 }
|
||||
}
|
||||
|
||||
static func sorted(_ tasks: [TaskItem], by sort: String) -> [TaskItem] {
|
||||
switch sort {
|
||||
case "due":
|
||||
return tasks.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
||||
case "priority":
|
||||
return tasks.sorted { priorityRank($0.priority) < priorityRank($1.priority) }
|
||||
case "title":
|
||||
return tasks.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
|
||||
default: // smart
|
||||
return tasks.sorted {
|
||||
if $0.isPinned != $1.isPinned { return $0.isPinned }
|
||||
return ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-date groupings → titled sections, each internally sorted. ("date" is handled
|
||||
/// by the existing day sections, so it isn't produced here.)
|
||||
@MainActor
|
||||
static func sections(_ tasks: [TaskItem], grouping: String, sort: String,
|
||||
vm: TaskViewModel) -> [(title: String, tasks: [TaskItem])] {
|
||||
let s = sorted(tasks, by: sort)
|
||||
switch grouping {
|
||||
case "priority":
|
||||
return [Priority.high, .medium, .low, .none].compactMap { p in
|
||||
let g = s.filter { $0.priority == p }
|
||||
return g.isEmpty ? nil : (p.label, g)
|
||||
}
|
||||
case "category":
|
||||
return [TaskCategory.birthday, .domain, .annual, .reminder, .custom].compactMap { c in
|
||||
let g = s.filter { $0.category == c }
|
||||
return g.isEmpty ? nil : (c.rawValue, g)
|
||||
}
|
||||
case "quadrant":
|
||||
return [Quadrant.urgent, .schedule, .delegate_, .eliminate].compactMap { q in
|
||||
let g = s.filter { vm.displayQuadrant($0) == q }
|
||||
return g.isEmpty ? nil : (q.matrixLabel, g)
|
||||
}
|
||||
default:
|
||||
return [("Tasks", s)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Background picker sheet
|
||||
|
||||
struct BackgroundPickerSheet: View {
|
||||
@Binding var selected: String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var cs
|
||||
|
||||
private let columns = [GridItem(.adaptive(minimum: 96), spacing: 14)]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 14) {
|
||||
ForEach(TodayBackground.presets, id: \.id) { preset in
|
||||
Button {
|
||||
withAnimation(KisaniSpring.snappy) { selected = preset.id }
|
||||
} label: {
|
||||
VStack(spacing: 8) {
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(TodayBackground.gradient(preset.id, cs))
|
||||
.frame(height: 80)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(selected == preset.id ? AppColors.accent : AppColors.border(cs),
|
||||
lineWidth: selected == preset.id ? 2.5 : 1)
|
||||
)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if selected == preset.id {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(AppColors.accent)
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
Text(preset.label)
|
||||
.font(AppFonts.sans(12, weight: .medium))
|
||||
.foregroundColor(AppColors.text2(cs))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(18)
|
||||
}
|
||||
.background(AppColors.background(cs).ignoresSafeArea())
|
||||
.navigationTitle("Background")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Group & Sort sheet
|
||||
|
||||
struct GroupSortSheet: View {
|
||||
@Binding var grouping: String
|
||||
@Binding var sort: String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section("Group by") {
|
||||
ForEach(TaskGrouping.allCases) { g in
|
||||
Button {
|
||||
withAnimation(KisaniSpring.micro) { grouping = g.rawValue }
|
||||
} label: {
|
||||
HStack {
|
||||
Text(g.label).foregroundColor(.primary)
|
||||
Spacer()
|
||||
if grouping == g.rawValue {
|
||||
Image(systemName: "checkmark").foregroundColor(AppColors.accent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("Sort by") {
|
||||
ForEach(TaskSort.allCases) { s in
|
||||
Button {
|
||||
withAnimation(KisaniSpring.micro) { sort = s.rawValue }
|
||||
} label: {
|
||||
HStack {
|
||||
Text(s.label).foregroundColor(.primary)
|
||||
Spacer()
|
||||
if sort == s.rawValue {
|
||||
Image(systemName: "checkmark").foregroundColor(AppColors.accent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Group & Sort")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Multi-select sheet
|
||||
|
||||
struct SelectableTaskListSheet: View {
|
||||
@EnvironmentObject var taskVM: TaskViewModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var cs
|
||||
@State private var selection: Set<UUID> = []
|
||||
|
||||
private let fmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "d MMM"; return f
|
||||
}()
|
||||
|
||||
private var rows: [TaskItem] { taskVM.allActiveRows }
|
||||
private var selectedTasks: [TaskItem] { rows.filter { selection.contains($0.id) } }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(rows) { task in
|
||||
Button { toggle(task.id) } label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: selection.contains(task.id) ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(selection.contains(task.id) ? AppColors.accent : AppColors.text3(cs))
|
||||
Text(task.title)
|
||||
.font(AppFonts.sans(14))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
if let d = task.dueDate {
|
||||
Text(fmt.string(from: d))
|
||||
.font(AppFonts.mono(10))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(selection.isEmpty ? "Select" : "\(selection.count) Selected")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(selection.count == rows.count && !rows.isEmpty ? "Deselect All" : "Select All") { toggleAll() }
|
||||
.disabled(rows.isEmpty)
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if !selection.isEmpty { bulkBar }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var bulkBar: some View {
|
||||
HStack(spacing: 18) {
|
||||
bulkButton("Complete", "checkmark.circle") {
|
||||
selectedTasks.forEach { taskVM.toggle($0) }
|
||||
selection.removeAll()
|
||||
}
|
||||
Menu {
|
||||
ForEach(Priority.allCases) { p in
|
||||
Button(p.label) { selectedTasks.forEach { taskVM.setPriority($0, priority: p) } }
|
||||
}
|
||||
} label: { bulkLabel("Priority", "flag") }
|
||||
|
||||
Menu {
|
||||
ForEach(Quadrant.allCases) { q in
|
||||
Button(q.matrixLabel) { selectedTasks.forEach { taskVM.moveToQuadrant(taskID: $0.id, quadrant: q) } }
|
||||
}
|
||||
} label: { bulkLabel("Move", "arrow.right.square") }
|
||||
|
||||
bulkButton("Delete", "trash", role: .destructive) {
|
||||
selectedTasks.forEach { taskVM.delete($0) }
|
||||
selection.removeAll()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.ultraThinMaterial)
|
||||
.overlay(Rectangle().frame(height: 1).foregroundColor(AppColors.border(cs)), alignment: .top)
|
||||
}
|
||||
|
||||
private func bulkButton(_ title: String, _ icon: String,
|
||||
role: ButtonRole? = nil, action: @escaping () -> Void) -> some View {
|
||||
Button(role: role, action: action) { bulkLabel(title, icon, destructive: role == .destructive) }
|
||||
}
|
||||
|
||||
private func bulkLabel(_ title: String, _ icon: String, destructive: Bool = false) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: icon).font(.system(size: 18))
|
||||
Text(title).font(AppFonts.sans(10, weight: .medium))
|
||||
}
|
||||
.foregroundColor(destructive ? .red : AppColors.accent)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func toggle(_ id: UUID) {
|
||||
if selection.contains(id) { selection.remove(id) } else { selection.insert(id) }
|
||||
}
|
||||
|
||||
private func toggleAll() {
|
||||
if selection.count == rows.count { selection.removeAll() }
|
||||
else { selection = Set(rows.map { $0.id }) }
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,12 @@ struct TodayView: View {
|
||||
@State private var showWorkoutSession = false
|
||||
@State private var editingTask: TaskItem? = nil
|
||||
@AppStorage("todayHideCompleted") private var hideCompleted = false
|
||||
@AppStorage("todayBackground") private var bgStyle = "default"
|
||||
@AppStorage("todayGroupMode") private var groupMode = "date"
|
||||
@AppStorage("todaySortMode") private var sortMode = "smart"
|
||||
@State private var showBackgroundSheet = false
|
||||
@State private var showGroupSortSheet = false
|
||||
@State private var showSelectSheet = false
|
||||
@ObservedObject private var tm = TutorialManager.shared
|
||||
@ObservedObject private var hk = HealthKitManager.shared
|
||||
|
||||
@@ -27,6 +33,29 @@ struct TodayView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Group & Sort: non-date groupings rendered as titled sections (reuses UpcomingSection).
|
||||
@ViewBuilder private var groupedTaskSections: some View {
|
||||
let sections = TaskOrganizer.sections(taskVM.allActiveRows, grouping: groupMode,
|
||||
sort: sortMode, vm: taskVM)
|
||||
ForEach(Array(sections.enumerated()), id: \.offset) { _, section in
|
||||
UpcomingSection(
|
||||
title: section.title,
|
||||
tasks: section.tasks,
|
||||
onToggle: { taskVM.toggle($0) },
|
||||
onPostpone: { taskVM.postpone($0) },
|
||||
onPin: { taskVM.pin($0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView(showsIndicators: false) {
|
||||
@@ -36,7 +65,7 @@ struct TodayView: View {
|
||||
// ── Header ──
|
||||
HStack {
|
||||
Menu {
|
||||
Button { } label: { Label("Background", systemImage: "paintbrush") }
|
||||
Button { showBackgroundSheet = true } label: { Label("Background", systemImage: "paintbrush") }
|
||||
Button {
|
||||
withAnimation(KisaniSpring.snappy) { hideCompleted.toggle() }
|
||||
} label: {
|
||||
@@ -44,8 +73,8 @@ struct TodayView: View {
|
||||
systemImage: hideCompleted ? "checkmark.circle" : "checkmark.circle.badge.xmark")
|
||||
}
|
||||
Divider()
|
||||
Button { } label: { Label("Group & Sort", systemImage: "arrow.up.arrow.down") }
|
||||
Button { } label: { Label("Select", systemImage: "checkmark.circle") }
|
||||
Button { showGroupSortSheet = true } label: { Label("Group & Sort", systemImage: "arrow.up.arrow.down") }
|
||||
Button { showSelectSheet = true } label: { Label("Select", systemImage: "checkmark.circle") }
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
@@ -81,6 +110,7 @@ struct TodayView: View {
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
|
||||
if groupMode == "date" {
|
||||
// ── Today timeline: active → completed (green) ──
|
||||
let activeTasks = taskVM.todayTasks
|
||||
let completedTasks = hideCompleted ? [] : taskVM.completedTodayTasks
|
||||
@@ -197,6 +227,10 @@ struct TodayView: View {
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
groupedTaskSections
|
||||
}
|
||||
|
||||
// ── Completed (all, collapsible) ──
|
||||
if !taskVM.completedTasks.isEmpty {
|
||||
CompletedSection(
|
||||
@@ -207,9 +241,7 @@ struct TodayView: View {
|
||||
.padding(.top, 10)
|
||||
}
|
||||
|
||||
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
|
||||
|| !taskVM.tomorrowTasks.isEmpty || !taskVM.next7DaysTasks.isEmpty
|
||||
|| !taskVM.laterTasks.isEmpty || !taskVM.completedTodayTasks.isEmpty
|
||||
let hasAnything = !taskVM.allActiveRows.isEmpty || !taskVM.completedTodayTasks.isEmpty
|
||||
|| !todayCalEvents.isEmpty
|
||||
|| workoutVM.program(for: Date()) != nil
|
||||
if !hasAnything {
|
||||
@@ -250,13 +282,28 @@ struct TodayView: View {
|
||||
.zIndex(10)
|
||||
}
|
||||
}
|
||||
.background(AppColors.background(cs).ignoresSafeArea())
|
||||
.background(TodayBackground.gradient(bgStyle, cs).ignoresSafeArea())
|
||||
.sheet(isPresented: $showAddTask) {
|
||||
AddTaskSheet()
|
||||
.environmentObject(taskVM)
|
||||
.presentationDetents([.height(150)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(isPresented: $showBackgroundSheet) {
|
||||
BackgroundPickerSheet(selected: $bgStyle)
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(isPresented: $showGroupSortSheet) {
|
||||
GroupSortSheet(grouping: $groupMode, sort: $sortMode)
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(isPresented: $showSelectSheet) {
|
||||
SelectableTaskListSheet()
|
||||
.environmentObject(taskVM)
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(isPresented: $showWorkoutSession) {
|
||||
WorkoutSessionSheet()
|
||||
.environmentObject(workoutVM)
|
||||
|
||||
Reference in New Issue
Block a user