Files
QueueCube/ios/QueueCubeShare/ShareViewController.swift
T

160 lines
5.9 KiB
Swift
Raw Normal View History

import SwiftUI
import UIKit
final class ShareViewController: UIViewController {
private let model = ShareViewModel()
private var inputTask: Task<Void, Never>?
private var playbackTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
preferredContentSize = CGSize(width: 480, height: 600)
let host = UIHostingController(rootView: ShareView(model: model, send: { [weak self] server in
self?.send(to: server)
}, close: { [weak self] in
self?.close()
}))
addChild(host)
host.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(host.view)
NSLayoutConstraint.activate([
host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
host.view.topAnchor.constraint(equalTo: view.topAnchor),
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
host.didMove(toParent: self)
inputTask = Task { [weak self] in
guard let self else { return }
do {
model.url = try await SharedInput.load(extensionContext?.inputItems ?? [])
} catch {
guard !Task.isCancelled else { return }
model.message = error.localizedDescription
}
model.loading = false
}
}
private func send(to server: Server) {
guard !model.sending, !model.finished, let url = model.url else { return }
model.sending = true
model.message = nil
playbackTask = Task { [weak self] in
guard let self else { return }
do {
try await SharePlayback.enqueueAndPlay(url, using: API(baseURL: server.baseURL, requestTimeout: 8))
try Task.checkCancellation()
model.succeeded = true
model.finished = true
model.message = "Queued and playing on \(server.displayName)."
} catch let error as SharePlayback.Failure {
guard !Task.isCancelled else { return }
model.message = error.localizedDescription
model.finished = !error.canRetry
} catch {
guard !Task.isCancelled else { return }
model.message = "The request could not be confirmed. Check the player in QueueCube before sharing again."
model.finished = true
}
model.sending = false
}
}
private func close() {
inputTask?.cancel()
playbackTask?.cancel()
if model.finished {
extensionContext?.completeRequest(returningItems: nil)
} else {
extensionContext?.cancelRequest(withError: NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
inputTask?.cancel()
playbackTask?.cancel()
}
deinit {
inputTask?.cancel()
playbackTask?.cancel()
}
}
@MainActor @Observable
private final class ShareViewModel {
let configuredServers = Settings.fromSharedDefaults().configuredServers
var url: URL?
var loading = true
var sending = false
var finished = false
var succeeded = false
var message: String?
}
private struct ShareView: View {
@Bindable var model: ShareViewModel
let send: (Server) -> Void
let close: () -> Void
var body: some View {
NavigationStack {
Form {
Section("Video link") {
if model.loading {
ProgressView("Reading link…")
} else if let url = model.url {
Text(url.absoluteString)
.font(.callout)
.lineLimit(1)
.truncationMode(.middle)
}
}
if !model.finished {
Section("Choose player") {
if model.configuredServers.isEmpty {
Text("Open QueueCube to set up your players.")
.foregroundStyle(.secondary)
}
ForEach(model.configuredServers) { server in
Button {
send(server)
} label: {
HStack {
Label(server.displayName, systemImage: "hifispeaker.fill")
Spacer()
}
.foregroundStyle(Color.primary)
.contentShape(Rectangle())
}
.tint(.primary)
.disabled(model.url == nil || model.sending)
}
}
}
if model.sending {
Section {
ProgressView("Queuing and starting playback…")
}
}
if let message = model.message {
Section {
Label(message, systemImage: model.succeeded ? "checkmark.circle.fill" : "exclamationmark.circle")
.foregroundStyle(model.succeeded ? Color.green : Color.primary)
}
}
}
.navigationTitle("QueueCube")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(model.finished ? "Done" : "Cancel", action: close)
.disabled(model.sending)
}
}
}
}
}