Commit 3a3dedba06
Verified · cmc
Layout: unified · split
Sources/Orgstar/AgendaView.swift +11 −7
| @@ -38,6 +38,8 @@ struct AgendaView: View { | |||
| 38 | /// The selected line: `day:index` (`day:o` + index in the overdue group), or `list:index`. | 38 | /// The selected line: `day:index` (`day:o` + index in the overdue group), or `list:index`. |
| 39 | @State private var selection: String? | 39 | @State private var selection: String? |
| 40 | @State private var tick = 0 | 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 | /// The view picker moved to the agenda for a followed timestamp, which sets its own days. | 43 | /// The view picker moved to the agenda for a followed timestamp, which sets its own days. |
| 42 | @State private var keepRequestedDays = false | 44 | @State private var keepRequestedDays = false |
| 43 | @State private var message: String? | 45 | @State private var message: String? |
| @@ -295,10 +297,12 @@ struct AgendaView: View { | |||
| 295 | } | 297 | } |
| 296 | .task { | 298 | .task { |
| 297 | loadViews() | 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 | while !Task.isCancelled { | 301 | while !Task.isCancelled { |
| 300 | try? await Task.sleep(for: .seconds(60)) | 302 | let next = Calendar.current.nextDate(after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime) ?? Date().addingTimeInterval(60) |
| 301 | tick += 1 | 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 | .padding(.horizontal, 16) | 329 | .padding(.horizontal, 16) |
| 326 | .padding(.vertical, 10) | 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 | ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, row in | 333 | ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, row in |
| 330 | if index == now { nowLine } | 334 | if index == nowAt { nowLine } |
| 331 | rowView(Line(id: "\(group.day):\(index)", row: row, day: group.day)) | 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 | private func header(@ViewBuilder _ content: () -> some View) -> some View { | 340 | private func header(@ViewBuilder _ content: () -> some View) -> some View { |
| @@ -400,7 +404,7 @@ struct AgendaView: View { | |||
| 400 | 404 | ||
| 401 | private var nowLine: some View { | 405 | private var nowLine: some View { |
| 402 | HStack(spacing: 6) { | 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 | .font(.system(size: 10, weight: .semibold).monospacedDigit()) | 408 | .font(.system(size: 10, weight: .semibold).monospacedDigit()) |
| 405 | .frame(width: AgendaRowView.gutterWidth, alignment: .leading) | 409 | .frame(width: AgendaRowView.gutterWidth, alignment: .leading) |
| 406 | Circle().frame(width: 6, height: 6) | 410 | Circle().frame(width: 6, height: 6) |
Sources/OrgstarMobile/AgendaScreen.swift +17 −5
| @@ -18,6 +18,8 @@ struct AgendaScreen: View { | |||
| 18 | @Environment(MobileModel.self) private var model | 18 | @Environment(MobileModel.self) private var model |
| 19 | @Environment(\.colorScheme) private var colorScheme | 19 | @Environment(\.colorScheme) private var colorScheme |
| 20 | @State private var tick = 0 | 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 | @State private var views = AgendaViewLoader.builtIn | 23 | @State private var views = AgendaViewLoader.builtIn |
| 22 | @State private var viewName = "Agenda" | 24 | @State private var viewName = "Agenda" |
| 23 | @State private var filter = AgendaFilter() | 25 | @State private var filter = AgendaFilter() |
| @@ -173,6 +175,15 @@ struct AgendaScreen: View { | |||
| 173 | views = loaded.views | 175 | views = loaded.views |
| 174 | if let problem = loaded.problems.first { message = problem } | 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 | // Calendar access is asked for when calendar-events is turned on. | 187 | // Calendar access is asked for when calendar-events is turned on. |
| 177 | .task(id: model.settingsVersion) { | 188 | .task(id: model.settingsVersion) { |
| 178 | guard OrgPreferences.calendarEvents(), !CalendarEvents.isAuthorized else { return } | 189 | guard OrgPreferences.calendarEvents(), !CalendarEvents.isAuthorized else { return } |
| @@ -232,17 +243,17 @@ struct AgendaScreen: View { | |||
| 232 | if group.isEmpty { | 243 | if group.isEmpty { |
| 233 | Text("Nothing scheduled").font(.subheadline).foregroundStyle(.secondary) | 244 | Text("Nothing scheduled").font(.subheadline).foregroundStyle(.secondary) |
| 234 | } | 245 | } |
| 235 | let now = nowIndex(group) | 246 | let nowAt = nowIndex(group) |
| 236 | ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, item in | 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 | row(item, day: group.day) | 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 | private func nowIndex(_ group: AgendaDayGroup) -> Int? { | 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 | return group.nowIndex(now: (c.hour ?? 0) * 100 + (c.minute ?? 0)) | 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 | /// The current time between timed rows, as Org's `now - - -` line. | 684 | /// The current time between timed rows, as Org's `now - - -` line. |
| 674 | struct NowLine: View { | 685 | struct NowLine: View { |
| 686 | let time: Date | ||
| 675 | let color: Color | 687 | let color: Color |
| 676 | 688 | ||
| 677 | var body: some View { | 689 | var body: some View { |
| 678 | HStack(spacing: 6) { | 690 | HStack(spacing: 6) { |
| 679 | Text(Date.now.formatted(date: .omitted, time: .shortened)) | 691 | Text(time.formatted(date: .omitted, time: .shortened)) |
| 680 | .font(.caption2.weight(.semibold).monospacedDigit()) | 692 | .font(.caption2.weight(.semibold).monospacedDigit()) |
| 681 | .foregroundStyle(color) | 693 | .foregroundStyle(color) |
| 682 | Circle().fill(color).frame(width: 6, height: 6) | 694 | Circle().fill(color).frame(width: 6, height: 6) |