App/Sources/PassFollowButton.swift
71 lines · 3347 bytes
1import ActivityKit
2import SkyEngine
3import SwiftUI
4
5/// Bell on a pass row: puts the pass on the Lock Screen from 10 minutes before rise. Hidden for passes more
6/// than a day away.
7struct PassFollowButton: View {
8 var pass: SatellitePass
9 @State private var following = false
10 @State private var message: String?
11
12 var body: some View {
13 if pass.activityStart(now: .now) != nil {
14 Button {
15 following ? unfollow() : follow()
16 } label: {
17 Image(systemName: following ? "bell.fill" : "bell")
18 .font(.subheadline)
19 .frame(minWidth: 44, minHeight: 44)
20 .contentShape(Rectangle())
21 }
22 .buttonStyle(.plain)
23 .accessibilityLabel(following ? "Stop following on Lock Screen" : "Follow on Lock Screen")
24 .onAppear { following = Activity<PassActivityAttributes>.activities.contains { matches($0.attributes, pass) } }
25 .alert(message ?? "", isPresented: Binding { message != nil } set: { if !$0 { message = nil } }) {}
26 }
27 }
28
29 private func follow() {
30 guard ActivityAuthorizationInfo().areActivitiesEnabled else {
31 message = "Live Activities are turned off for Moonrise in Settings."
32 return
33 }
34 guard let start = pass.activityStart(now: .now) else { return }
35 let attributes = PassActivityAttributes(name: pass.displayName, rise: pass.rise, peak: pass.peak, set: pass.set,
36 riseAzimuth: pass.riseAzimuth, setAzimuth: pass.setAzimuth,
37 maxAltitude: pass.maxAltitude, visible: pass.visible)
38 // It cannot end itself without an update, so it goes stale a minute after set.
39 let content = ActivityContent(state: PassActivityAttributes.ContentState(), staleDate: pass.set.addingTimeInterval(60))
40 do {
41 if start > .now {
42 let rise = pass.rise.formatted(date: .omitted, time: .shortened)
43 let alert = AlertConfiguration(
44 title: "\(pass.displayName) pass in 10 minutes",
45 body: "Rises \(SkyEngine.compass(pass.riseAzimuth)) at \(rise), highest \(Int(pass.maxAltitude.rounded()))°, sets \(SkyEngine.compass(pass.setAzimuth)).",
46 sound: .default)
47 _ = try Activity.request(attributes: attributes, content: content, style: .standard, alertConfiguration: alert, start: start)
48 } else {
49 _ = try Activity.request(attributes: attributes, content: content)
50 }
51 following = true
52 } catch {
53 message = "Couldn't add the pass: \(error.localizedDescription)"
54 }
55 }
56
57 private func unfollow() {
58 let pass = pass
59 Task {
60 for a in Activity<PassActivityAttributes>.activities where matches(a.attributes, pass) {
61 await a.end(nil, dismissalPolicy: .immediate)
62 }
63 following = false
64 }
65 }
66}
67
68/// Rise times shift by a 20 s step each time passes are recomputed.
69private func matches(_ attributes: PassActivityAttributes, _ pass: SatellitePass) -> Bool {
70 attributes.name == pass.displayName && abs(attributes.rise.timeIntervalSince(pass.rise)) < 120
71}