ios: add share extension to queue and play video links
Embed an iOS and Catalyst share extension using existing discovery and player APIs. Share saved players through an App Group snapshot, preserve the queue, and verify advancing playback before reporting success. Cover URL provider transport, settings migration, discovery addresses, duplicate handling, cancellation, and partial enqueue/play outcomes with focused tests. Document authentication and signing limits.
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
// Created by James Magahern on 6/10/25.
|
||||
//
|
||||
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
struct AddServerView: View
|
||||
@@ -60,14 +59,18 @@ struct AddServerView: View
|
||||
|
||||
// Discovered
|
||||
Section(.discovered) {
|
||||
if model.discoveredServers.isEmpty {
|
||||
if let message = model.resolutionError ?? model.discovery.errorMessage {
|
||||
Text(message)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
if model.discovery.servers.isEmpty {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text(.findingServers)
|
||||
}
|
||||
} else {
|
||||
List(model.discoveredServers) { (server: DiscoveredEndpoint) in
|
||||
List(model.discovery.servers) { (server: DiscoveredEndpoint) in
|
||||
Button {
|
||||
resolveEndpoint(server)
|
||||
} label: {
|
||||
@@ -90,23 +93,30 @@ struct AddServerView: View
|
||||
}
|
||||
|
||||
.task {
|
||||
model.startDiscovery()
|
||||
model.discovery.start()
|
||||
}
|
||||
.onDisappear { model.discovery.stop() }
|
||||
}
|
||||
|
||||
private func resolveEndpoint(_ endpoint: DiscoveredEndpoint) {
|
||||
Task {
|
||||
model.resolvingServers.insert(endpoint)
|
||||
|
||||
let server = try await endpoint.resolve()
|
||||
onAddServer(server)
|
||||
|
||||
model.resolvingServers.remove(endpoint)
|
||||
model.resolutionError = nil
|
||||
defer { model.resolvingServers.remove(endpoint) }
|
||||
do {
|
||||
let server = try await endpoint.resolve()
|
||||
onAddServer(server)
|
||||
} catch is CancellationError {
|
||||
// The view or its caller no longer needs this result.
|
||||
} catch {
|
||||
model.resolutionError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
class ViewModel
|
||||
{
|
||||
@@ -114,10 +124,9 @@ struct AddServerView: View
|
||||
var validationURL: String = ""
|
||||
var validationState: ValidationState = .empty
|
||||
|
||||
var discoveredServers: [DiscoveredEndpoint] = []
|
||||
let discovery = ServerDiscovery()
|
||||
var resolvingServers = Set<DiscoveredEndpoint>()
|
||||
|
||||
private let browser = NWBrowser(for: .bonjour(type: "_queuecube._tcp.", domain: "local."), using: .tcp)
|
||||
var resolutionError: String?
|
||||
|
||||
private var validationTimer: Timer? = nil
|
||||
|
||||
@@ -125,21 +134,6 @@ struct AddServerView: View
|
||||
observeForValidation()
|
||||
}
|
||||
|
||||
public func startDiscovery() {
|
||||
browser.browseResultsChangedHandler = { [weak self] results, changes in
|
||||
guard let self else { return }
|
||||
self.discoveredServers = results.map { DiscoveredEndpoint(result: $0) }
|
||||
}
|
||||
|
||||
browser.stateUpdateHandler = { state in
|
||||
if case .failed(let error) = state {
|
||||
print("Discovery error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
browser.start(queue: .global(qos: .userInitiated))
|
||||
}
|
||||
|
||||
private func observeForValidation() {
|
||||
withObservationTracking {
|
||||
_ = serverURL
|
||||
@@ -156,7 +150,9 @@ struct AddServerView: View
|
||||
self.validationURL = self.serverURL
|
||||
self.validationTimer?.invalidate()
|
||||
self.validationTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
self?.validateSettings()
|
||||
Task { @MainActor [weak self] in
|
||||
self?.validateSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,77 +200,3 @@ struct AddServerView: View
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DiscoveredEndpoint: Identifiable, Hashable
|
||||
{
|
||||
let endpoint: NWEndpoint
|
||||
let serviceName: String
|
||||
|
||||
var displayName: String {
|
||||
serviceName.queueCubeServiceName
|
||||
}
|
||||
|
||||
var id: String { serviceName }
|
||||
|
||||
init(result: NWBrowser.Result) {
|
||||
self.endpoint = result.endpoint
|
||||
|
||||
switch result.endpoint {
|
||||
case .service(name: let name, type: _, domain: _, interface: _):
|
||||
self.serviceName = name
|
||||
default:
|
||||
self.serviceName = "(Unknown)"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func resolve() async throws -> Server {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let connection = NWConnection(to: endpoint, using: .tcp)
|
||||
connection.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .preparing: break
|
||||
case .ready:
|
||||
// xxx: is this really the right way to do this? Maybe we should not try to turn this into a URL.
|
||||
if case .hostPort(host: let host, port: let port) = connection.currentPath?.remoteEndpoint {
|
||||
let address = switch host {
|
||||
case .name(let string, _): string
|
||||
case .ipv4(let iPv4Address): iPv4Address.debugDescription
|
||||
case .ipv6(let iPv6Address): iPv6Address.debugDescription
|
||||
default: "unknown"
|
||||
}
|
||||
|
||||
if let server = Server(serviceName: serviceName, host: address, port: port.rawValue) {
|
||||
continuation.resume(returning: server)
|
||||
} else {
|
||||
continuation.resume(throwing: Self.Error.urlError)
|
||||
}
|
||||
} else {
|
||||
continuation.resume(throwing: Self.Error.endpointIncorrect)
|
||||
}
|
||||
|
||||
connection.cancel()
|
||||
case .cancelled:
|
||||
// expected
|
||||
break
|
||||
case .failed(let error):
|
||||
continuation.resume(throwing: error)
|
||||
connection.cancel()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .global(qos: .userInitiated))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
enum Error: Swift.Error
|
||||
{
|
||||
case cancelledConnection
|
||||
case endpointIncorrect
|
||||
case urlError
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user