import SwiftUI import UIKit final class ShareViewController: UIViewController { private let model = ShareViewModel() private var inputTask: Task? private var playbackTask: Task? 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) } } } } }