diff --git a/Sources/VoidBar/Services/WeatherStore.swift b/Sources/VoidBar/Services/WeatherStore.swift index 37f06b5..1b55540 100644 --- a/Sources/VoidBar/Services/WeatherStore.swift +++ b/Sources/VoidBar/Services/WeatherStore.swift @@ -1,10 +1,18 @@ import Foundation import Combine +struct HourlyWeather: Codable, Identifiable { + let id = UUID() + let time: Date + let temperature: Double + let condition: Int +} + struct WeatherData: Codable { let temperature: Double let condition: Int let locationName: String? + let hourly: [HourlyWeather] } @MainActor @@ -46,7 +54,7 @@ final class WeatherStore: ObservableObject { let ipResponse = try JSONDecoder().decode(IPResponse.self, from: locationData) // Get weather from Open-Meteo - let weatherUrlString = "https://api.open-meteo.com/v1/forecast?latitude=\(ipResponse.latitude)&longitude=\(ipResponse.longitude)¤t_weather=true" + let weatherUrlString = "https://api.open-meteo.com/v1/forecast?latitude=\(ipResponse.latitude)&longitude=\(ipResponse.longitude)¤t_weather=true&hourly=temperature_2m,weathercode&timezone=auto" guard let weatherUrl = URL(string: weatherUrlString) else { return } let (weatherJsonData, _) = try await URLSession.shared.data(from: weatherUrl) @@ -56,14 +64,39 @@ final class WeatherStore: ObservableObject { let temperature: Double let weathercode: Int } + struct HourlyData: Decodable { + let time: [String] + let temperature_2m: [Double] + let weathercode: [Int] + } let current_weather: CurrentWeather + let hourly: HourlyData? } let meteoResponse = try JSONDecoder().decode(MeteoResponse.self, from: weatherJsonData) + var hourlyForecast: [HourlyWeather] = [] + if let hourly = meteoResponse.hourly { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime, .withColonSeparatorInTimeZone] + // Open-Meteo timezone=auto returns format like "2023-08-07T12:00" without Z. + // We will use a simpler formatter. + let simpleFormatter = DateFormatter() + simpleFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm" + + let now = Date() + for i in 0..= now.addingTimeInterval(-3600) { + hourlyForecast.append(HourlyWeather(time: date, temperature: hourly.temperature_2m[i], condition: hourly.weathercode[i])) + if hourlyForecast.count >= 24 { break } // Only next 24 hours + } + } + } + self.weather = WeatherData( temperature: meteoResponse.current_weather.temperature, condition: meteoResponse.current_weather.weathercode, - locationName: ipResponse.city + locationName: ipResponse.city, + hourly: hourlyForecast ) self.error = nil } catch { diff --git a/Sources/VoidBar/UI/WeatherPane.swift b/Sources/VoidBar/UI/WeatherPane.swift index 2e57127..14042f6 100644 --- a/Sources/VoidBar/UI/WeatherPane.swift +++ b/Sources/VoidBar/UI/WeatherPane.swift @@ -24,6 +24,19 @@ struct WeatherPane: View { .font(.system(size: 11, weight: .medium)) .foregroundStyle(Theme.secondary) } + + if !weather.hourly.isEmpty { + Spacer(minLength: 8) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(weather.hourly) { hour in + hourlyItem(hour) + } + } + .padding(.horizontal, 16) + } + .frame(height: 60) + } } else { ProgressView() .controlSize(.small) @@ -45,4 +58,18 @@ struct WeatherPane: View { default: return "cloud.fill" } } + + private func hourlyItem(_ hour: HourlyWeather) -> some View { + VStack(spacing: 4) { + Text(hour.time, format: .dateTime.hour()) + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(Theme.secondary) + Image(systemName: icon(for: hour.condition)) + .font(.system(size: 14)) + .foregroundStyle(.white) + Text(String(format: "%.0f°", hour.temperature)) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.white) + } + } }