diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-03-23-sunrise-sunset-widget-implementation.md | 539 |
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. + |
