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.
This commit is contained in:
2026-10-21 01:06:45 -07:00
parent 174a8f8255
commit cedd0c6aa2
7 changed files with 36 additions and 113 deletions
+1 -5
View File
@@ -4,12 +4,8 @@
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSBonjourServices</key>
<array>
<string>_queuecube._tcp.</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>QueueCube discovers nearby players so you can choose where to play this video.</string>
<string>QueueCube connects to your saved players to queue and play this video.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
+22 -92
View File
@@ -9,8 +9,8 @@ final class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
preferredContentSize = CGSize(width: 480, height: 600)
let host = UIHostingController(rootView: ShareView(model: model, send: { [weak self] in
self?.send()
let host = UIHostingController(rootView: ShareView(model: model, send: { [weak self] server in
self?.send(to: server)
}, close: { [weak self] in
self?.close()
}))
@@ -24,7 +24,6 @@ final class ShareViewController: UIViewController {
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
host.didMove(toParent: self)
model.discovery.start()
inputTask = Task { [weak self] in
guard let self else { return }
do {
@@ -37,12 +36,10 @@ final class ShareViewController: UIViewController {
}
}
private func send() {
guard !model.sending, !model.finished, let url = model.url,
let server = model.selectedServer else { return }
private func send(to server: Server) {
guard !model.sending, !model.finished, let url = model.url else { return }
model.sending = true
model.message = nil
model.discovery.stop()
playbackTask = Task { [weak self] in
guard let self else { return }
do {
@@ -55,7 +52,6 @@ final class ShareViewController: UIViewController {
guard !Task.isCancelled else { return }
model.message = error.localizedDescription
model.finished = !error.canRetry
if error.canRetry { model.discovery.start() }
} catch {
guard !Task.isCancelled else { return }
model.message = "The request could not be confirmed. Check the player in QueueCube before sharing again."
@@ -68,7 +64,6 @@ final class ShareViewController: UIViewController {
private func close() {
inputTask?.cancel()
playbackTask?.cancel()
model.stop()
if model.finished {
extensionContext?.completeRequest(returningItems: nil)
} else {
@@ -80,7 +75,6 @@ final class ShareViewController: UIViewController {
super.viewDidDisappear(animated)
inputTask?.cancel()
playbackTask?.cancel()
model.stop()
}
deinit {
@@ -91,53 +85,18 @@ final class ShareViewController: UIViewController {
@MainActor @Observable
private final class ShareViewModel {
let discovery = ServerDiscovery()
let configuredServers: [Server]
var selectedServer: Server?
let configuredServers = Settings.fromSharedDefaults().configuredServers
var url: URL?
var loading = true
var sending = false
var finished = false
var succeeded = false
var resolving = false
var message: String?
private var resolutionTask: Task<Void, Never>?
init() {
let settings = Settings.fromSharedDefaults()
configuredServers = settings.configuredServers
selectedServer = settings.selectedServer.flatMap { selected in
settings.configuredServers.first { $0.id == selected.id }
} ?? settings.configuredServers.first
}
func select(_ endpoint: DiscoveredEndpoint) {
resolutionTask?.cancel()
resolving = true
message = nil
resolutionTask = Task {
do {
let server = try await endpoint.resolve()
try Task.checkCancellation()
selectedServer = server
} catch {
if !Task.isCancelled {
message = "Could not connect to that player. Check its connection and try again."
}
}
if !Task.isCancelled { resolving = false }
}
}
func stop() {
resolutionTask?.cancel()
discovery.stop()
}
}
private struct ShareView: View {
@Bindable var model: ShareViewModel
let send: () -> Void
let send: (Server) -> Void
let close: () -> Void
var body: some View {
@@ -149,40 +108,35 @@ private struct ShareView: View {
} else if let url = model.url {
Text(url.absoluteString)
.font(.callout)
.lineLimit(3)
.lineLimit(1)
.truncationMode(.middle)
}
}
if !model.finished {
Section("Choose player") {
if model.configuredServers.isEmpty {
Text("Open QueueCube once to share your saved players, or choose a nearby player below. Allow local network access in QueueCube to reach nearby players.")
Text("Open QueueCube to set up your players.")
.foregroundStyle(.secondary)
}
ForEach(model.configuredServers) { server in
Button {
model.selectedServer = server
send(server)
} label: {
playerLabel(server.displayName, selected: model.selectedServer?.id == server.id)
HStack {
Label(server.displayName, systemImage: "hifispeaker.fill")
Spacer()
}
.foregroundStyle(Color.primary)
.contentShape(Rectangle())
}
.disabled(model.sending || model.resolving)
}
if let selected = model.selectedServer,
!model.configuredServers.contains(where: { $0.id == selected.id }) {
playerLabel(selected.displayName, selected: true)
.tint(.primary)
.disabled(model.url == nil || model.sending)
}
}
if !model.sending {
Section("Nearby players") {
if model.discovery.servers.isEmpty {
Text(model.discovery.errorMessage ?? "Looking for QueueCube players on your local network…")
.foregroundStyle(.secondary)
}
ForEach(model.discovery.servers) { endpoint in
Button(endpoint.displayName) { model.select(endpoint) }
.disabled(model.resolving)
}
if model.resolving { ProgressView("Connecting…") }
}
}
if model.sending {
Section {
ProgressView("Queuing and starting playback…")
}
}
if let message = model.message {
@@ -191,22 +145,6 @@ private struct ShareView: View {
.foregroundStyle(model.succeeded ? Color.green : Color.primary)
}
}
if !model.finished {
Section {
Button(action: send) {
HStack {
Spacer()
if model.sending { ProgressView() }
Text(model.sending ? "Queuing and starting playback…" : "Queue & Play")
Spacer()
}
}
.disabled(model.url == nil || model.selectedServer == nil || model.sending || model.resolving)
Text("Adds this link to the queue and plays it now. Existing queued items are kept.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("QueueCube")
.navigationBarTitleDisplayMode(.inline)
@@ -218,12 +156,4 @@ private struct ShareView: View {
}
}
}
private func playerLabel(_ name: String, selected: Bool) -> some View {
HStack {
Label(name, systemImage: "hifispeaker.fill")
Spacer()
if selected { Image(systemName: "checkmark").accessibilityLabel("Selected") }
}
}
}