Files
QueueCube/ios/QueueCubeShare/ShareViewController.swift
buzzert cedd0c6aa2 ios: simplify share extension to saved-player tap-to-play
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.
2026-10-21 01:06:45 -07:00

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)
}
}
}
}
}