App/Sources/TourViews.swift
128 lines · 5713 bytes
3 symbols in this file
1import Library
2import SkyEngine
3import SwiftUI
4
5/// The tours, starting with the one for this month.
6struct TourPicker: View {
7 var tonight: Tonight?
8 var onPick: (Tour) -> Void
9 @Environment(\.dismiss) private var dismiss
10
11 private let ink = Color(red: 0.95, green: 0.95, blue: 0.93)
12 private let accent = Color(red: 0.88, green: 0.24, blue: 0.19)
13 private let month = Calendar.current.component(.month, from: .now)
14
15 var body: some View {
16 NavigationStack {
17 ScrollView {
18 VStack(alignment: .leading, spacing: 0) {
19 Text("Guided tours").font(.title.weight(.medium))
20 Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 8).padding(.bottom, 12)
21 Text("A walk around the evening sky, one sight at a time. The view turns to each stop and marks what to look for. Best outside after dark, but they work anywhere.")
22 .font(.subheadline).foregroundStyle(ink.opacity(0.75))
23 .padding(.bottom, 20)
24 if let tonight, tonight.moonlit {
25 Text("Tonight: \(tonight.moonAndDarkness()) Fainter stars are harder to see while the Moon is up.")
26 .font(.footnote).foregroundStyle(ink.opacity(0.75))
27 .padding(.bottom, 20)
28 }
29 ForEach(Tour.ordered(month: month)) { tour in card(tour) }
30 }
31 .padding(20)
32 .readableWidth()
33 }
34 .background(Color.black.ignoresSafeArea())
35 .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } }
36 }
37 .foregroundStyle(ink)
38 .tint(accent)
39 }
40
41 private func card(_ tour: Tour) -> some View {
42 let now = tour.months.contains(month)
43 return Button {
44 onPick(tour)
45 dismiss()
46 } label: {
47 VStack(alignment: .leading, spacing: 10) {
48 HStack {
49 if now {
50 Text("THIS MONTH").font(.caption2.weight(.bold)).tracking(0.8)
51 .padding(.horizontal, 6).padding(.vertical, 2)
52 .background(accent, in: RoundedRectangle(cornerRadius: 3))
53 }
54 Text(tour.title).font(.caption.weight(.medium)).foregroundStyle(ink.opacity(0.7))
55 Spacer()
56 Text("\(tour.steps.count) stops").font(.caption).foregroundStyle(ink.opacity(0.6))
57 }
58 Text(tour.steps.prefix(4).map(\.title).joined(separator: " · ") + (tour.steps.count > 4 ? " …" : ""))
59 .font(.title3.weight(.medium))
60 .multilineTextAlignment(.leading)
61 Text(tour.intro).font(.footnote).foregroundStyle(ink.opacity(0.75)).multilineTextAlignment(.leading)
62 HStack(spacing: 6) {
63 Text("Start tour").font(.subheadline.weight(.semibold))
64 Image(systemName: "arrow.right").font(.footnote.weight(.semibold))
65 }
66 .foregroundStyle(now ? accent : ink.opacity(0.8))
67 .padding(.top, 2)
68 }
69 .padding(18)
70 .frame(maxWidth: .infinity, alignment: .leading)
71 .background(ink.opacity(now ? 0.09 : 0.05), in: RoundedRectangle(cornerRadius: 18))
72 .overlay(RoundedRectangle(cornerRadius: 18).stroke(now ? accent.opacity(0.6) : ink.opacity(0.1), lineWidth: now ? 1 : 0.5))
73 .contentShape(RoundedRectangle(cornerRadius: 18))
74 }
75 .buttonStyle(.plain)
76 .padding(.bottom, 14)
77 .accessibilityHint("Starts the tour")
78 }
79}
80
81/// One stop of a tour, with back, next and a link to the Library page.
82struct TourCard: View {
83 var tour: Tour
84 @Binding var step: Int
85 /// Whether the target is below the horizon at the time shown.
86 var belowHorizon: Bool
87 var ink: Color
88 var background: Color
89 var onOpen: (LibraryPage) -> Void
90 var onEnd: () -> Void
91
92 var body: some View {
93 let s = tour.steps[step]
94 VStack(alignment: .leading, spacing: 8) {
95 HStack {
96 Text("\(tour.title) · \(step + 1) of \(tour.steps.count)").font(.caption2).dimmed(0.7)
97 Spacer()
98 Button("End tour", action: onEnd).font(.caption.weight(.medium))
99 }
100 Text(s.title).font(.headline)
101 Text(s.text).font(.subheadline).fixedSize(horizontal: false, vertical: true)
102 if belowHorizon {
103 Text("Below your horizon at this time. Try later in the evening or another month.").font(.footnote).dimmed(0.7)
104 }
105 HStack {
106 if step > 0 {
107 Button { step -= 1 } label: { Label("Back", systemImage: "chevron.left") }
108 }
109 Spacer()
110 if let page = s.libraryPage {
111 Button("Read more") { onOpen(page) }
112 Spacer()
113 }
114 if step < tour.steps.count - 1 {
115 Button { step += 1 } label: { Label("Next", systemImage: "chevron.right").labelStyle(.titleAndIcon) }
116 }
117 }
118 .font(.subheadline.weight(.medium))
119 .padding(.top, 2)
120 }
121 .padding(16)
122 .background(background, in: RoundedRectangle(cornerRadius: 16))
123 .overlay(RoundedRectangle(cornerRadius: 16).stroke(ink.opacity(0.15), lineWidth: 0.5))
124 .foregroundStyle(ink)
125 .padding(.horizontal, 12)
126 .padding(.bottom, 8)
127 }
128}