import Foundation import AVKit import Combine import SwiftUI // MARK: - ViewModel @MainActor class SightViewModel: ObservableObject { private let fileDownloader = FileDownloader() private var mediaCache: [String: URL] = [:] @Published var sightName: String = "Загрузка..." @Published var allArticles: [Article] = [] @Published var selectedArticle: Article? @Published var articleHeading: String = "" @Published var articleBody: String = "" @Published var mediaState: MediaState = .loading @Published var downloadProgress: Double? = nil // прогресс загрузки private var sightModel: SightModel? private var selectedLanguage: String = "ru" // по умолчанию enum MediaState { case loading case image(URL) case video(AVPlayer) case error } func setLanguage(_ language: String) { self.selectedLanguage = language } func loadInitialData(sightId: Int) async { // Вот это исправление. Сбрасываем выбранную статью перед загрузкой новых данных. self.selectedArticle = nil do { 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) self.sightModel = fetchedSightModel self.sightName = fetchedSightModel.name let reviewArticle = Article(id: -1, body: "", heading: "Обзор", isReviewArticle: true) self.allArticles = [reviewArticle] + fetchedArticles selectArticle(reviewArticle) } catch { print("Ошибка начальной загрузки данных: \(error)") self.mediaState = .error } } func selectArticle(_ article: Article) { guard selectedArticle != article else { return } self.selectedArticle = article self.articleHeading = article.heading self.articleBody = article.body Task { await updateMedia(for: article) } } // MARK: - Загрузка медиа @MainActor private func updateMedia(for article: Article) async { self.mediaState = .loading self.downloadProgress = nil do { if article.isReviewArticle ?? false { guard let sight = sightModel else { return } if let videoPreviewId = sight.video_preview { if let cachedURL = mediaCache[videoPreviewId] { let player = AVPlayer(url: cachedURL) self.mediaState = .video(player) player.play() await player.seek(to: .zero) return } let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(videoPreviewId)/download?lang=\(selectedLanguage)")! let (data, _) = try await URLSession.shared.data(from: url) let localFile = try saveMediaToFile(data: data, fileExtension: "mp4") mediaCache[videoPreviewId] = localFile let player = AVPlayer(url: localFile) let asset = player.currentItem?.asset if let asset = asset { try await asset.load(.isPlayable) if asset.isPlayable { self.mediaState = .video(player) player.play() await player.seek(to: .zero) } else { self.mediaState = .error } } else { self.mediaState = .error } } else if let previewMediaId = sight.preview_media { if let cachedURL = mediaCache[previewMediaId] { self.mediaState = .image(cachedURL) return } let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(previewMediaId)/download?lang=\(selectedLanguage)")! let (data, _) = try await URLSession.shared.data(from: url) if let image = UIImage(data: data) { let localFile = try saveMediaToFile(data: data, fileExtension: "jpeg") mediaCache[previewMediaId] = localFile self.mediaState = .image(localFile) } else { self.mediaState = .error } } else { self.mediaState = .error } } else { // Загрузка медиа для статьи 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 mediaId = "\(firstMedia.id)" if let cachedURL = mediaCache[mediaId] { self.mediaState = .image(cachedURL) return } if let url = URL(string: "https://white-nights.krbl.ru/services/content/media/\(firstMedia.id)/download?lang=\(selectedLanguage)") { let (data, _) = try await URLSession.shared.data(from: url) if let image = UIImage(data: data) { let localFile = try saveMediaToFile(data: data, fileExtension: "jpeg") mediaCache[mediaId] = localFile self.mediaState = .image(localFile) } else { self.mediaState = .error } } } } } catch { print("Ошибка загрузки файла: \(error)") self.mediaState = .error self.downloadProgress = nil } } private func saveMediaToFile(data: Data, fileExtension: String) throws -> URL { let destinationURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension(fileExtension) try data.write(to: destinationURL) return destinationURL } // MARK: - Загрузка файла с прогрессом private func downloadFile(from url: URL) async throws -> URL { let (tempLocalUrl, response) = try await URLSession.shared.download(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw URLError(.badServerResponse) } // Пытаемся угадать расширение let suggestedName = response.suggestedFilename ?? UUID().uuidString let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(suggestedName) // Удаляем файл, если уже есть if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.moveItem(at: tempLocalUrl, to: destinationURL) return destinationURL } // MARK: - JSON загрузчик private func fetchJSON(from urlString: String, type: T.Type) async throws -> T { guard let url = URL(string: urlString) else { throw URLError(.badURL) } let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(T.self, from: data) } }