Show only configured players, enqueue and play immediately on row tap, and keep the URL on one truncated line. Remove extension discovery, selection state, and the bottom action while retaining progress and existing duplicate/partial-failure guards. Preserve main-app discovery. Update share-extension guidance to match the simplified flow.
160 lines
5.9 KiB
Swift
160 lines
5.9 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|