Add MoonWatch app, widget extension, shared module, and .gitignore
Adds the real MoonWatch implementation on top of the initial Xcode template: new SwiftUI views (MoonDiskView, StarFieldView), the MoonWatchWidget extension, and a Shared module containing the moon phase calculator, shadow shape, and shared asset catalog. Also adds a Swift/Xcode/macOS .gitignore and untracks per-user xcuserdata files. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,14 +8,195 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@AppStorage("moonSouthernHemisphere") private var southernHemisphere = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
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)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user