// // ContentView.swift // MoonWatch // // Created by david on 4/26/26. // import SwiftUI struct ContentView: View { @AppStorage("moonSouthernHemisphere") private var southernHemisphere = false var body: some View { TimelineView(.periodic(from: .now, by: 60)) { context in let snapshot = MoonPhaseCalculator.snapshot(for: context.date) ZStack { Color.black .ignoresSafeArea() NightSkyBackground() ScrollView { VStack(spacing: 22) { FloatingMoon(normalizedPhase: snapshot.normalizedPhase) .frame(maxWidth: 320, maxHeight: 320) .padding(.top, 32) .padding(.bottom, 4) VStack(spacing: 5) { Text(snapshot.namedPhase.rawValue) .font(.title.weight(.semibold)) .foregroundStyle(.white) .multilineTextAlignment(.center) Text(detailLine(snapshot)) .font(.subheadline) .foregroundStyle(.white.opacity(0.75)) .multilineTextAlignment(.center) Text(nextPhaseLine(snapshot)) .font(.caption) .foregroundStyle(.white.opacity(0.55)) .multilineTextAlignment(.center) Text(dateLine(snapshot.date)) .font(.caption2) .foregroundStyle(.white.opacity(0.40)) .multilineTextAlignment(.center) } .padding(.horizontal, 28) .padding(.vertical, 18) .glassEffect( .regular, in: RoundedRectangle(cornerRadius: 28, style: .continuous) ) .padding(.horizontal, 24) HemisphereSettingsCard( southernHemisphere: $southernHemisphere ) .padding(.horizontal, 24) } .frame(maxWidth: .infinity) .padding(.bottom, 40) } .scrollIndicators(.hidden) .scrollContentBackground(.hidden) .background(Color.clear) } } .environment(\.moonSouthernHemisphere, southernHemisphere) .preferredColorScheme(.dark) } private func detailLine(_ s: MoonPhaseSnapshot) -> String { let pct = Int((s.illuminatedFraction * 100).rounded()) let fmt = NumberFormatter() fmt.minimumFractionDigits = 1 fmt.maximumFractionDigits = 1 let age = fmt.string(from: NSNumber(value: s.moonAgeDays)) ?? String(format: "%.1f", s.moonAgeDays) return "\(pct)% illuminated ยท \(age) days old" } private func nextPhaseLine(_ s: MoonPhaseSnapshot) -> String { let days = s.daysToNextPhase if days < 1 { let hours = Int((days * 24).rounded()) return "Next: \(s.nextPhaseName.rawValue) in ~\(hours)h" } else { let d = String(format: "%.1f", days) return "Next: \(s.nextPhaseName.rawValue) in ~\(d) days" } } private func dateLine(_ date: Date) -> String { let df = DateFormatter() df.dateStyle = .medium df.timeStyle = .short return "Updated \(df.string(from: date))" } } private struct FloatingMoon: View { var normalizedPhase: Double var body: some View { TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { ctx in let t = ctx.date.timeIntervalSinceReferenceDate let dy = CGFloat(sin(t * 0.55) * 7) let dx = CGFloat(sin(t * 0.37 + 1.2) * 4) MoonDiskView(normalizedPhase: normalizedPhase) .offset(x: dx, y: dy) } } } private struct HemisphereSettingsCard: View { @Binding var southernHemisphere: Bool var body: some View { Toggle(isOn: $southernHemisphere) { Label { VStack(alignment: .leading, spacing: 2) { Text("Southern hemisphere") .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) Text("Mirror waxing/waning orientation") .font(.caption) .foregroundStyle(.white.opacity(0.55)) } } icon: { Image(systemName: "globe.americas.fill") .foregroundStyle(.white.opacity(0.85)) } } .tint(Color(red: 0.55, green: 0.62, blue: 1.0)) .padding(.horizontal, 20) .padding(.vertical, 16) .glassEffect( .regular, in: RoundedRectangle(cornerRadius: 24, style: .continuous) ) } } private struct NightSkyBackground: View { var body: some View { ZStack { LinearGradient( colors: [ Color(red: 0.02, green: 0.02, blue: 0.06), Color(red: 0.04, green: 0.04, blue: 0.14), Color(red: 0.06, green: 0.04, blue: 0.18), Color(red: 0.01, green: 0.01, blue: 0.04), ], startPoint: .top, endPoint: .bottom ) RadialGradient( colors: [ Color(red: 0.40, green: 0.18, blue: 0.55).opacity(0.45), Color(red: 0.30, green: 0.12, blue: 0.45).opacity(0.18), Color.clear, ], center: UnitPoint(x: 0.85, y: 0.15), startRadius: 0, endRadius: 520 ) .blendMode(.screen) .blur(radius: 40) RadialGradient( colors: [ Color(red: 0.10, green: 0.35, blue: 0.70).opacity(0.40), Color(red: 0.06, green: 0.20, blue: 0.50).opacity(0.16), Color.clear, ], center: UnitPoint(x: 0.05, y: 0.90), startRadius: 0, endRadius: 540 ) .blendMode(.screen) .blur(radius: 40) RadialGradient( colors: [ Color(red: 0.78, green: 0.42, blue: 0.85).opacity(0.20), Color.clear, ], center: UnitPoint(x: 0.6, y: 0.55), startRadius: 0, endRadius: 380 ) .blendMode(.screen) .blur(radius: 60) StarFieldView(starCount: 200) } .ignoresSafeArea() } } #Preview { ContentView() }