fix video

This commit is contained in:
15lu.akari
2025-08-27 00:19:05 +03:00
parent a87a3d12ab
commit 30d97f420e
5 changed files with 186 additions and 52 deletions

View File

@ -15,13 +15,7 @@ struct ContentView: View {
WeatherView()
}
if let sightId = appState.sightId {
SightView(sightId: sightId)
} else {
Text("Выберите маршрут, чтобы увидеть достопримечательность")
.foregroundColor(.gray)
.padding()
}
SightView()
}
.padding()
.blur(radius: isLoading ? 5 : 0) // слегка размываем, пока загрузка

View File

@ -0,0 +1,36 @@
import Foundation
class FileDownloader: NSObject, URLSessionDownloadDelegate {
private var completion: ((URL?, Error?) -> Void)?
func downloadFile(from url: URL, completion: @escaping (URL?, Error?) -> Void) {
self.completion = completion
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
}
// MARK: - URLSessionDownloadDelegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
do {
try FileManager.default.moveItem(at: location, to: destinationURL)
DispatchQueue.main.async {
self.completion?(destinationURL, nil)
}
} catch {
DispatchQueue.main.async {
self.completion?(nil, error)
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
DispatchQueue.main.async {
self.completion?(nil, error)
}
}
}
}

View File

@ -3,7 +3,6 @@ import AVKit
import NukeUI
struct SightView: View {
let sightId: Int
@StateObject private var viewModel = SightViewModel()
@EnvironmentObject private var appState: AppState
@ -75,16 +74,18 @@ struct SightView: View {
.padding(.horizontal, 8)
.padding(.bottom, 4)
}
// MARK: - Initial load
.task(id: sightId) {
// MARK: - Initial load and reload on sight change
.task(id: appState.sightId) {
guard let currentSightId = appState.sightId else { return }
viewModel.setLanguage(appState.selectedLanguage)
await viewModel.loadInitialData(sightId: sightId)
await viewModel.loadInitialData(sightId: currentSightId)
}
// MARK: - Reload on language change
.onChange(of: appState.selectedLanguage) { newLang in
guard let currentSightId = appState.sightId else { return }
viewModel.setLanguage(newLang)
Task {
await viewModel.loadInitialData(sightId: sightId)
await viewModel.loadInitialData(sightId: currentSightId)
}
}
.blockStyle(cornerRadius: 25)
@ -98,34 +99,49 @@ struct SightView: View {
case .loading:
ZStack {
Color.gray.opacity(0.3)
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
}
.frame(maxWidth: .infinity)
.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()
} else {
ZStack {
Color.gray.opacity(0.3)
ProgressView()
.progressViewStyle(.circular)
if let progress = viewModel.downloadProgress {
VStack {
ProgressView(value: progress)
.progressViewStyle(.linear)
.tint(.white)
Text("\(Int(progress * 100))%")
.foregroundColor(.white)
.font(.caption)
}
.padding()
} else {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
}
}
.frame(maxWidth: .infinity, minHeight: 200)
.cornerRadius(24, corners: [.topLeft, .topRight])
.clipped()
case .image(let url):
if let uiImage = UIImage(contentsOfFile: url.path) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.cornerRadius(24, corners: [.topLeft, .topRight])
.clipped()
} else {
Image(systemName: "photo")
.resizable()
.scaledToFit()
.foregroundColor(.gray)
.frame(maxWidth: .infinity, minHeight: 200)
.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()

View File

@ -1,14 +1,20 @@
import Foundation
import AVKit
import Combine
import SwiftUI
// MARK: - ViewModel
@MainActor
class SightViewModel: ObservableObject {
private let fileDownloader = FileDownloader()
@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" // по умолчанию
@ -25,6 +31,9 @@ class SightViewModel: ObservableObject {
}
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)",
@ -63,44 +72,119 @@ class SightViewModel: ObservableObject {
}
}
// MARK: - Загрузка медиа
// MARK: - Загрузка медиа
@MainActor
private func updateMedia(for article: Article) async {
self.mediaState = .loading
if article.isReviewArticle == true {
guard let sight = sightModel else {
self.mediaState = .error
return
}
self.downloadProgress = nil
do {
if article.isReviewArticle ?? false {
guard let sight = sightModel else { return }
if let videoPreviewId = sight.video_preview {
// Пытаемся загрузить видео-превью
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)
// Пробуем создать AVPlayer из данных
let localFile = try saveMediaToFile(data: data, fileExtension: "mp4")
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)
self.downloadProgress = nil
player.play()
await player.seek(to: .zero)
} else {
self.mediaState = .error
}
} else {
self.mediaState = .error
}
} else if let previewMediaId = sight.preview_media {
// Если нет видео-превью, пытаемся загрузить изображение
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)
// Пробуем создать UIImage из данных
if let image = UIImage(data: data) {
let localFile = try saveMediaToFile(data: data, fileExtension: "jpeg")
self.mediaState = .image(localFile)
self.downloadProgress = nil
} else {
self.mediaState = .error
}
} else {
self.mediaState = .error
}
if let videoPreviewId = sight.video_preview,
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?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?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?lang=\(selectedLanguage)") {
self.mediaState = .image(url)
} else {
self.mediaState = .error
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
let localFile = try saveMediaToFile(data: data, fileExtension: "jpeg")
self.mediaState = .image(localFile)
self.downloadProgress = nil
} else {
self.mediaState = .error
}
}
} catch {
print("Ошибка загрузки медиа для статьи '\(article.heading)': \(error)")
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<T: Decodable>(from urlString: String, type: T.Type) async throws -> T {
guard let url = URL(string: urlString) else {
throw URLError(.badURL)