import Foundation @MainActor final class SessionStore: ObservableObject { /// Readable from ApiClient (actor) without MainActor hop. nonisolated static let defaultApiBase = "https://master.vonas.nl" @Published var refreshToken: String? @Published var apiBase: String @Published var profileId: String? @Published var profileName: String? @Published var profileAvatarId: String? @Published var profileShowAvatar: Bool = true @Published var profileAvatarUrl: String? @Published var iptvFavoriteIds: Set = [] @Published var lastIptvStreamId: Int? private let defaults = UserDefaults.standard private enum Key { static let token = "vmc.tv.refresh_token" static let api = "vmc.tv.api_base" static let profileId = "vmc.tv.profile_id" static let profileName = "vmc.tv.profile_name" static let profileAvatar = "vmc.tv.profile_avatar" static let profileShowAvatar = "vmc.tv.profile_show_avatar" static let profileAvatarUrl = "vmc.tv.profile_avatar_url" static let iptvFavorites = "vmc.tv.iptv.favorites" static let iptvLastStream = "vmc.tv.iptv.last_stream" } init() { refreshToken = defaults.string(forKey: Key.token) apiBase = defaults.string(forKey: Key.api) ?? Self.defaultApiBase profileId = defaults.string(forKey: Key.profileId) profileName = defaults.string(forKey: Key.profileName) profileAvatarId = defaults.string(forKey: Key.profileAvatar) let showRaw = defaults.string(forKey: Key.profileShowAvatar) profileShowAvatar = showRaw != "0" profileAvatarUrl = defaults.string(forKey: Key.profileAvatarUrl) let favs = defaults.array(forKey: Key.iptvFavorites) as? [Int] ?? [] iptvFavoriteIds = Set(favs) let last = defaults.integer(forKey: Key.iptvLastStream) lastIptvStreamId = last > 0 ? last : nil } var isAuthed: Bool { !(refreshToken ?? "").isEmpty } var hasProfile: Bool { !(profileId ?? "").isEmpty } func setToken(_ token: String?) { refreshToken = token if let token, !token.isEmpty { defaults.set(token, forKey: Key.token) } else { defaults.removeObject(forKey: Key.token) setProfile(id: nil, name: nil, avatarId: nil) } } func setApiBase(_ url: String) { let clean = url.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: CharacterSet(charactersIn: "/")) apiBase = clean.isEmpty ? Self.defaultApiBase : clean defaults.set(apiBase, forKey: Key.api) } func setProfile( id: String?, name: String?, avatarId: String?, showAvatar: Bool? = nil, avatarUrl: String? = nil ) { profileId = id profileName = name profileAvatarId = avatarId if let id, !id.isEmpty { defaults.set(id, forKey: Key.profileId) if let name, !name.isEmpty { defaults.set(name, forKey: Key.profileName) } else { defaults.removeObject(forKey: Key.profileName) } if let avatarId, !avatarId.isEmpty { defaults.set(avatarId, forKey: Key.profileAvatar) } else { defaults.removeObject(forKey: Key.profileAvatar) } if let showAvatar { profileShowAvatar = showAvatar defaults.set(showAvatar ? "1" : "0", forKey: Key.profileShowAvatar) } if let avatarUrl { if avatarUrl.isEmpty { profileAvatarUrl = nil defaults.removeObject(forKey: Key.profileAvatarUrl) } else { profileAvatarUrl = avatarUrl defaults.set(avatarUrl, forKey: Key.profileAvatarUrl) } } } else { defaults.removeObject(forKey: Key.profileId) defaults.removeObject(forKey: Key.profileName) defaults.removeObject(forKey: Key.profileAvatar) defaults.removeObject(forKey: Key.profileShowAvatar) defaults.removeObject(forKey: Key.profileAvatarUrl) profileName = nil profileAvatarId = nil profileShowAvatar = true profileAvatarUrl = nil } } func isIptvFavorite(_ streamId: Int) -> Bool { iptvFavoriteIds.contains(streamId) } @discardableResult func toggleIptvFavorite(_ streamId: Int) -> Bool { if iptvFavoriteIds.contains(streamId) { iptvFavoriteIds.remove(streamId) } else { iptvFavoriteIds.insert(streamId) } defaults.set(Array(iptvFavoriteIds), forKey: Key.iptvFavorites) return iptvFavoriteIds.contains(streamId) } func setLastIptvStream(_ streamId: Int) { lastIptvStreamId = streamId defaults.set(streamId, forKey: Key.iptvLastStream) } }