Files
iOS/Sources/App/Cameras/CameraList/CameraCardViewModel.swift
Bruno Pantaleão Gonçalves 3f2727411c Improve cameras list view (#4210)
<!-- 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>
2026-01-12 17:05:13 +01:00

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
}
}
}