From a87a3d12abe53ed14098298e41a03414ed199bed Mon Sep 17 00:00:00 2001 From: "15lu.akari" <92751432+mikaeloganesian@users.noreply.github.com> Date: Tue, 26 Aug 2025 23:37:39 +0300 Subject: [PATCH] big update --- WhiteNights/AppState.swift | 20 +- .../AppIcon.appiconset/1024-logo.png | Bin 0 -> 39977 bytes .../AppIcon.appiconset/Contents.json | 1 + .../bus_icon.imageset/Contents.json | 21 ++ .../Assets.xcassets/bus_icon.imageset/bus.svg | 11 + .../metro_blue_icon.imageset/Contents.json | 21 ++ .../metro_blue_icon.imageset/metroBlue.svg | 4 + .../metro_green_icon.imageset/Contents.json | 21 ++ .../metro_green_icon.imageset/metroGreen.svg | 4 + .../metro_orange_icon.imageset/Contents.json | 21 ++ .../metroOrange.svg | 4 + .../metro_purple_icon.imageset/Contents.json | 21 ++ .../metroPurple.svg | 4 + .../metro_red_icon.imageset/Contents.json | 21 ++ .../metro_red_icon.imageset/metroRed.svg | 4 + .../open_menu_icon.imageset/Contents.json | 21 ++ .../open_menu_icon.svg | 3 + .../train_icon.imageset/Contents.json | 21 ++ .../train_icon.imageset/train.svg | 4 + .../tram_icon.imageset/Contents.json | 21 ++ .../tram_icon.imageset/tram.svg | 12 + .../trolley_icon.imageset/Contents.json | 21 ++ .../trolley_icon.imageset/trolley.svg | 13 + .../Contents.json | 21 ++ .../waiting_screen_logo.imageset/logo.svg | 3 + WhiteNights/ContentView.swift | 59 +++- WhiteNights/WhiteNightsApp.swift | 16 +- WhiteNights/Widgets/BottomMenu.swift | 329 +++++++++++------- WhiteNights/Widgets/RouteSelectionView.swift | 20 +- WhiteNights/Widgets/RouteView.swift | 71 +++- WhiteNights/Widgets/SightView.swift | 75 ++-- WhiteNights/Widgets/SightViewModel.swift | 29 +- WhiteNights/Widgets/VisualEffectBlur.swift | 4 +- WhiteNights/Widgets/WeatherView.swift | 164 +++++---- 34 files changed, 803 insertions(+), 282 deletions(-) create mode 100644 WhiteNights/Assets.xcassets/AppIcon.appiconset/1024-logo.png create mode 100644 WhiteNights/Assets.xcassets/bus_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/bus_icon.imageset/bus.svg create mode 100644 WhiteNights/Assets.xcassets/metro_blue_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/metro_blue_icon.imageset/metroBlue.svg create mode 100644 WhiteNights/Assets.xcassets/metro_green_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/metro_green_icon.imageset/metroGreen.svg create mode 100644 WhiteNights/Assets.xcassets/metro_orange_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/metro_orange_icon.imageset/metroOrange.svg create mode 100644 WhiteNights/Assets.xcassets/metro_purple_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/metro_purple_icon.imageset/metroPurple.svg create mode 100644 WhiteNights/Assets.xcassets/metro_red_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/metro_red_icon.imageset/metroRed.svg create mode 100644 WhiteNights/Assets.xcassets/open_menu_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/open_menu_icon.imageset/open_menu_icon.svg create mode 100644 WhiteNights/Assets.xcassets/train_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/train_icon.imageset/train.svg create mode 100644 WhiteNights/Assets.xcassets/tram_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/tram_icon.imageset/tram.svg create mode 100644 WhiteNights/Assets.xcassets/trolley_icon.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/trolley_icon.imageset/trolley.svg create mode 100644 WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/Contents.json create mode 100644 WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/logo.svg diff --git a/WhiteNights/AppState.swift b/WhiteNights/AppState.swift index 06b4461..d31bfe5 100644 --- a/WhiteNights/AppState.swift +++ b/WhiteNights/AppState.swift @@ -18,11 +18,24 @@ class AppState: ObservableObject { @Published var allRoutes: [Route] = [] @Published var sightId: Int? = nil - @Published var sights: [SightModel] = [] // <- все достопримечательности маршрута + @Published var sights: [SightModel] = [] // все достопримечательности маршрута + + @Published var selectedLanguage: String = "ru" { + didSet { + // если язык поменялся и маршрут уже выбран, перезагружаем достопримечательности + if let routeId = selectedRoute?.id { + Task { + await fetchSights(for: routeId) + } + } + } + } +// язык с начальным значением "ru" // MARK: - Fetch Sights private func fetchSights(for routeId: Int) async { - let urlString = "https://white-nights.krbl.ru/services/content/route/\(routeId)/sight" + // Добавляем параметр выбранного языка + let urlString = "https://white-nights.krbl.ru/services/content/route/\(routeId)/sight?lang=\(selectedLanguage)" guard let url = URL(string: urlString) else { return } do { @@ -50,7 +63,8 @@ class AppState: ObservableObject { private func preloadThumbnails(for sights: [SightModel]) { let session = URLSession.shared for sight in sights { - guard let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.thumbnail)/download") else { continue } + // Миниатюры тоже адаптируем под выбранный язык + guard let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.thumbnail)/download?lang=\(selectedLanguage)") else { continue } let request = URLRequest(url: url) // Если уже в кэше, пропускаем diff --git a/WhiteNights/Assets.xcassets/AppIcon.appiconset/1024-logo.png b/WhiteNights/Assets.xcassets/AppIcon.appiconset/1024-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..858af84b0b2430ff994893cfaf8ead1e35bd4d58 GIT binary patch literal 39977 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 literal 0 HcmV?d00001 diff --git a/WhiteNights/Assets.xcassets/AppIcon.appiconset/Contents.json b/WhiteNights/Assets.xcassets/AppIcon.appiconset/Contents.json index 532cd72..1398365 100644 --- a/WhiteNights/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/WhiteNights/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "1024-logo.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/WhiteNights/Assets.xcassets/bus_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/bus_icon.imageset/Contents.json new file mode 100644 index 0000000..206796e --- /dev/null +++ b/WhiteNights/Assets.xcassets/bus_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bus.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/bus_icon.imageset/bus.svg b/WhiteNights/Assets.xcassets/bus_icon.imageset/bus.svg new file mode 100644 index 0000000..00f52e2 --- /dev/null +++ b/WhiteNights/Assets.xcassets/bus_icon.imageset/bus.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/WhiteNights/Assets.xcassets/metro_blue_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/metro_blue_icon.imageset/Contents.json new file mode 100644 index 0000000..e10e8c0 --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_blue_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "metroBlue.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/metro_blue_icon.imageset/metroBlue.svg b/WhiteNights/Assets.xcassets/metro_blue_icon.imageset/metroBlue.svg new file mode 100644 index 0000000..884b565 --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_blue_icon.imageset/metroBlue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/WhiteNights/Assets.xcassets/metro_green_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/metro_green_icon.imageset/Contents.json new file mode 100644 index 0000000..7f204d5 --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_green_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "metroGreen.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/metro_green_icon.imageset/metroGreen.svg b/WhiteNights/Assets.xcassets/metro_green_icon.imageset/metroGreen.svg new file mode 100644 index 0000000..039f95a --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_green_icon.imageset/metroGreen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/WhiteNights/Assets.xcassets/metro_orange_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/metro_orange_icon.imageset/Contents.json new file mode 100644 index 0000000..c3ee67e --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_orange_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "metroOrange.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/metro_orange_icon.imageset/metroOrange.svg b/WhiteNights/Assets.xcassets/metro_orange_icon.imageset/metroOrange.svg new file mode 100644 index 0000000..8b27624 --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_orange_icon.imageset/metroOrange.svg @@ -0,0 +1,4 @@ + + + + diff --git a/WhiteNights/Assets.xcassets/metro_purple_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/metro_purple_icon.imageset/Contents.json new file mode 100644 index 0000000..164caef --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_purple_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "metroPurple.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/metro_purple_icon.imageset/metroPurple.svg b/WhiteNights/Assets.xcassets/metro_purple_icon.imageset/metroPurple.svg new file mode 100644 index 0000000..6813ae1 --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_purple_icon.imageset/metroPurple.svg @@ -0,0 +1,4 @@ + + + + diff --git a/WhiteNights/Assets.xcassets/metro_red_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/metro_red_icon.imageset/Contents.json new file mode 100644 index 0000000..52d530e --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_red_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "metroRed.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/metro_red_icon.imageset/metroRed.svg b/WhiteNights/Assets.xcassets/metro_red_icon.imageset/metroRed.svg new file mode 100644 index 0000000..9db2a0e --- /dev/null +++ b/WhiteNights/Assets.xcassets/metro_red_icon.imageset/metroRed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/WhiteNights/Assets.xcassets/open_menu_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/open_menu_icon.imageset/Contents.json new file mode 100644 index 0000000..a64f174 --- /dev/null +++ b/WhiteNights/Assets.xcassets/open_menu_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "open_menu_icon.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/open_menu_icon.imageset/open_menu_icon.svg b/WhiteNights/Assets.xcassets/open_menu_icon.imageset/open_menu_icon.svg new file mode 100644 index 0000000..a83b0fe --- /dev/null +++ b/WhiteNights/Assets.xcassets/open_menu_icon.imageset/open_menu_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/WhiteNights/Assets.xcassets/train_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/train_icon.imageset/Contents.json new file mode 100644 index 0000000..d4310ef --- /dev/null +++ b/WhiteNights/Assets.xcassets/train_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "train.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/train_icon.imageset/train.svg b/WhiteNights/Assets.xcassets/train_icon.imageset/train.svg new file mode 100644 index 0000000..60bc427 --- /dev/null +++ b/WhiteNights/Assets.xcassets/train_icon.imageset/train.svg @@ -0,0 +1,4 @@ + + + + diff --git a/WhiteNights/Assets.xcassets/tram_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/tram_icon.imageset/Contents.json new file mode 100644 index 0000000..04a616c --- /dev/null +++ b/WhiteNights/Assets.xcassets/tram_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "tram.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/tram_icon.imageset/tram.svg b/WhiteNights/Assets.xcassets/tram_icon.imageset/tram.svg new file mode 100644 index 0000000..b01c9fc --- /dev/null +++ b/WhiteNights/Assets.xcassets/tram_icon.imageset/tram.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/WhiteNights/Assets.xcassets/trolley_icon.imageset/Contents.json b/WhiteNights/Assets.xcassets/trolley_icon.imageset/Contents.json new file mode 100644 index 0000000..0e60a56 --- /dev/null +++ b/WhiteNights/Assets.xcassets/trolley_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "trolley.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/trolley_icon.imageset/trolley.svg b/WhiteNights/Assets.xcassets/trolley_icon.imageset/trolley.svg new file mode 100644 index 0000000..eab02b6 --- /dev/null +++ b/WhiteNights/Assets.xcassets/trolley_icon.imageset/trolley.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/Contents.json b/WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/Contents.json new file mode 100644 index 0000000..6777028 --- /dev/null +++ b/WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "logo.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/logo.svg b/WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/logo.svg new file mode 100644 index 0000000..dc4c21e --- /dev/null +++ b/WhiteNights/Assets.xcassets/waiting_screen_logo.imageset/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/WhiteNights/ContentView.swift b/WhiteNights/ContentView.swift index 1a983ac..58bc4e5 100644 --- a/WhiteNights/ContentView.swift +++ b/WhiteNights/ContentView.swift @@ -3,10 +3,12 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var appState: AppState @State private var showMenu = false + @State private var isLoading = true // состояние загрузки var body: some View { NavigationStack { ZStack { + // Основной контент VStack(spacing: 10) { HStack(spacing: 10) { RouteView() @@ -22,8 +24,10 @@ struct ContentView: View { } } .padding() + .blur(radius: isLoading ? 5 : 0) // слегка размываем, пока загрузка + .disabled(isLoading) // блокируем взаимодействие с контентом при загрузке - // Плавающая кнопка в правом нижнем углу + // Плавающая кнопка VStack { Spacer() HStack { @@ -33,35 +37,60 @@ struct ContentView: View { showMenu.toggle() } }) { - Image(systemName: "line.3.horizontal") - .font(.system(size: 24, weight: .bold)) - .foregroundColor(.white) - .padding() - .background(Color.blue) + Image("open_menu_icon") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + .padding(10) + .background(Color(hex: 0x806C59)) .clipShape(Circle()) .shadow(radius: 5) } .padding(.trailing, 20) - // .padding(.bottom, -20) // Убираем отрицательный паддинг - .padding(.bottom, -20) // Добавляем паддинг, чтобы кнопка не перекрывалась - + .padding(.bottom, -20) } } - // Используем кастомное BottomMenu + // BottomMenu if showMenu { - // Используем Binding для двусторонней связи BottomMenu(isPresented: $showMenu) .transition(.move(edge: .bottom)) - // Добавляем игнорирование safe area сверху, если BottomMenu - // должно полностью закрывать контент, но обычно для - // BottomSheet это не требуется, GeometryReader в BottomMenu - // уже делает нужное растяжение. + .animation(.spring(response: 0.35, dampingFraction: 0.9), value: showMenu) + } + + // Экран загрузки + if isLoading { + ZStack { + Color(hex: 0x806C59) + .edgesIgnoringSafeArea(.all) + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.5) + VStack { + Spacer() + HStack { + Spacer() + Image("waiting_screen_logo") + .resizable() + .scaledToFit() + .frame(width: 200) + .padding(20) + } + } + } + .transition(.opacity) + .zIndex(1) } } } .task { await fetchRoutes() + // Убираем экран загрузки через 2 секунды + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation(.easeOut(duration: 0.5)) { + isLoading = false + } + } } .preferredColorScheme(.light) } diff --git a/WhiteNights/WhiteNightsApp.swift b/WhiteNights/WhiteNightsApp.swift index 8d54cff..d76b2b9 100644 --- a/WhiteNights/WhiteNightsApp.swift +++ b/WhiteNights/WhiteNightsApp.swift @@ -1,20 +1,20 @@ -// -// WhiteNightsApp.swift -// WhiteNights -// -// Created by Микаэл Оганесян on 24.08.2025. -// - import SwiftUI +import SDWebImageSVGCoder // <-- импортируем SVG кодер @main struct WhiteNightsApp: App { @StateObject private var appState = AppState() + init() { + // Регистрируем SVG кодер + let svgCoder = SDImageSVGCoder.shared + SDImageCodersManager.shared.addCoder(svgCoder) + } + var body: some Scene { WindowGroup { ContentView() - .environmentObject(appState) // <- обязательно! + .environmentObject(appState) } } } diff --git a/WhiteNights/Widgets/BottomMenu.swift b/WhiteNights/Widgets/BottomMenu.swift index ee9e507..95ccbe4 100644 --- a/WhiteNights/Widgets/BottomMenu.swift +++ b/WhiteNights/Widgets/BottomMenu.swift @@ -1,6 +1,7 @@ import SwiftUI import SDWebImageSwiftUI +// MARK: - String Extension private extension String { var trimmedNonEmpty: String? { let t = trimmingCharacters(in: .whitespacesAndNewlines) @@ -9,37 +10,52 @@ private extension String { } // MARK: - ViewModel -@MainActor final class StopsViewModel: ObservableObject { - @Published var stops: [StopDetail] = [] + @Published var stops: [Stop] = [] @Published var isLoading: Bool = false - @Published var selectedStopId: Int? + @Published var selectedStopDetail: StopDetail? - func fetchStops(for routeId: Int) { - guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeId)/station") else { return } + private var detailCache: [Int: StopDetail] = [:] + + // MARK: - Fetch Stops With Transfers + func fetchStops(routeId: Int, language: String = "ru") { + guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeId)/station?lang=\(language)") else { return } isLoading = true - Task { + URLSession.shared.dataTask(with: url) { data, _, error in + DispatchQueue.main.async { self.isLoading = false } + guard let data = data, error == nil else { return } + do { - let (data, _) = try await URLSession.shared.data(from: url) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let stops = try decoder.decode([StopDetail].self, from: data) - self.stops = stops + + let stopDetails = try decoder.decode([StopDetail].self, from: data) + + // Раздаем детали по остановкам + var stops: [Stop] = [] + var cache: [Int: StopDetail] = [:] + for detail in stopDetails { + cache[detail.id] = detail + stops.append(Stop(id: detail.id, name: detail.name)) + } + + DispatchQueue.main.async { + self.stops = stops + self.detailCache = cache + } } catch { - print("Ошибка загрузки остановок:", error) - self.stops = [] + print("Parse stops error:", error) } - self.isLoading = false - } + }.resume() } func toggleStop(id: Int) { - withAnimation(.easeInOut(duration: 0.25)) { - if selectedStopId == id { - selectedStopId = nil + withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) { + if selectedStopDetail?.id == id { + selectedStopDetail = nil } else { - selectedStopId = id + selectedStopDetail = detailCache[id] } } } @@ -63,12 +79,19 @@ struct BottomMenu: View { var body: some View { GeometryReader { geo in ZStack { - if isPresented { - Color.black.opacity(0.4) - .ignoresSafeArea() - .onTapGesture { isPresented = false } - .transition(.opacity) - } + VisualEffectBlur(blurStyle: .systemUltraThinMaterialDark) + .ignoresSafeArea() + .mask( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black.opacity(0), location: 0), + .init(color: Color.black.opacity(1), location: 0.25), + .init(color: Color.black.opacity(1), location: 1) + ]), + startPoint: .top, + endPoint: .bottom + ) + ).onTapGesture { isPresented = false } VStack { Spacer() @@ -80,101 +103,19 @@ struct BottomMenu: View { .padding(.top, 8) VStack(spacing: 12) { - menuButton(title: "Достопримечательности", tab: .sights) - menuButton(title: "Остановки", tab: .stops) + menuButton(title: appState.selectedLanguage == "ru" ? "Достопримечательности" : appState.selectedLanguage == "zh" ? "景点" : "Sights", tab: .sights) + menuButton(title: appState.selectedLanguage == "ru" ? "Остановки" : appState.selectedLanguage == "zh" ? "车站" : "Stops", tab: .stops) if selectedTab == .sights { - // --- Достопримечательности --- - ScrollView { - LazyVGrid(columns: columns, spacing: 20) { - ForEach(appState.sights) { sight in - VStack(spacing: 8) { - SightThumbnail( - url: URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.thumbnail)/download"), - size: 80 - ) - .onTapGesture { - appState.sightId = sight.id - isPresented = false - } - - Text(sight.name) - .font(.footnote) - .multilineTextAlignment(.center) - .foregroundColor(.white) - .lineLimit(2) - .frame(maxHeight: .infinity, alignment: .top) - } - } - } - .padding(.horizontal, 20) - .padding(.vertical, 10) - } + sightsView } else { - // --- Остановки + пересадки --- - ScrollView { - LazyVStack(spacing: 0) { - if stopsVM.isLoading { - ProgressView("Загрузка остановок...") - .padding() - } else { - ForEach(stopsVM.stops) { stop in - VStack(alignment: .leading, spacing: 0) { - Button { - stopsVM.toggleStop(id: stop.id) - } label: { - Text(stop.name) - .font(.subheadline) // меньше размер - .frame(maxWidth: .infinity, alignment: .leading) // левое выравнивание - .padding(.vertical, 10) - .padding(.horizontal, 20) - .foregroundColor(.white) - .transaction { $0.animation = nil } - } - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(.white.opacity(0.3)), - alignment: .bottom - ) - - if stopsVM.selectedStopId == stop.id { - transfersView(for: stop) - .transition(.opacity.combined(with: .move(edge: .top))) - } - } - } - } - } - } - .onAppear { - if stopsVM.stops.isEmpty, - let routeId = appState.selectedRoute?.id { - stopsVM.fetchStops(for: routeId) - } - } + stopsView } } .padding(.horizontal, 20) .padding(.top, 2) - HStack(spacing: 16) { - Image("GAT_Icon") - .resizable() - .scaledToFit() - .frame(height: 30) - - Spacer() - - HStack(spacing: 2) { - Image("ru_lang_icon").resizable().scaledToFit().frame(width: 24, height: 24) - Image("zh_lang_icon").resizable().scaledToFit().frame(width: 24, height: 24) - Image("en_lang_icon").resizable().scaledToFit().frame(width: 24, height: 24) - } - } - .padding(.horizontal, 26) - .padding(.top, 16) - .padding(.bottom, 32) + menuFooter } .frame(height: geo.size.height * 0.8) .frame(maxWidth: .infinity) @@ -184,14 +125,12 @@ struct BottomMenu: View { .shadow(radius: 10) ) .offset(y: isPresented ? dragOffset : geo.size.height) - .animation(.spring(response: 0.35, dampingFraction: 0.9), value: isPresented) - .animation(.spring(response: 0.35, dampingFraction: 0.9), value: dragOffset) + .animation(.spring(response: 0.4, dampingFraction: 0.85), value: isPresented) + .animation(.spring(response: 0.4, dampingFraction: 0.85), value: dragOffset) .gesture( DragGesture() .onChanged { value in - if value.translation.height > 0 { - dragOffset = value.translation.height - } + if value.translation.height > 0 { dragOffset = value.translation.height } } .onEnded { value in if value.translation.height > 100 { isPresented = false } @@ -209,8 +148,9 @@ struct BottomMenu: View { private func menuButton(title: String, tab: Tab) -> some View { Button { selectedTab = tab } label: { Text(title) + .font(.system(size: 14)) .frame(maxWidth: .infinity) - .frame(height: 47) + .frame(height: 40) .foregroundColor(.white) .background( RoundedRectangle(cornerRadius: 16) @@ -220,6 +160,146 @@ struct BottomMenu: View { .buttonStyle(.plain) } + // MARK: - Sights View + @ViewBuilder + private var sightsView: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(appState.sights) { sight in + VStack(spacing: 8) { + SightThumbnail( + url: URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.thumbnail)/download?lang=\(appState.selectedLanguage)"), + size: 80 + ) + .onTapGesture { + appState.sightId = sight.id + isPresented = false + } + + Text(sight.name) + .font(.footnote) + .multilineTextAlignment(.center) + .foregroundColor(.white) + .lineLimit(2) + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 10) + }.mask( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black.opacity(0), location: 0), + .init(color: Color.black, location: 0.05), + .init(color: Color.black, location: 0.95), + .init(color: Color.black.opacity(0), location: 1) + ]), + startPoint: .top, + endPoint: .bottom + ) + ) + } + + // MARK: - Stops View + @ViewBuilder + private var stopsView: some View { + ScrollView { + LazyVStack(spacing: 10) { + if stopsVM.isLoading { + ProgressView(appState.selectedLanguage == "ru" ? "Загрузка остановок..." : appState.selectedLanguage == "zh" ? "加载车站..." : "Loading stops...") + .padding() + } else { + ForEach(stopsVM.stops) { stop in + VStack(alignment: .leading, spacing: 0) { + Button { + stopsVM.toggleStop(id: stop.id) + } label: { + Text(stop.name) + .font(.subheadline) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + .multilineTextAlignment(.leading) + .padding(.vertical, 8) + .padding(.horizontal, 20) + } + .overlay( + Rectangle() + .frame(height: 1) + .foregroundColor(.white.opacity(0.3)), + alignment: .bottom + ) + + if stopsVM.selectedStopDetail?.id == stop.id, + let detail = stopsVM.selectedStopDetail { + transfersView(for: detail) + .transition(.opacity.combined(with: .scale(scale: 0.95, anchor: .top))) + } + } + } + } + } + } + .mask( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black.opacity(0), location: 0), + .init(color: Color.black, location: 0.05), + .init(color: Color.black, location: 0.95), + .init(color: Color.black.opacity(0), location: 1) + ]), + startPoint: .top, + endPoint: .bottom + ) + ) + .onAppear { + if stopsVM.stops.isEmpty, let routeId = appState.selectedRoute?.id { + stopsVM.fetchStops(routeId: routeId, language: appState.selectedLanguage) + } + } + .animation(.easeInOut(duration: 0.25), + value: stopsVM.selectedStopDetail?.id) + } + + // MARK: - Footer + @ViewBuilder + private var menuFooter: some View { + HStack(spacing: 16) { + Image("GAT_Icon") + .resizable() + .scaledToFit() + .frame(height: 30) + + Spacer() + + HStack(spacing: 4) { + languageButton(imageName: "ru_lang_icon", code: "ru") + languageButton(imageName: "zh_lang_icon", code: "zh") + languageButton(imageName: "en_lang_icon", code: "en") + } + } + .padding(.horizontal, 26) + .padding(.top, 16) + .padding(.bottom, 32) + } + + @ViewBuilder + private func languageButton(imageName: String, code: String) -> some View { + Button { + appState.selectedLanguage = code + // Перезагрузка остановок при смене языка + if let routeId = appState.selectedRoute?.id { + stopsVM.fetchStops(routeId: routeId, language: code) + } + } label: { + Image(imageName) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + .brightness(appState.selectedLanguage == code ? 0.5 : 0) // осветление выбранного языка + } + .buttonStyle(.plain) + } + // MARK: - Transfers View @ViewBuilder private func transfersView(for stop: StopDetail) -> some View { @@ -240,31 +320,30 @@ struct BottomMenu: View { } if items.isEmpty { - Text("Нет пересадок") - .font(.caption) // меньше размер + Text(appState.selectedLanguage == "ru" ? "Нет пересадок" : appState.selectedLanguage == "zh" ? "没有换乘" : "No transfers") + .font(.caption2) .foregroundColor(.white.opacity(0.7)) .padding(.leading, 20) - .padding(.vertical, 8) + .padding(.vertical, 4) } else { - VStack(alignment: .leading, spacing: 6) { // меньше расстояние + VStack(alignment: .leading, spacing: 6) { ForEach(items, id: \.0) { icon, text in - HStack(spacing: 8) { + HStack(spacing: 6) { Image(icon) .resizable() .scaledToFit() .frame(width: 18, height: 18) Text(text) - .font(.caption) // меньше размер + .font(.caption2) .foregroundColor(.white) .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .leading) // левое выравнивание } .padding(.leading, 20) - .padding(.vertical, 4) + .padding(.vertical, 2) } } .padding(.bottom, 8) - .padding(.top, 6) + .padding(.top, 4) } } } diff --git a/WhiteNights/Widgets/RouteSelectionView.swift b/WhiteNights/Widgets/RouteSelectionView.swift index a868068..1270357 100644 --- a/WhiteNights/Widgets/RouteSelectionView.swift +++ b/WhiteNights/Widgets/RouteSelectionView.swift @@ -2,6 +2,7 @@ import SwiftUI struct RouteSelectionView: View { @EnvironmentObject var appState: AppState + @Environment(\.dismiss) private var dismiss @State private var routes: [Route] = [] @State private var isLoading = true @@ -9,12 +10,17 @@ struct RouteSelectionView: View { var body: some View { VStack { if isLoading { - ProgressView("Загрузка маршрутов...") - .padding() + ProgressView( + appState.selectedLanguage == "ru" ? "Загрузка маршрутов..." : + appState.selectedLanguage == "zh" ? "正在加载路线..." : + "Loading routes..." + ) + .padding() } else { List(routes, id: \.id) { route in Button(action: { appState.selectedRoute = route + dismiss() }) { HStack { Text("\(route.routeNumber)") @@ -26,15 +32,21 @@ struct RouteSelectionView: View { .listStyle(PlainListStyle()) } } - .navigationTitle("Выберите маршрут") + .navigationTitle( + appState.selectedLanguage == "ru" ? "Выберите маршрут" : + appState.selectedLanguage == "zh" ? "选择路线" : + "Select route" + ) .onAppear { Task { await fetchRoutes() } } + .onChange(of: appState.selectedLanguage) { _ in + // просто перерисовываем view, navigationTitle автоматически обновится + } } - // MARK: - Fetch Routes private func fetchRoutes() async { isLoading = true defer { isLoading = false } diff --git a/WhiteNights/Widgets/RouteView.swift b/WhiteNights/Widgets/RouteView.swift index b878194..f8f6865 100644 --- a/WhiteNights/Widgets/RouteView.swift +++ b/WhiteNights/Widgets/RouteView.swift @@ -5,7 +5,7 @@ struct RouteView: View { @State private var firstStationName: String = "Загрузка..." @State private var lastStationName: String = "Загрузка..." - @State private var engStationsName: String = "Загрузка..." + @State private var stationsRangeName: String = "Загрузка..." private var topBackgroundColor = Color(hex: 0xFCD500) @@ -42,7 +42,7 @@ struct RouteView: View { foregroundColor: .white ) MarqueeText( - text: engStationsName, + text: stationsRangeName, font: .caption, foregroundColor: .white.opacity(0.5) ) @@ -76,33 +76,72 @@ struct RouteView: View { await fetchStations(forRoute: routeID) } } + .onChange(of: appState.selectedLanguage) { _ in + if let routeID = appState.selectedRoute?.id { + Task { + await fetchStations(forRoute: routeID) + } + } + } } // MARK: - Fetch Stations private func fetchStations(forRoute routeID: Int) async { - firstStationName = "Загрузка..." - lastStationName = "Загрузка..." - engStationsName = "Loading..." - - guard let url = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station"), - let urlEng = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station?lang=en") else { return } + // текст загрузки + switch appState.selectedLanguage { + case "ru": + firstStationName = "Загрузка..." + lastStationName = "Загрузка..." + stationsRangeName = "Loading..." + case "zh": + firstStationName = "正在加载..." + lastStationName = "正在加载..." + stationsRangeName = "正在加载..." + default: + firstStationName = "Loading..." + lastStationName = "Loading..." + stationsRangeName = "Loading..." + } do { - let (data, _) = try await URLSession.shared.data(from: url) - let (dataEn, _) = try await URLSession.shared.data(from: urlEng) + // Загружаем станции на русском для диапазона + guard let urlRu = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station?lang=ru") else { return } + let (dataRu, _) = try await URLSession.shared.data(from: urlRu) + let stationsRu = try JSONDecoder().decode([Station].self, from: dataRu) - let stations = try JSONDecoder().decode([Station].self, from: data) + // Загружаем станции на английском для диапазона + guard let urlEn = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station?lang=en") else { return } + let (dataEn, _) = try await URLSession.shared.data(from: urlEn) let stationsEn = try JSONDecoder().decode([Station].self, from: dataEn) - if let firstStation = stations.first { firstStationName = firstStation.name } - if let lastStation = stations.last { lastStationName = lastStation.name } + // Загружаем станции на языке интерфейса для отображения first/last + let langCode = appState.selectedLanguage + guard let urlCurrent = URL(string: "https://white-nights.krbl.ru/services/content/route/\(routeID)/station?lang=\(langCode)") else { return } + let (dataCurrent, _) = try await URLSession.shared.data(from: urlCurrent) + let stationsCurrent = try JSONDecoder().decode([Station].self, from: dataCurrent) - let firstStationEn = stationsEn.first?.name ?? "Loading..." - let lastStationEn = stationsEn.last?.name ?? "Loading..." - engStationsName = "\(firstStationEn) - \(lastStationEn)" + // Устанавливаем first и last на текущем языке интерфейса + if let first = stationsCurrent.first { firstStationName = first.name } + if let last = stationsCurrent.last { lastStationName = last.name } + + // Диапазон станций + if appState.selectedLanguage == "ru" { + // для русского языка диапазон на английском + let firstEn = stationsEn.first?.name ?? "" + let lastEn = stationsEn.last?.name ?? "" + stationsRangeName = "\(firstEn) - \(lastEn)" + } else { + // для всех остальных языков диапазон на русском + let firstRu = stationsRu.first?.name ?? "" + let lastRu = stationsRu.last?.name ?? "" + stationsRangeName = "\(firstRu) - \(lastRu)" + } } catch { print("Ошибка загрузки станций: \(error)") + firstStationName = appState.selectedLanguage == "ru" ? "Ошибка загрузки" : appState.selectedLanguage == "zh" ? "加载失败" : "Failed to load" + lastStationName = appState.selectedLanguage == "ru" ? "Ошибка загрузки" : appState.selectedLanguage == "zh" ? "加载失败" : "Failed to load" + stationsRangeName = appState.selectedLanguage == "ru" ? "Ошибка загрузки" : appState.selectedLanguage == "zh" ? "加载失败" : "Failed to load" } } } diff --git a/WhiteNights/Widgets/SightView.swift b/WhiteNights/Widgets/SightView.swift index 5f7c103..732ee2e 100644 --- a/WhiteNights/Widgets/SightView.swift +++ b/WhiteNights/Widgets/SightView.swift @@ -1,27 +1,28 @@ import SwiftUI import AVKit import NukeUI -import UIKit - - -// MARK: - SightView struct SightView: View { let sightId: Int @StateObject private var viewModel = SightViewModel() + @EnvironmentObject private var appState: AppState var body: some View { VStack(alignment: .leading, spacing: 4) { mediaSection - + VStack(alignment: .leading, spacing: 8) { // Заголовок статьи - Text(viewModel.selectedArticle?.isReviewArticle == true ? viewModel.sightName : viewModel.articleHeading) + Text(viewModel.selectedArticle?.isReviewArticle == true + ? viewModel.sightName + : viewModel.articleHeading) .font(.title2) .fontWeight(.bold) .foregroundColor(.white) - .frame(maxWidth: .infinity, alignment: viewModel.selectedArticle?.isReviewArticle == true ? .center : .leading) - + .frame(maxWidth: .infinity, + alignment: viewModel.selectedArticle?.isReviewArticle == true ? .center : .leading) + .multilineTextAlignment(viewModel.selectedArticle?.isReviewArticle == true ? .center : .leading) + // Тело статьи ScrollView { if viewModel.selectedArticle?.isReviewArticle == true { @@ -38,16 +39,14 @@ struct SightView: View { .frame(maxWidth: .infinity, alignment: .leading) } } - - // Список статей (кнопки навигации) - ФИНАЛЬНОЕ ИСПРАВЛЕНИЕ ЦЕНТРИРОВАНИЯ + + // Список статей GeometryReader { geometry in ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 6) { - // Spacers для центрирования + HStack(spacing: 10) { Spacer(minLength: 0) - ForEach(viewModel.allArticles) { article in - Text(article.heading) + Text(localizedHeading(article)) .font(.system(size: 12)) .lineLimit(1) .padding(.vertical, 6) @@ -64,28 +63,34 @@ struct SightView: View { viewModel.selectArticle(article) } } - Spacer(minLength: 0) } - // Принудительно задаем ширину HStack как ширину GeometryReader .frame(minWidth: geometry.size.width) } - .scrollIndicators(.hidden) // Скрываем полосу прокрутки + .scrollIndicators(.hidden) } - // Задаем высоту для GeometryReader .frame(height: 34) .frame(maxWidth: .infinity) } .padding(.horizontal, 8) - .padding(.bottom, 10) + .padding(.bottom, 4) } + // MARK: - Initial load .task(id: sightId) { + viewModel.setLanguage(appState.selectedLanguage) await viewModel.loadInitialData(sightId: sightId) } + // MARK: - Reload on language change + .onChange(of: appState.selectedLanguage) { newLang in + viewModel.setLanguage(newLang) + Task { + await viewModel.loadInitialData(sightId: sightId) + } + } .blockStyle(cornerRadius: 25) } - // Медиа-секция + // MARK: - Медиа @ViewBuilder private var mediaSection: some View { Group { @@ -98,29 +103,29 @@ struct SightView: View { .tint(.white) } .frame(maxWidth: .infinity) - .aspectRatio(16/9, contentMode: .fit) + .frame(height: 160) .cornerRadius(24, corners: [.topLeft, .topRight]) .clipped() - case .image(let url): LazyImage(url: url) { state in if let image = state.image { - image - .resizable() - .scaledToFit() + image.resizable().scaledToFit() } else { - ProgressView() + ZStack { + Color.gray.opacity(0.3) + ProgressView() + .progressViewStyle(.circular) + .tint(.white) + } } } .cornerRadius(24, corners: [.topLeft, .topRight]) .clipped() - case .video(let player): VideoPlayer(player: player) .aspectRatio(16/9, contentMode: .fit) .cornerRadius(24, corners: [.topLeft, .topRight]) .clipped() - case .error: Image(systemName: "photo") .resizable() @@ -134,4 +139,18 @@ struct SightView: View { .padding(4) .frame(maxWidth: .infinity) } + + // MARK: - Локализованные заголовки статей + private func localizedHeading(_ article: Article) -> String { + if article.isReviewArticle == true { + switch appState.selectedLanguage { + case "ru": return "Обзор" + case "en": return "Review" + case "zh": return "奥布佐尔" + default: return "Обзор" + } + } else { + return article.heading + } + } } diff --git a/WhiteNights/Widgets/SightViewModel.swift b/WhiteNights/Widgets/SightViewModel.swift index f37b5da..f28ef8e 100644 --- a/WhiteNights/Widgets/SightViewModel.swift +++ b/WhiteNights/Widgets/SightViewModel.swift @@ -1,7 +1,6 @@ import Foundation import AVKit import Combine - @MainActor class SightViewModel: ObservableObject { @Published var sightName: String = "Загрузка..." @@ -10,8 +9,9 @@ class SightViewModel: ObservableObject { @Published var articleHeading: String = "" @Published var articleBody: String = "" @Published var mediaState: MediaState = .loading - + private var sightModel: SightModel? + private var selectedLanguage: String = "ru" // по умолчанию enum MediaState { case loading @@ -20,10 +20,20 @@ class SightViewModel: ObservableObject { case error } + func setLanguage(_ language: String) { + self.selectedLanguage = language + } + func loadInitialData(sightId: Int) async { do { - async let sightModelTask = fetchJSON(from: "https://white-nights.krbl.ru/services/content/sight/\(sightId)", type: SightModel.self) - async let articlesTask = fetchJSON(from: "https://white-nights.krbl.ru/services/content/sight/\(sightId)/article", type: [Article].self) + async let sightModelTask = fetchJSON( + from: "https://white-nights.krbl.ru/services/content/sight/\(sightId)?lang=\(selectedLanguage)", + type: SightModel.self + ) + async let articlesTask = fetchJSON( + from: "https://white-nights.krbl.ru/services/content/sight/\(sightId)/article?lang=\(selectedLanguage)", + type: [Article].self + ) let (fetchedSightModel, fetchedArticles) = try await (sightModelTask, articlesTask) @@ -63,20 +73,23 @@ class SightViewModel: ObservableObject { } if let videoPreviewId = sight.video_preview, - let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(videoPreviewId)/download") { + let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(videoPreviewId)/download?lang=\(selectedLanguage)") { let player = AVPlayer(url: url) player.play() self.mediaState = .video(player) - } else if let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.preview_media)/download") { + } else if let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(sight.preview_media)/download?lang=\(selectedLanguage)") { self.mediaState = .image(url) } else { self.mediaState = .error } } else { do { - let mediaItems = try await fetchJSON(from: "https://white-nights.krbl.ru/services/content/article/\(article.id)/media", type: [ArticleMedia].self) + let mediaItems = try await fetchJSON( + from: "https://white-nights.krbl.ru/services/content/article/\(article.id)/media?lang=\(selectedLanguage)", + type: [ArticleMedia].self + ) if let firstMedia = mediaItems.first, - let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(firstMedia.id)/download") { + let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(firstMedia.id)/download?lang=\(selectedLanguage)") { self.mediaState = .image(url) } else { self.mediaState = .error diff --git a/WhiteNights/Widgets/VisualEffectBlur.swift b/WhiteNights/Widgets/VisualEffectBlur.swift index e81a004..a8a1b62 100644 --- a/WhiteNights/Widgets/VisualEffectBlur.swift +++ b/WhiteNights/Widgets/VisualEffectBlur.swift @@ -7,5 +7,7 @@ struct VisualEffectBlur: UIViewRepresentable { UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) } - func updateUIView(_ uiView: UIVisualEffectView, context: Context) { } + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + uiView.effect = UIBlurEffect(style: blurStyle) + } } diff --git a/WhiteNights/Widgets/WeatherView.swift b/WhiteNights/Widgets/WeatherView.swift index 790317f..bae9a79 100644 --- a/WhiteNights/Widgets/WeatherView.swift +++ b/WhiteNights/Widgets/WeatherView.swift @@ -1,6 +1,6 @@ import SwiftUI -private let WEATHER_STATUS_MAP: [String: String] = [ +private let WEATHER_STATUS_MAP_RU: [String: String] = [ "Rain": "дождливо", "Clouds": "облачно", "Clear": "солнечно", @@ -10,52 +10,81 @@ private let WEATHER_STATUS_MAP: [String: String] = [ "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 status: String + let statusCode: String let precipitation: Int? let windSpeed: Double? - let dayOfWeek: String? + 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: .leading, spacing: 2) { - Image(getWeatherIconName(for: today.status)) + 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.status) + Text(today.localizedStatus(language: appState.selectedLanguage)) .font(.system(size: 12)) .foregroundColor(.white) + .multilineTextAlignment(.center) } - - // ПРАВЫЙ СТОЛБЕЦ: Прогноз с иконками - VStack(alignment: .leading, spacing: 10) { - // Прогноз на 3 дня + + VStack(alignment: .leading, spacing: 6) { ForEach(forecast.prefix(3).indices, id: \.self) { index in let day = forecast[index] - HStack(spacing: 5) { - Image(getWeatherIconName(for: day.status)) + HStack(spacing: 4) { + Image(getWeatherIconName(for: day.localizedStatus(language: appState.selectedLanguage))) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 14, height: 14) - if let dayName = day.dayOfWeek { - Text(dayName) - .font(.system(size: 12)) + if let date = day.originalDate { + Text(getDayOfWeek(from: date)) + .font(.system(size: 10)) .foregroundColor(.white) } @@ -66,9 +95,12 @@ struct WeatherView: View { } } - // Влажность и скорость ветра + Divider() + .background(Color.white.opacity(0.8)) + .padding(.vertical, 1) + if let precipitation = today.precipitation { - HStack(spacing: 5) { + HStack(spacing: 4) { Image("det_humidity") .resizable() .aspectRatio(contentMode: .fit) @@ -78,20 +110,25 @@ struct WeatherView: View { .foregroundColor(.white) } } + if let windSpeed = today.windSpeed { HStack(spacing: 5) { Image("det_wind_speed") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 14, height: 14) - Text("\(Int(windSpeed)) м/с") + Text(appState.selectedLanguage == "ru" ? "\(Int(windSpeed)) м/с" : + appState.selectedLanguage == "zh" ? "\(Int(windSpeed)) 米/秒" : + "\(Int(windSpeed)) m/s") .font(.system(size: 12)) .foregroundColor(.white) } } } } else { - Text("Загрузка погоды...") + Text(appState.selectedLanguage == "ru" ? "Загрузка погоды..." : + appState.selectedLanguage == "zh" ? "正在加载天气..." : + "Loading weather...") .foregroundColor(.white) .padding() } @@ -117,13 +154,16 @@ struct WeatherView: View { .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") else { return } + 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" @@ -136,14 +176,8 @@ struct WeatherView: View { do { request.httpBody = try JSONSerialization.data(withJSONObject: parameters) let (data, _) = try await URLSession.shared.data(for: request) - - // 💡 Отладка: Печатаем полученные данные для проверки - // print("Received Data (String): \(String(data: data, encoding: .utf8) ?? "N/A")") - let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: data) - formatWeatherData(data: weatherResponse) - } catch { print("Ошибка загрузки погоды: \(error)") } @@ -152,103 +186,85 @@ struct WeatherView: View { private func formatWeatherData(data: WeatherResponse) { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) // UTC - dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Добавление POSIX-локали для надежного парсинга + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.locale = Locale(identifier: "en_US_POSIX") var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! // UTC + calendar.timeZone = TimeZone(secondsFromGMT: 0)! - // 1. Фильтруем все записи, которые соответствуют 12:00 UTC let middayForecast = data.forecast.filter { item in - guard let date = dateFormatter.date(from: item.date) else { - // 💡 Отладка: если здесь вы увидите сообщения, значит, формат даты API не совпадает с форматом dateFormatter - // print("Парсинг даты провален для: \(item.date)") - return false - } + guard let date = dateFormatter.date(from: item.date) else { return false } return calendar.component(.hour, from: date) == 12 } - - // 💡 Отладка: Проверяем, сколько записей найдено - // print("Найдено записей на 12:00 UTC: \(middayForecast.count)") - - // Сегодня + if let today = data.currentWeather { - self.todayWeather = FormattedWeather( + todayWeather = FormattedWeather( temperature: Int(today.temperatureCelsius.rounded()), - status: WEATHER_STATUS_MAP[today.description] ?? today.description, + statusCode: today.description, precipitation: today.humidity, windSpeed: today.windSpeed, - dayOfWeek: nil + dayOfWeek: nil, + originalDate: nil ) } - // 🚀 ИСПРАВЛЕНИЕ: Прогноз на 3 дня. Берем первые 3 доступные записи с 12:00. var formattedForecast: [FormattedWeather] = [] - - // Перебираем первые 3 записи с 12:00 UTC + for item in middayForecast.prefix(3) { guard let date = dateFormatter.date(from: item.date) else { continue } - let averageTemp = (item.minTemperatureCelsius + item.maxTemperatureCelsius) / 2 - let dayOfWeekString = getDayOfWeek(from: date) - - // 💡 Отладка: Печатаем найденный прогноз - // print("Прогноз найден: \(dayOfWeekString) - \(Int(averageTemp.rounded()))°") - formattedForecast.append(FormattedWeather( temperature: Int(averageTemp.rounded()), - status: WEATHER_STATUS_MAP[item.description] ?? item.description, + statusCode: item.description, precipitation: item.humidity, windSpeed: item.windSpeed, - dayOfWeek: dayOfWeekString + dayOfWeek: getDayOfWeek(from: date), + originalDate: date )) } - // Если данных меньше 3, заполняем оставшиеся места нулями (N/A) let daysToFill = 3 - formattedForecast.count if daysToFill > 0 { let baseDate = Date() - for i in 1...daysToFill { - guard let nextDayForNA = Calendar.current.date(byAdding: .day, value: formattedForecast.count + i, to: baseDate) else { continue } - - let dayOfWeekString = getDayOfWeek(from: nextDayForNA) - + guard let nextDay = Calendar.current.date(byAdding: .day, value: formattedForecast.count + i, to: baseDate) else { continue } formattedForecast.append(FormattedWeather( temperature: 0, - status: "N/A", + statusCode: "N/A", precipitation: nil, windSpeed: nil, - dayOfWeek: dayOfWeekString + dayOfWeek: getDayOfWeek(from: nextDay), + originalDate: nextDay )) } } - + self.forecast = formattedForecast } private func getDayOfWeek(from date: Date) -> String { let formatter = DateFormatter() - formatter.locale = Locale(identifier: "ru_RU") + 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 "солнечно": + case "солнечно", "晴朗", "sunny": return "cond_sunny" - case "облачно": + case "облачно", "多云", "cloudy": return "cond_cloudy" - case "дождливо", "мелкий дождь": + case "дождливо", "мелкий дождь", "下雨", "毛毛雨", "rainy", "drizzle": return "cond_rainy" - case "снег": + case "снег", "下雪", "snowy": return "cond_snowy" - case "гроза": + case "гроза", "雷雨", "thunderstorm": return "cond_thunder" - case "туман": + case "туман", "雾", "fog": return "det_humidity" default: return "cond_sunny"