Files
WhiteNights_iOS/WhiteNights/Widgets/WeatherView.swift
15lu.akari a87a3d12ab big update
2025-08-26 23:37:39 +03:00

274 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
private let WEATHER_STATUS_MAP_RU: [String: String] = [
"Rain": "дождливо",
"Clouds": "облачно",
"Clear": "солнечно",
"Thunderstorm": "гроза",
"Snow": "снег",
"Drizzle": "мелкий дождь",
"Fog": "туман"
]
private let WEATHER_STATUS_MAP_ZH: [String: String] = [
"Rain": "下雨",
"Clouds": "多云",
"Clear": "晴朗",
"Thunderstorm": "雷雨",
"Snow": "下雪",
"Drizzle": "毛毛雨",
"Fog": ""
]
private let WEATHER_STATUS_MAP_EN: [String: String] = [
"Rain": "Rain",
"Clouds": "Cloudy",
"Clear": "Sunny",
"Thunderstorm": "Thunderstorm",
"Snow": "Snow",
"Drizzle": "Drizzle",
"Fog": "Fog"
]
struct FormattedWeather {
let temperature: Int
let statusCode: String
let precipitation: Int?
let windSpeed: Double?
var dayOfWeek: String?
let originalDate: Date? // добавлено для пересчета дня недели
func localizedStatus(language: String) -> String {
switch language {
case "ru": return WEATHER_STATUS_MAP_RU[statusCode] ?? statusCode
case "zh": return WEATHER_STATUS_MAP_ZH[statusCode] ?? statusCode
default: return WEATHER_STATUS_MAP_EN[statusCode] ?? statusCode
}
}
}
struct WeatherView: View {
@EnvironmentObject var appState: AppState
@State private var todayWeather: FormattedWeather?
@State private var forecast: [FormattedWeather] = []
var body: some View {
HStack(alignment: .center, spacing: 4) {
if let today = todayWeather {
VStack(alignment: .center, spacing: 2) {
Image(getWeatherIconName(for: today.localizedStatus(language: appState.selectedLanguage)))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36)
.padding(.bottom, 5)
Text("\(today.temperature)°")
.font(.system(size: 30, weight: .bold))
.foregroundColor(.white)
Text(today.localizedStatus(language: appState.selectedLanguage))
.font(.system(size: 12))
.foregroundColor(.white)
.multilineTextAlignment(.center)
}
VStack(alignment: .leading, spacing: 6) {
ForEach(forecast.prefix(3).indices, id: \.self) { index in
let day = forecast[index]
HStack(spacing: 4) {
Image(getWeatherIconName(for: day.localizedStatus(language: appState.selectedLanguage)))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
if let date = day.originalDate {
Text(getDayOfWeek(from: date))
.font(.system(size: 10))
.foregroundColor(.white)
}
Text("\(day.temperature)°")
.font(.system(size: 12))
.foregroundColor(.white)
.fontWeight(.bold)
}
}
Divider()
.background(Color.white.opacity(0.8))
.padding(.vertical, 1)
if let precipitation = today.precipitation {
HStack(spacing: 4) {
Image("det_humidity")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
Text("\(precipitation)%")
.font(.system(size: 12))
.foregroundColor(.white)
}
}
if let windSpeed = today.windSpeed {
HStack(spacing: 5) {
Image("det_wind_speed")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
Text(appState.selectedLanguage == "ru" ? "\(Int(windSpeed)) м/с" :
appState.selectedLanguage == "zh" ? "\(Int(windSpeed)) 米/秒" :
"\(Int(windSpeed)) m/s")
.font(.system(size: 12))
.foregroundColor(.white)
}
}
}
} else {
Text(appState.selectedLanguage == "ru" ? "Загрузка погоды..." :
appState.selectedLanguage == "zh" ? "正在加载天气..." :
"Loading weather...")
.foregroundColor(.white)
.padding()
}
}
.padding(.horizontal, 10)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fit)
.background(
ZStack {
Color(hex: 0x806C59)
LinearGradient(
stops: [
.init(color: .white.opacity(0.0), location: 0.0871),
.init(color: .white.opacity(0.16), location: 0.6969)
],
startPoint: .bottomLeading,
endPoint: .topTrailing
)
}
)
.cornerRadius(25)
.shadow(color: Color.black.opacity(0.10), radius: 8, x: 0, y: 2)
.task {
await fetchAndFormatWeather()
}
.onChange(of: appState.selectedLanguage) { _ in
// при смене языка день недели пересчитывается динамически в View
}
}
private func fetchAndFormatWeather() async {
let lat = 59.938784
let lng = 30.314997
guard let url = URL(string: "https://white-nights.krbl.ru/services/weather?lang=\(appState.selectedLanguage)") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters: [String: Any] = [
"coordinates": ["latitude": lat, "longitude": lng]
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
let (data, _) = try await URLSession.shared.data(for: request)
let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: data)
formatWeatherData(data: weatherResponse)
} catch {
print("Ошибка загрузки погоды: \(error)")
}
}
private func formatWeatherData(data: WeatherResponse) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
var calendar = Calendar.current
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
let middayForecast = data.forecast.filter { item in
guard let date = dateFormatter.date(from: item.date) else { return false }
return calendar.component(.hour, from: date) == 12
}
if let today = data.currentWeather {
todayWeather = FormattedWeather(
temperature: Int(today.temperatureCelsius.rounded()),
statusCode: today.description,
precipitation: today.humidity,
windSpeed: today.windSpeed,
dayOfWeek: nil,
originalDate: nil
)
}
var formattedForecast: [FormattedWeather] = []
for item in middayForecast.prefix(3) {
guard let date = dateFormatter.date(from: item.date) else { continue }
let averageTemp = (item.minTemperatureCelsius + item.maxTemperatureCelsius) / 2
formattedForecast.append(FormattedWeather(
temperature: Int(averageTemp.rounded()),
statusCode: item.description,
precipitation: item.humidity,
windSpeed: item.windSpeed,
dayOfWeek: getDayOfWeek(from: date),
originalDate: date
))
}
let daysToFill = 3 - formattedForecast.count
if daysToFill > 0 {
let baseDate = Date()
for i in 1...daysToFill {
guard let nextDay = Calendar.current.date(byAdding: .day, value: formattedForecast.count + i, to: baseDate) else { continue }
formattedForecast.append(FormattedWeather(
temperature: 0,
statusCode: "N/A",
precipitation: nil,
windSpeed: nil,
dayOfWeek: getDayOfWeek(from: nextDay),
originalDate: nextDay
))
}
}
self.forecast = formattedForecast
}
private func getDayOfWeek(from date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = appState.selectedLanguage == "ru" ? Locale(identifier: "ru_RU") :
appState.selectedLanguage == "zh" ? Locale(identifier: "zh_CN") :
Locale(identifier: "en_US")
formatter.dateFormat = "E"
return formatter.string(from: date).capitalized
}
private func getWeatherIconName(for status: String) -> String {
let normalizedStatus = status.lowercased()
switch normalizedStatus {
case "солнечно", "晴朗", "sunny":
return "cond_sunny"
case "облачно", "多云", "cloudy":
return "cond_cloudy"
case "дождливо", "мелкий дождь", "下雨", "毛毛雨", "rainy", "drizzle":
return "cond_rainy"
case "снег", "下雪", "snowy":
return "cond_snowy"
case "гроза", "雷雨", "thunderstorm":
return "cond_thunder"
case "туман", "", "fog":
return "det_humidity"
default:
return "cond_sunny"
}
}
}