Merge develop -> main: full session (KC-51 through KC-71) #28
@@ -1004,27 +1004,27 @@ struct CalendarView: View {
|
|||||||
ForEach(cvTimeItems(date)) { item in
|
ForEach(cvTimeItems(date)) { item in
|
||||||
let top = max(0, CGFloat(item.startMin - cvStartH * 60) / 60.0 * cvHourH)
|
let top = max(0, CGFloat(item.startMin - cvStartH * 60) / 60.0 * cvHourH)
|
||||||
let dur = max(CGFloat(item.endMin - item.startMin) / 60.0 * cvHourH, 28)
|
let dur = max(CGFloat(item.endMin - item.startMin) / 60.0 * cvHourH, 28)
|
||||||
RoundedRectangle(cornerRadius: 4)
|
CVTimeBlock(
|
||||||
.fill(item.color.opacity(0.28))
|
item: item, top: top, height: dur, hourH: cvHourH,
|
||||||
.overlay(alignment: .leading) {
|
draggable: item.taskID != nil && !item.recurring,
|
||||||
Rectangle().fill(item.color).frame(width: 3)
|
onReschedule: { newStartMin in
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 2))
|
rescheduleTask(id: item.taskID, on: date, toStartMin: newStartMin)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .topLeading) {
|
)
|
||||||
Text(item.title)
|
|
||||||
.font(AppFonts.sans(9, weight: .medium))
|
|
||||||
.foregroundColor(item.color)
|
|
||||||
.padding(.horizontal, 6).padding(.top, 4)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity).frame(height: dur)
|
|
||||||
.padding(.trailing, 2)
|
|
||||||
.offset(y: top)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move a timed task to a new start minute on the same day (drag-to-reschedule).
|
||||||
|
private func rescheduleTask(id: UUID?, on date: Date, toStartMin newStartMin: Int) {
|
||||||
|
guard let id, let task = taskVM.tasks.first(where: { $0.id == id }) else { return }
|
||||||
|
let clamped = min(max(newStartMin, cvStartH * 60), cvEndH * 60 - 15)
|
||||||
|
let h = clamped / 60, m = clamped % 60
|
||||||
|
guard let newDate = cal.date(bySettingHour: h, minute: m, second: 0, of: date) else { return }
|
||||||
|
withAnimation(KisaniSpring.snappy) { taskVM.setDate(task, date: newDate) }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Shared helpers
|
// MARK: - Shared helpers
|
||||||
|
|
||||||
private var cvWeekStrip: some View {
|
private var cvWeekStrip: some View {
|
||||||
@@ -1084,7 +1084,9 @@ struct CalendarView: View {
|
|||||||
for task in taskVM.occurrences(on: date) where !task.isComplete && task.hasTime {
|
for task in taskVM.occurrences(on: date) where !task.isComplete && task.hasTime {
|
||||||
guard let d = task.dueDate else { continue }
|
guard let d = task.dueDate else { continue }
|
||||||
let h = cal.component(.hour, from: d), m = cal.component(.minute, from: d)
|
let h = cal.component(.hour, from: d), m = cal.component(.minute, from: d)
|
||||||
items.append(CVTimeItem(title: task.title, color: task.quadrant.color, startMin: h * 60 + m, endMin: h * 60 + m + 30))
|
items.append(CVTimeItem(title: task.title, color: task.quadrant.color,
|
||||||
|
startMin: h * 60 + m, endMin: h * 60 + m + 30,
|
||||||
|
taskID: task.id, recurring: taskVM.recurs(task)))
|
||||||
}
|
}
|
||||||
for ev in calStore.isAuthorized ? calStore.events(for: date) : [] {
|
for ev in calStore.isAuthorized ? calStore.events(for: date) : [] {
|
||||||
guard let s = ev.startDate, let e = ev.endDate, !ev.isAllDay else { continue }
|
guard let s = ev.startDate, let e = ev.endDate, !ev.isAllDay else { continue }
|
||||||
@@ -1107,13 +1109,65 @@ struct CVTimeItem: Identifiable {
|
|||||||
let color: Color
|
let color: Color
|
||||||
let startMin: Int
|
let startMin: Int
|
||||||
let endMin: Int
|
let endMin: Int
|
||||||
|
let taskID: UUID? // non-nil for task-backed items (draggable)
|
||||||
|
let recurring: Bool // recurring tasks aren't drag-rescheduled
|
||||||
|
|
||||||
init(title: String, color: Color, startMin: Int, endMin: Int) {
|
init(title: String, color: Color, startMin: Int, endMin: Int,
|
||||||
self.id = "\(title)-\(startMin)"
|
taskID: UUID? = nil, recurring: Bool = false) {
|
||||||
|
self.id = taskID?.uuidString ?? "\(title)-\(startMin)"
|
||||||
self.title = title
|
self.title = title
|
||||||
self.color = color
|
self.color = color
|
||||||
self.startMin = startMin
|
self.startMin = startMin
|
||||||
self.endMin = endMin
|
self.endMin = endMin
|
||||||
|
self.taskID = taskID
|
||||||
|
self.recurring = recurring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single timed block on the week/day timeline. Task-backed blocks can be
|
||||||
|
// dragged vertically to reschedule (snapped to 15-minute steps); events and
|
||||||
|
// recurring tasks render fixed.
|
||||||
|
private struct CVTimeBlock: View {
|
||||||
|
let item: CVTimeItem
|
||||||
|
let top: CGFloat
|
||||||
|
let height: CGFloat
|
||||||
|
let hourH: CGFloat
|
||||||
|
let draggable: Bool
|
||||||
|
let onReschedule: (Int) -> Void
|
||||||
|
@GestureState private var dragY: CGFloat = 0
|
||||||
|
|
||||||
|
private var block: some View {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(item.color.opacity(dragY == 0 ? 0.28 : 0.45))
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
Rectangle().fill(item.color).frame(width: 3)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 2))
|
||||||
|
}
|
||||||
|
.overlay(alignment: .topLeading) {
|
||||||
|
Text(item.title)
|
||||||
|
.font(AppFonts.sans(9, weight: .medium))
|
||||||
|
.foregroundColor(item.color)
|
||||||
|
.padding(.horizontal, 6).padding(.top, 4)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity).frame(height: height)
|
||||||
|
.padding(.trailing, 2)
|
||||||
|
.offset(y: top + dragY)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if draggable {
|
||||||
|
block.gesture(
|
||||||
|
DragGesture(minimumDistance: 6)
|
||||||
|
.updating($dragY) { value, state, _ in state = value.translation.height }
|
||||||
|
.onEnded { value in
|
||||||
|
let deltaMin = Int((Double(value.translation.height) / Double(hourH) * 60).rounded() / 15) * 15
|
||||||
|
onReschedule(item.startMin + deltaMin)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
block
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user