274 lines
11 KiB
Swift
274 lines
11 KiB
Swift
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"
|
||
}
|
||
}
|
||
}
|