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:
@@ -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>
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user