mirror of
https://github.com/home-assistant/iOS.git
synced 2026-04-12 15:26:45 -05:00
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> - Add fallback support to HLS and MJPEG - Improve UI - Move WebRTC known issues disclaimer to a sheet view ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> <img width="3160" height="1068" alt="CleanShot 2026-01-12 at 15 43 49@2x" src="https://github.com/user-attachments/assets/77d445d7-88f0-40fb-8b0d-b1eab3c56e3d" /> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
118 lines
3.5 KiB
Swift
118 lines
3.5 KiB
Swift
import Foundation
|
|
import Shared
|
|
import SwiftUI
|
|
|
|
final class CameraCardViewModel: ObservableObject {
|
|
@Published var image: Image?
|
|
@Published var errorMessage: String?
|
|
@Published var isLoading = false
|
|
@Published var snapshotDate: Date?
|
|
|
|
private let serverId: String
|
|
private let entityId: String
|
|
private let imageExpirationDuration: Measurement<UnitDuration> = .init(value: 10, unit: .seconds)
|
|
private var refreshTimer: Timer?
|
|
private var isViewVisible = false
|
|
|
|
init(serverId: String, entityId: String) {
|
|
self.serverId = serverId
|
|
self.entityId = entityId
|
|
}
|
|
|
|
deinit {
|
|
stopRefreshTimer()
|
|
}
|
|
|
|
func viewDidAppear() {
|
|
isViewVisible = true
|
|
loadImageURL()
|
|
startRefreshTimer()
|
|
}
|
|
|
|
func viewDidDisappear() {
|
|
isViewVisible = false
|
|
stopRefreshTimer()
|
|
}
|
|
|
|
func loadImageURL() {
|
|
// Check if image is still valid
|
|
if let snapshotDate {
|
|
let elapsedTime = Current.date().timeIntervalSince(snapshotDate)
|
|
let expirationInterval = imageExpirationDuration.converted(to: .seconds).value
|
|
|
|
if elapsedTime < expirationInterval {
|
|
// Image is still fresh, don't reload
|
|
return
|
|
}
|
|
}
|
|
|
|
setLoading(true)
|
|
guard let server = Current.servers.all.first(where: { $0.identifier.rawValue == serverId }) else {
|
|
setError(L10n.Camera.serverNotFound)
|
|
return
|
|
}
|
|
Current.api(for: server)?.getCameraSnapshot(cameraEntityID: entityId).pipe { [weak self] result in
|
|
switch result {
|
|
case let .fulfilled(image):
|
|
self?.setImage(image)
|
|
case let .rejected(error):
|
|
let errorMessage = L10n.Camera.snapshotFailed
|
|
Current.Log
|
|
.error("\(errorMessage) for \(String(describing: self?.entityId)): \(error.localizedDescription)")
|
|
self?.setError(errorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
func forceReload() {
|
|
// Clear the snapshot date to force a reload
|
|
snapshotDate = nil
|
|
loadImageURL()
|
|
}
|
|
|
|
private func startRefreshTimer() {
|
|
stopRefreshTimer()
|
|
refreshTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
guard let self, isViewVisible else { return }
|
|
|
|
if let snapshotDate {
|
|
let elapsedTime = Current.date().timeIntervalSince(snapshotDate)
|
|
let expirationInterval = imageExpirationDuration.converted(to: .seconds).value
|
|
|
|
if elapsedTime >= expirationInterval {
|
|
loadImageURL()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stopRefreshTimer() {
|
|
refreshTimer?.invalidate()
|
|
refreshTimer = nil
|
|
}
|
|
|
|
private func setImage(_ uiImage: UIImage) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.image = Image(uiImage: uiImage)
|
|
self?.snapshotDate = Current.date()
|
|
self?.errorMessage = nil
|
|
self?.isLoading = false
|
|
}
|
|
}
|
|
|
|
private func setError(_ message: String) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.errorMessage = message
|
|
self?.image = nil
|
|
self?.snapshotDate = nil
|
|
self?.isLoading = false
|
|
}
|
|
}
|
|
|
|
private func setLoading(_ loading: Bool) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.isLoading = loading
|
|
}
|
|
}
|
|
}
|