jellyflood/Shared/ViewModels/ConnectToXtreamViewModel.swift

184 lines
5.4 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Defaults
import Foundation
final class ConnectToXtreamViewModel: ViewModel, Eventful, Stateful {
// MARK: Event
enum Event {
case connected(XtreamServer)
case error(XtreamAPIError)
}
// MARK: Action
enum Action: Equatable {
case cancel
case connect(name: String, url: String, username: String, password: String)
case testConnection(name: String, url: String, username: String, password: String)
}
// MARK: State
enum State: Hashable {
case connecting
case initial
case testing
}
@Published
var state: State = .initial
var events: AnyPublisher<Event, Never> {
eventSubject
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
private var connectTask: AnyCancellable?
private var eventSubject: PassthroughSubject<Event, Never> = .init()
func respond(to action: Action) -> State {
switch action {
case .cancel:
connectTask?.cancel()
return .initial
case let .connect(name, urlString, username, password):
connectTask?.cancel()
connectTask = Task {
do {
let server = try await connectToXtream(
name: name,
url: urlString,
username: username,
password: password
)
await MainActor.run {
self.eventSubject.send(.connected(server))
self.state = .initial
}
} catch is CancellationError {
// cancel doesn't matter
} catch let error as XtreamAPIError {
await MainActor.run {
self.eventSubject.send(.error(error))
self.state = .initial
}
} catch {
await MainActor.run {
self.eventSubject.send(.error(.invalidResponse))
self.state = .initial
}
}
}
.asAnyCancellable()
return .connecting
case let .testConnection(name, urlString, username, password):
connectTask?.cancel()
connectTask = Task {
do {
_ = try await connectToXtream(
name: name,
url: urlString,
username: username,
password: password
)
await MainActor.run {
self.state = .initial
}
} catch is CancellationError {
// cancel doesn't matter
} catch let error as XtreamAPIError {
await MainActor.run {
self.eventSubject.send(.error(error))
self.state = .initial
}
} catch {
await MainActor.run {
self.eventSubject.send(.error(.invalidResponse))
self.state = .initial
}
}
}
.asAnyCancellable()
return .testing
}
}
private func connectToXtream(
name: String,
url urlString: String,
username: String,
password: String
) async throws -> XtreamServer {
let formattedURL = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmingCharacters(in: .objectReplacement)
.trimmingCharacters(in: ["/"])
.prepending("http://", if: !urlString.contains("://"))
guard let url = URL(string: formattedURL) else {
throw XtreamAPIError.invalidURL
}
let server = XtreamServer(
name: name.isEmpty ? "Xtream Server" : name,
url: url,
username: username,
password: password
)
// Test connection
let client = XtreamAPIClient(server: server)
_ = try await client.testConnection()
return server
}
func saveServer(_ server: XtreamServer) {
var servers = Defaults[.xtreamServers]
// Check if server with same ID exists and update, otherwise append
if let index = servers.firstIndex(where: { $0.id == server.id }) {
servers[index] = server
} else {
servers.append(server)
}
Defaults[.xtreamServers] = servers
// Set as current server if it's the first one
if Defaults[.currentXtreamServerID] == nil {
Defaults[.currentXtreamServerID] = server.id
}
}
func deleteServer(_ server: XtreamServer) {
var servers = Defaults[.xtreamServers]
servers.removeAll { $0.id == server.id }
Defaults[.xtreamServers] = servers
// Clear current server if deleted
if Defaults[.currentXtreamServerID] == server.id {
Defaults[.currentXtreamServerID] = servers.first?.id
}
}
}