fix video
This commit is contained in:
@ -41,6 +41,7 @@
|
||||
628A47C72E5A99A20099CAA0 /* VisualEffectBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47C62E5A99A20099CAA0 /* VisualEffectBlur.swift */; };
|
||||
628A47C92E5A9A970099CAA0 /* StopModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47C82E5A9A970099CAA0 /* StopModels.swift */; };
|
||||
628A47CB2E5A9BD60099CAA0 /* RouteSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628A47CA2E5A9BD60099CAA0 /* RouteSelectionView.swift */; };
|
||||
62F969FD2E5E5691009674E8 /* URLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F969FC2E5E5691009674E8 /* URLSessionDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -91,6 +92,7 @@
|
||||
628A47C62E5A99A20099CAA0 /* VisualEffectBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectBlur.swift; sourceTree = "<group>"; };
|
||||
628A47C82E5A9A970099CAA0 /* StopModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopModels.swift; sourceTree = "<group>"; };
|
||||
628A47CA2E5A9BD60099CAA0 /* RouteSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteSelectionView.swift; sourceTree = "<group>"; };
|
||||
62F969FC2E5E5691009674E8 /* URLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDelegate.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -208,6 +210,7 @@
|
||||
628A47972E5A92FC0099CAA0 /* View+Extensions.swift */,
|
||||
628A47A42E5A957A0099CAA0 /* MarqueeText.swift */,
|
||||
628A47A82E5A961D0099CAA0 /* View+CornerRadius.swift */,
|
||||
62F969FC2E5E5691009674E8 /* URLSessionDelegate.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -377,6 +380,7 @@
|
||||
628A479D2E5A94D30099CAA0 /* SightModel.swift in Sources */,
|
||||
628A478D2E5A922A0099CAA0 /* WeatherView.swift in Sources */,
|
||||
628A47C72E5A99A20099CAA0 /* VisualEffectBlur.swift in Sources */,
|
||||
62F969FD2E5E5691009674E8 /* URLSessionDelegate.swift in Sources */,
|
||||
628A479F2E5A94EB0099CAA0 /* Article.swift in Sources */,
|
||||
628A47922E5A926A0099CAA0 /* SightView.swift in Sources */,
|
||||
628A47A72E5A95F60099CAA0 /* Station.swift in Sources */,
|
||||
|
@ -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) // слегка размываем, пока загрузка
|
||||
|
36
WhiteNights/Utils/URLSessionDelegate.swift
Normal file
36
WhiteNights/Utils/URLSessionDelegate.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user