stremio/apps/ios/VMCTV/Data/FavoritesStore.swift
Jos Vooges | STH ac951617cb Add Live Events across API/admin/Android TV, mobile ClearKey/DASH fixes, and tvOS app.
Wire FAAB schedule-events into master-api and admin IPTV settings, add Android TV Events UI, fix mobile ClearKey/stream playback (DASH + bump 1.0.23), and scaffold the Apple TV (VMCTV) client with docs/scripts.
2026-09-18 23:34:42 +02:00

143 lines
4.2 KiB
Swift

import Foundation
import SwiftUI
@MainActor
final class FavoritesStore: ObservableObject {
@Published private(set) var items: [MediaItem] = []
private var profileId: String?
private let defaults = UserDefaults.standard
private func cacheKey(for profileId: String) -> String {
"vmc.tv.favorites.cache.\(profileId)"
}
func bind(profileId: String?) {
self.profileId = profileId
guard let profileId, !profileId.isEmpty else {
items = []
return
}
items = loadCache(profileId: profileId)
}
func contains(type: String, id: String) -> Bool {
let want = normalizedType(type)
return items.contains {
$0.id == id && normalizedType($0.type) == want
}
}
private func normalizedType(_ type: String) -> String {
let t = type.lowercased()
return (t == "series" || t == "show") ? "series" : "movie"
}
func replaceAll(_ next: [MediaItem]) {
items = next
persistCache()
}
@discardableResult
func toggle(
type: String,
id: String,
title: String,
year: Int?,
posterUrl: String?,
backdropUrl: String?,
logoUrl: String?,
api: ApiClient
) async -> Bool {
let normalized = normalizedType(type)
let wasOn = contains(type: normalized, id: id)
if wasOn {
items.removeAll { $0.id == id && normalizedType($0.type) == normalized }
} else {
let item = MediaItem(
type: normalized.uppercased() == "SERIES" ? "SERIES" : "MOVIE",
id: id,
title: title,
subtitle: nil,
year: year,
posterUrl: posterUrl,
backdropUrl: backdropUrl,
logoUrl: logoUrl,
mediaFileId: nil,
seriesId: nil,
positionMs: nil,
durationMs: nil,
progressId: nil,
badge: nil
)
items.insert(item, at: 0)
}
persistCache()
do {
let result = try await api.toggleFavorite(type: normalized, id: id)
if let remote = result.items {
replaceAll(remote)
} else {
await refresh(using: api)
}
return result.favorited
} catch {
if wasOn {
let item = MediaItem(
type: normalized.uppercased() == "SERIES" ? "SERIES" : "MOVIE",
id: id,
title: title,
subtitle: nil,
year: year,
posterUrl: posterUrl,
backdropUrl: backdropUrl,
logoUrl: logoUrl,
mediaFileId: nil,
seriesId: nil,
positionMs: nil,
durationMs: nil,
progressId: nil,
badge: nil
)
items.insert(item, at: 0)
} else {
items.removeAll { $0.id == id && normalizedType($0.type) == normalized }
}
persistCache()
return wasOn
}
}
func refresh(using api: ApiClient) async {
guard let profileId, !profileId.isEmpty else {
items = []
return
}
do {
let remote = try await api.listFavorites()
replaceAll(remote)
} catch {
if items.isEmpty {
items = loadCache(profileId: profileId)
}
}
}
var mediaItems: [MediaItem] { items }
private func loadCache(profileId: String) -> [MediaItem] {
guard let data = defaults.data(forKey: cacheKey(for: profileId)),
let decoded = try? JSONDecoder().decode([MediaItem].self, from: data)
else { return [] }
return decoded
}
private func persistCache() {
guard let profileId, !profileId.isEmpty else { return }
if let data = try? JSONEncoder().encode(items) {
defaults.set(data, forKey: cacheKey(for: profileId))
}
}
}