summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-03-23-sunrise-sunset-widget-implementation.md539
1 files changed, 539 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-03-23-sunrise-sunset-widget-implementation.md b/docs/superpowers/plans/2026-03-23-sunrise-sunset-widget-implementation.md
new file mode 100644
index 0000000..19c36fb
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-23-sunrise-sunset-widget-implementation.md
@@ -0,0 +1,539 @@
+# Sunrise/Sunset Widget Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Display sunrise and sunset times in HH:mm format on small and medium widgets, calculated locally using the user's location.
+
+**Architecture:** Widget Provider fetches cached user location from AppGroupManager, calculates sunrise/sunset times using the existing SunTimes utility, and passes formatted times to widget views. Gracefully handles unavailable locations by showing no times.
+
+**Tech Stack:**
+- SwiftUI WidgetKit (existing)
+- SunTimes (NOAA algorithm, already available)
+- AppGroupManager (existing shared storage)
+
+---
+
+## File Structure
+
+**Files to Modify:**
+- `Solsnu.Widget/Solsnu_Widget.swift` — Add time properties to SolvervDef, update Provider logic
+- `Solsnu.Widget/Views/SmallWidgetView.swift` — Display sunrise/sunset if available
+- `Solsnu.Widget/Views/MediumWidgetView.swift` — Display sunrise/sunset if available
+
+**Files to Create:**
+- None (all infrastructure already exists)
+
+---
+
+## Task 1: Add Properties to SolvervDef
+
+**Files:**
+- Modify: `Solsnu.Widget/Solsnu_Widget.swift:72-90`
+
+- [ ] **Step 1: Add sunrise/sunset Date properties to SolvervDef struct**
+
+Add these properties after `bg: String`:
+```swift
+let sunriseTime: Date?
+let sunsetTime: Date?
+```
+
+- [ ] **Step 2: Update existing constructors to accept nil times**
+
+Modify `init(date:Date)`:
+```swift
+init(date: Date, sunriseTime: Date? = nil, sunsetTime: Date? = nil) {
+ self.date = date
+ self.sunriseTime = sunriseTime
+ self.sunsetTime = sunsetTime
+ self.bg = "smallbg"
+}
+```
+
+Modify `init(utcString:String)`:
+```swift
+init(utcString: String, sunriseTime: Date? = nil, sunsetTime: Date? = nil) {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
+ let date = formatter.date(from: utcString)!
+ self.date = date
+ self.sunriseTime = sunriseTime
+ self.sunsetTime = sunsetTime
+ self.bg = "smallbg"
+}
+```
+
+- [ ] **Step 3: Add computed properties for formatted times**
+
+Add after `bg: String` property declarations:
+```swift
+var sunriseFormatted: String {
+ guard let time = sunriseTime else { return "" }
+ let formatter = DateFormatter()
+ formatter.timeStyle = .short
+ formatter.dateStyle = .none
+ return formatter.string(from: time)
+}
+
+var sunsetFormatted: String {
+ guard let time = sunsetTime else { return "" }
+ let formatter = DateFormatter()
+ formatter.timeStyle = .short
+ formatter.dateStyle = .none
+ return formatter.string(from: time)
+}
+```
+
+- [ ] **Step 4: Verify compilation**
+
+Build the widget target:
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+Expected: Build succeeds with no errors
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add Solsnu.Widget/Solsnu_Widget.swift
+git commit -m "feat: add sunrise/sunset properties to SolvervDef"
+```
+
+---
+
+## Task 2: Update Provider to Fetch and Calculate Sun Times
+
+**Files:**
+- Modify: `Solsnu.Widget/Solsnu_Widget.swift:11-37` (Provider struct and getTimeline method)
+
+- [ ] **Step 1: Import AppGroupManager in widget**
+
+Add to imports at top of Solsnu_Widget.swift:
+```swift
+import WidgetKit
+import SwiftUI
+// Add this:
+import Combine
+```
+
+Note: Check if you need to add AppGroupManager to the widget target. It may require adding the file to the widget's Build Phases.
+
+- [ ] **Step 2: Rewrite Provider.getTimeline() to fetch location**
+
+Replace the entire `getTimeline` method with:
+```swift
+func getTimeline(in context: Context, completion: @escaping (Timeline<SolvervEntry>) -> ()) {
+ var entries: [SolvervEntry] = []
+ let currentDate = Date()
+
+ // Fetch location from AppGroupManager
+ let location = AppGroupManager.shared.getLocation()
+
+ var sunriseTime: Date? = nil
+ var sunsetTime: Date? = nil
+
+ // If location exists and is less than 24 hours old, calculate sun times
+ if let location = location {
+ let locationFormatter = ISO8601DateFormatter()
+ if let locationTimestamp = locationFormatter.date(from: location.timestamp) {
+ let hoursSinceLocation = currentDate.timeIntervalSince(locationTimestamp) / 3600.0
+
+ if hoursSinceLocation < 24.0 {
+ // Calculate sun times using SunTimes utility
+ let calculator = SunTimes(
+ latitude: location.latitude,
+ longitude: location.longitude,
+ date: currentDate
+ )
+ sunriseTime = calculator.sunrise()
+ sunsetTime = calculator.sunset()
+ }
+ }
+ }
+
+ // Create entry with calculated times
+ let entry = SolvervEntry(
+ def: SolvervDef(
+ date: currentDate,
+ sunriseTime: sunriseTime,
+ sunsetTime: sunsetTime
+ )
+ )
+ entries.append(entry)
+
+ // Refresh at next midnight
+ let calendar = Calendar.current
+ var components = calendar.dateComponents([.year, .month, .day], from: currentDate)
+ components.hour = 0
+ components.minute = 0
+ components.second = 0
+ let todayMidnight = calendar.date(from: components)!
+ let nextMidnight = calendar.date(byAdding: .day, value: 1, to: todayMidnight)!
+
+ let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
+ completion(timeline)
+}
+```
+
+- [ ] **Step 3: Verify Provider needs SunTimes import**
+
+At the top of Solsnu_Widget.swift, verify you have:
+```swift
+import WidgetKit
+import SwiftUI
+```
+
+Note: SunTimes is in the main app target. You may need to either:
+- Move SunTimes to a shared framework, OR
+- Copy/duplicate SunTimes into the widget target, OR
+- Add SunTimes file to widget's Build Phases
+
+Check your project setup. If SunTimes isn't accessible to the widget, you'll need to add it to the widget's Build Phases.
+
+- [ ] **Step 4: Build and verify no errors**
+
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+Expected: Build succeeds
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add Solsnu.Widget/Solsnu_Widget.swift
+git commit -m "feat: add location fetching and sun time calculation to widget provider"
+```
+
+---
+
+## Task 3: Update SmallWidgetView to Display Times
+
+**Files:**
+- Modify: `Solsnu.Widget/Views/SmallWidgetView.swift`
+
+- [ ] **Step 1: Understand current SmallWidgetView layout**
+
+Read the file to see how it currently displays the countdown
+
+- [ ] **Step 2: Add sunrise/sunset display**
+
+Update `body` to include times if available:
+
+```swift
+var body: some View {
+ ZStack {
+ Image(entry.def.bg)
+ .resizable()
+ .scaledToFill()
+
+ VStack(spacing: 8) {
+ // Days until next event (existing)
+ Text("\(entry.def.daysUntilNext())")
+ .font(.system(size: 26, weight: .bold, design: .serif))
+ .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056))
+ .italic()
+
+ // Sunrise/Sunset times (new)
+ if !entry.def.sunriseFormatted.isEmpty && !entry.def.sunsetFormatted.isEmpty {
+ HStack(spacing: 4) {
+ Text("↑ \(entry.def.sunriseFormatted)")
+ Text("↓ \(entry.def.sunsetFormatted)")
+ }
+ .font(.system(size: 11, weight: .regular))
+ .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056))
+ }
+ }
+ .position(x: 50, y: 50)
+ }
+ .containerBackground(for: .widget, alignment: .center) { Color.clear }
+}
+```
+
+- [ ] **Step 3: Build and preview**
+
+Build the widget:
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+Expected: Build succeeds
+
+Check the preview in Xcode to see the new layout with times (if available)
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add Solsnu.Widget/Views/SmallWidgetView.swift
+git commit -m "feat: display sunrise/sunset times in small widget"
+```
+
+---
+
+## Task 4: Update MediumWidgetView to Display Times
+
+**Files:**
+- Modify: `Solsnu.Widget/Views/MediumWidgetView.swift`
+
+- [ ] **Step 1: Read current MediumWidgetView**
+
+Understand its current structure
+
+- [ ] **Step 2: Add sunrise/sunset display**
+
+Add the times to the medium widget view. Example approach:
+
+```swift
+var body: some View {
+ ZStack {
+ Image(entry.def.bg)
+ .resizable()
+ .scaledToFill()
+
+ VStack(alignment: .leading, spacing: 12) {
+ // Existing countdown content
+ Text("\(entry.def.daysUntilNext())")
+ .font(.system(size: 32, weight: .bold, design: .serif))
+ .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056))
+
+ // Sunrise/Sunset times
+ if !entry.def.sunriseFormatted.isEmpty && !entry.def.sunsetFormatted.isEmpty {
+ HStack {
+ Text("Sunrise: \(entry.def.sunriseFormatted)")
+ Spacer()
+ Text("Sunset: \(entry.def.sunsetFormatted)")
+ }
+ .font(.system(size: 12, weight: .regular))
+ .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056))
+ }
+ }
+ .padding()
+ }
+ .containerBackground(for: .widget, alignment: .topLeading) { Color.clear }
+}
+```
+
+(Adjust layout to match your design preferences)
+
+- [ ] **Step 3: Build and preview**
+
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+Expected: Build succeeds
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add Solsnu.Widget/Views/MediumWidgetView.swift
+git commit -m "feat: display sunrise/sunset times in medium widget"
+```
+
+---
+
+## Task 5: Handle SunTimes Availability in Widget
+
+**Files:**
+- Modify: `Solsnu.Widget/Solsnu_Widget.swift` (if SunTimes is not available)
+
+**Note:** This task only applies if SunTimes cannot be imported directly into the widget. If you got build errors in Task 2 Step 3, you need to copy SunTimes into the widget target.
+
+- [ ] **Step 1: Check if SunTimes is available in widget**
+
+Try to build:
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" 2>&1 | grep -i suntimes
+```
+
+If no errors: Skip to Step 4
+If error: Proceed to Step 2
+
+- [ ] **Step 2: Copy SunTimes to widget target (if needed)**
+
+If build failed because SunTimes isn't accessible:
+
+Copy the file:
+```bash
+cp Solverv/Utilities/SunTimes.swift Solsnu.Widget/Utilities/SunTimes.swift
+```
+
+Create the Utilities folder in widget if needed:
+```bash
+mkdir -p Solsnu.Widget/Utilities
+```
+
+- [ ] **Step 3: Add SunTimes to widget target in Xcode**
+
+In Xcode:
+1. Open the project
+2. Select `Solsnu.Widget` target
+3. Go to Build Phases → Compile Sources
+4. Add `Solsnu.Widget/Utilities/SunTimes.swift`
+
+Or via command line:
+```bash
+# This is typically done in Xcode UI
+```
+
+Build again:
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+Expected: Build succeeds
+
+- [ ] **Step 4: Commit (if changes made)**
+
+```bash
+git add Solsnu.Widget/Utilities/SunTimes.swift Solsnu.Widget/Solsnu_Widget.swift
+git commit -m "feat: add SunTimes calculator to widget target for sun time calculations"
+```
+
+If no changes needed, skip this commit.
+
+---
+
+## Task 6: Integration Testing
+
+**Files:**
+- Test: Solsnu_Widget preview in Xcode
+
+- [ ] **Step 1: Test with valid location**
+
+In Solsnu_Widget.swift preview, modify the preview to include sample sun times:
+
+```swift
+#Preview(as: .systemSmall) {
+ Solsnu_Widget()
+} timeline: {
+ let formatter = DateFormatter()
+ formatter.timeStyle = .short
+ formatter.dateStyle = .none
+
+ // Sample entry with sun times
+ let springDate = Calendar.current.date(from: DateComponents(year: 2026, month: 3, day: 20))!
+ let sunriseTime = Calendar.current.date(bySettingHour: 7, minute: 30, second: 0, of: springDate)!
+ let sunsetTime = Calendar.current.date(bySettingHour: 19, minute: 45, second: 0, of: springDate)!
+
+ SolvervEntry(
+ def: SolvervDef(
+ date: springDate,
+ sunriseTime: sunriseTime,
+ sunsetTime: sunsetTime
+ )
+ )
+}
+```
+
+Build the preview:
+```bash
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+- [ ] **Step 2: Verify widget displays times correctly**
+
+In Xcode, view the canvas/preview and confirm:
+- Sunrise time displays as HH:mm format
+- Sunset time displays as HH:mm format
+- Both times appear on the widget (layout may vary)
+
+- [ ] **Step 3: Test with nil times (no location)**
+
+Update preview to test with nil times:
+
+```swift
+SolvervEntry(
+ def: SolvervDef(
+ date: springDate,
+ sunriseTime: nil,
+ sunsetTime: nil
+ )
+)
+```
+
+Build and verify:
+- Widget displays without crashing
+- No times are shown
+- Layout remains clean (no empty space for times)
+
+- [ ] **Step 4: Test different locations**
+
+Manually verify with sample calculations:
+- Oslo (59.9°N, 10.7°E): Should show reasonable spring times
+- Equator (0°, 0°): Should show ~6am and ~6pm
+- Near poles: May show nil (handled gracefully)
+
+- [ ] **Step 5: Commit testing changes (if any)**
+
+```bash
+git add Solsnu.Widget/Solsnu_Widget.swift
+git commit -m "test: add preview entries with sunrise/sunset times"
+```
+
+---
+
+## Task 7: Final Integration & Verification
+
+**Files:**
+- Verify: All modified files
+
+- [ ] **Step 1: Run full build**
+
+```bash
+xcodebuild build -scheme Solverv -destination "generic/platform=iOS"
+xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS"
+```
+
+Expected: Both schemes build successfully with no warnings
+
+- [ ] **Step 2: Check widget refresh behavior**
+
+The widget should:
+- Refresh at midnight (existing policy maintained)
+- Show sun times if location is available and fresh (< 24 hours)
+- Show nothing for times if location is unavailable or stale
+
+This is verified automatically via the Provider logic.
+
+- [ ] **Step 3: Verify no regressions**
+
+Check that existing functionality still works:
+- Countdown still displays
+- Solstice events still calculate correctly
+- Widget background image still loads
+- Other widget configurations still work
+
+- [ ] **Step 4: Final commit and review**
+
+Review all changes:
+```bash
+git log --oneline -7
+```
+
+Should see commits for:
+1. Add sunrise/sunset properties to SolvervDef
+2. Add location fetching and sun time calculation to widget provider
+3. Display sunrise/sunset times in small widget
+4. Display sunrise/sunset times in medium widget
+5. (Optional) Add SunTimes to widget target
+6. (Optional) Add preview entries
+
+All changes complete ✅
+
+---
+
+## Notes
+
+- **AppGroupManager Dependency:** The widget must be able to access AppGroupManager to read cached location. Ensure both the main app and widget targets have access to this file or share it via a framework.
+
+- **SunTimes Availability:** The SunTimes calculator must be available to the widget. Either ensure it's in a shared framework or copy it to the widget target.
+
+- **Timezone Handling:** SunTimes already handles timezone conversion internally. Times are in device local time.
+
+- **Stale Location:** If location is older than 24 hours, no sun times are displayed. This aligns with the widget's daily refresh policy.
+
+- **Privacy:** Widget cannot request location permission—must rely on main app providing location via AppGroupManager.
+