Commit 3a3dedba06
Verified · cmc
Layout: unified · split
Sources/Orgstar/AgendaView.swift +11 −7
| @@ -38,6 +38,8 @@ struct AgendaView: View { | ||
| 38 | 38 | /// The selected line: `day:index` (`day:o` + index in the overdue group), or `list:index`. |
| 39 | 39 | @State private var selection: String? |
| 40 | 40 | @State private var tick = 0 |
| 41 | /// The time the now line shows, kept to the minute while the agenda is open. | |
| 42 | @State private var now = Date() | |
| 41 | 43 | /// The view picker moved to the agenda for a followed timestamp, which sets its own days. |
| 42 | 44 | @State private var keepRequestedDays = false |
| 43 | 45 | @State private var message: String? |
| @@ -295,10 +297,12 @@ struct AgendaView: View { | ||
| 295 | 297 | } |
| 296 | 298 | .task { |
| 297 | 299 | loadViews() |
| 298 | // The time grid's "now" line and the day itself move on. | |
| 300 | // The now line moves each minute; a new day refreshes the statuses. | |
| 299 | 301 | while !Task.isCancelled { |
| 300 | try? await Task.sleep(for: .seconds(60)) | |
| 301 | tick += 1 | |
| 302 | let next = Calendar.current.nextDate(after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime) ?? Date().addingTimeInterval(60) | |
| 303 | try? await Task.sleep(for: .seconds(next.timeIntervalSinceNow)) | |
| 304 | now = Date() | |
| 305 | if Days.absolute(now, calendar: .current) != agenda.today { tick += 1 } | |
| 302 | 306 | } |
| 303 | 307 | } |
| 304 | 308 | } |
| @@ -325,12 +329,12 @@ struct AgendaView: View { | ||
| 325 | 329 | .padding(.horizontal, 16) |
| 326 | 330 | .padding(.vertical, 10) |
| 327 | 331 | } |
| 328 | let now = group.nowIndex(now: Self.hhmm(Date())) | |
| 332 | let nowAt = group.nowIndex(now: Self.hhmm(now)) | |
| 329 | 333 | ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, row in |
| 330 | if index == now { nowLine } | |
| 334 | if index == nowAt { nowLine } | |
| 331 | 335 | rowView(Line(id: "\(group.day):\(index)", row: row, day: group.day)) |
| 332 | 336 | } |
| 333 | if let now, now == group.rows.count { nowLine } | |
| 337 | if let nowAt, nowAt == group.rows.count { nowLine } | |
| 334 | 338 | } |
| 335 | 339 | |
| 336 | 340 | private func header(@ViewBuilder _ content: () -> some View) -> some View { |
| @@ -400,7 +404,7 @@ struct AgendaView: View { | ||
| 400 | 404 | |
| 401 | 405 | private var nowLine: some View { |
| 402 | 406 | HStack(spacing: 6) { |
| 403 | Text(Date().formatted(.dateTime.hour(.twoDigits(amPM: .omitted)).minute())) | |
| 407 | Text(now.formatted(.dateTime.hour(.twoDigits(amPM: .omitted)).minute())) | |
| 404 | 408 | .font(.system(size: 10, weight: .semibold).monospacedDigit()) |
| 405 | 409 | .frame(width: AgendaRowView.gutterWidth, alignment: .leading) |
| 406 | 410 | Circle().frame(width: 6, height: 6) |
Sources/OrgstarMobile/AgendaScreen.swift +17 −5
| @@ -18,6 +18,8 @@ struct AgendaScreen: View { | ||
| 18 | 18 | @Environment(MobileModel.self) private var model |
| 19 | 19 | @Environment(\.colorScheme) private var colorScheme |
| 20 | 20 | @State private var tick = 0 |
| 21 | /// The time the now line shows, kept to the minute while the agenda is open. | |
| 22 | @State private var now = Date() | |
| 21 | 23 | @State private var views = AgendaViewLoader.builtIn |
| 22 | 24 | @State private var viewName = "Agenda" |
| 23 | 25 | @State private var filter = AgendaFilter() |
| @@ -173,6 +175,15 @@ struct AgendaScreen: View { | ||
| 173 | 175 | views = loaded.views |
| 174 | 176 | if let problem = loaded.problems.first { message = problem } |
| 175 | 177 | } |
| 178 | // The now line moves each minute; a new day refreshes the statuses. | |
| 179 | .task { | |
| 180 | while !Task.isCancelled { | |
| 181 | let next = Calendar.current.nextDate(after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime) ?? Date().addingTimeInterval(60) | |
| 182 | try? await Task.sleep(for: .seconds(next.timeIntervalSinceNow)) | |
| 183 | now = Date() | |
| 184 | if Days.absolute(now, calendar: .current) != agenda.today { tick += 1 } | |
| 185 | } | |
| 186 | } | |
| 176 | 187 | // Calendar access is asked for when calendar-events is turned on. |
| 177 | 188 | .task(id: model.settingsVersion) { |
| 178 | 189 | guard OrgPreferences.calendarEvents(), !CalendarEvents.isAuthorized else { return } |
| @@ -232,17 +243,17 @@ struct AgendaScreen: View { | ||
| 232 | 243 | if group.isEmpty { |
| 233 | 244 | Text("Nothing scheduled").font(.subheadline).foregroundStyle(.secondary) |
| 234 | 245 | } |
| 235 | let now = nowIndex(group) | |
| 246 | let nowAt = nowIndex(group) | |
| 236 | 247 | ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, item in |
| 237 | if index == now { NowLine(color: todayColor) } | |
| 248 | if index == nowAt { NowLine(time: now, color: todayColor) } | |
| 238 | 249 | row(item, day: group.day) |
| 239 | 250 | } |
| 240 | if let now, now == group.rows.count { NowLine(color: todayColor) } | |
| 251 | if let nowAt, nowAt == group.rows.count { NowLine(time: now, color: todayColor) } | |
| 241 | 252 | } |
| 242 | 253 | } |
| 243 | 254 | |
| 244 | 255 | private func nowIndex(_ group: AgendaDayGroup) -> Int? { |
| 245 | let c = Calendar.current.dateComponents([.hour, .minute], from: Date()) | |
| 256 | let c = Calendar.current.dateComponents([.hour, .minute], from: now) | |
| 246 | 257 | return group.nowIndex(now: (c.hour ?? 0) * 100 + (c.minute ?? 0)) |
| 247 | 258 | } |
| 248 | 259 | |
| @@ -672,11 +683,12 @@ struct AgendaRowView: View { | ||
| 672 | 683 | |
| 673 | 684 | /// The current time between timed rows, as Org's `now - - -` line. |
| 674 | 685 | struct NowLine: View { |
| 686 | let time: Date | |
| 675 | 687 | let color: Color |
| 676 | 688 | |
| 677 | 689 | var body: some View { |
| 678 | 690 | HStack(spacing: 6) { |
| 679 | Text(Date.now.formatted(date: .omitted, time: .shortened)) | |
| 691 | Text(time.formatted(date: .omitted, time: .shortened)) | |
| 680 | 692 | .font(.caption2.weight(.semibold).monospacedDigit()) |
| 681 | 693 | .foregroundStyle(color) |
| 682 | 694 | Circle().fill(color).frame(width: 6, height: 6) |