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:
@@ -7,7 +7,7 @@ It allows you to remotely manage mpv's playqueue as well as the ability to searc
|
|||||||
## Sharing video links
|
## Sharing video links
|
||||||
|
|
||||||
The iOS and Mac Catalyst app includes a QueueCube share extension. Share one
|
The iOS and Mac Catalyst app includes a QueueCube share extension. Share one
|
||||||
video link, choose a player, and tap **Queue & Play** to append and play it while
|
video link and tap a saved player to append and play it while
|
||||||
keeping the existing queue. See [share extension setup, API behavior, and validation](ios/SHARE_EXTENSION.md).
|
keeping the existing queue. See [share extension setup, API behavior, and validation](ios/SHARE_EXTENSION.md).
|
||||||
|
|
||||||
## iOS TestFlight releases
|
## iOS TestFlight releases
|
||||||
|
|||||||
+3
-2
@@ -13,7 +13,7 @@ QueueCube is a SwiftUI-based jukebox client application for iOS and macOS (via M
|
|||||||
- **API.swift**: Central networking layer that handles all communication with the jukebox server. Includes REST API methods for playback control, playlist management, and WebSocket events for real-time updates.
|
- **API.swift**: Central networking layer that handles all communication with the jukebox server. Includes REST API methods for playback control, playlist management, and WebSocket events for real-time updates.
|
||||||
- **ContentView.swift**: Main view controller containing the `MainViewModel` that coordinates between UI components and API calls. Handles WebSocket event processing and data flow.
|
- **ContentView.swift**: Main view controller containing the `MainViewModel` that coordinates between UI components and API calls. Handles WebSocket event processing and data flow.
|
||||||
- **Server.swift**: Represents individual jukebox servers with support for both manual configuration and Bonjour service discovery.
|
- **Server.swift**: Represents individual jukebox servers with support for both manual configuration and Bonjour service discovery.
|
||||||
- **ServerDiscovery.swift**: Shared Bonjour browsing and bounded, cancellation-safe endpoint resolution for the app and share extension.
|
- **ServerDiscovery.swift**: Main-app Bonjour browsing and bounded, cancellation-safe endpoint resolution for Add Server.
|
||||||
- **RequestBuilder.swift**: Shared extension-safe HTTP builder and error handling.
|
- **RequestBuilder.swift**: Shared extension-safe HTTP builder and error handling.
|
||||||
- **SharePlayback.swift**: Append once, select the new queue item, unpause, and verify playback using existing endpoints.
|
- **SharePlayback.swift**: Append once, select the new queue item, unpause, and verify playback using existing endpoints.
|
||||||
- **Settings.swift**: Manages multiple server configurations stored in UserDefaults with validation through `SettingsViewModel` that tests connectivity on URL changes.
|
- **Settings.swift**: Manages multiple server configurations stored in UserDefaults with validation through `SettingsViewModel` that tests connectivity on URL changes.
|
||||||
@@ -99,5 +99,6 @@ QueueCubeApp
|
|||||||
## Share extension
|
## Share extension
|
||||||
|
|
||||||
`QueueCubeShare` is embedded in iOS and Mac Catalyst builds. It reads an App Group
|
`QueueCubeShare` is embedded in iOS and Mac Catalyst builds. It reads an App Group
|
||||||
settings snapshot and reuses the API/discovery code. See [SHARE_EXTENSION.md](SHARE_EXTENSION.md)
|
settings snapshot and reuses the API code. Tapping a saved player queues and plays
|
||||||
|
the shared link; discovery remains in the main app. See [SHARE_EXTENSION.md](SHARE_EXTENSION.md)
|
||||||
for authentication limitations, queue/play failure semantics, signing setup, and focused tests.
|
for authentication limitations, queue/play failure semantics, signing setup, and focused tests.
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
CD2026102000000000000201 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000101 /* RequestBuilder.swift */; };
|
CD2026102000000000000201 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000101 /* RequestBuilder.swift */; };
|
||||||
CD2026102000000000000202 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000102 /* Server.swift */; };
|
CD2026102000000000000202 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000102 /* Server.swift */; };
|
||||||
CD2026102000000000000203 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000103 /* Settings.swift */; };
|
CD2026102000000000000203 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000103 /* Settings.swift */; };
|
||||||
CD2026102000000000000204 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000104 /* ServerDiscovery.swift */; };
|
|
||||||
CD2026102000000000000205 /* SharedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000105 /* SharedVideo.swift */; };
|
CD2026102000000000000205 /* SharedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000105 /* SharedVideo.swift */; };
|
||||||
CD2026102000000000000206 /* SharePlayback.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000106 /* SharePlayback.swift */; };
|
CD2026102000000000000206 /* SharePlayback.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000106 /* SharePlayback.swift */; };
|
||||||
CD2026102000000000000207 /* SharedInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000107 /* SharedInput.swift */; };
|
CD2026102000000000000207 /* SharedInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2026102000000000000107 /* SharedInput.swift */; };
|
||||||
@@ -49,7 +48,6 @@
|
|||||||
CD2026102000000000000101 /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = "<group>"; };
|
CD2026102000000000000101 /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = "<group>"; };
|
||||||
CD2026102000000000000102 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
|
CD2026102000000000000102 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
|
||||||
CD2026102000000000000103 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
CD2026102000000000000103 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||||
CD2026102000000000000104 /* ServerDiscovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
|
|
||||||
CD2026102000000000000105 /* SharedVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedVideo.swift; sourceTree = "<group>"; };
|
CD2026102000000000000105 /* SharedVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedVideo.swift; sourceTree = "<group>"; };
|
||||||
CD2026102000000000000106 /* SharePlayback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePlayback.swift; sourceTree = "<group>"; };
|
CD2026102000000000000106 /* SharePlayback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePlayback.swift; sourceTree = "<group>"; };
|
||||||
CD2026102000000000000107 /* SharedInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedInput.swift; sourceTree = "<group>"; };
|
CD2026102000000000000107 /* SharedInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedInput.swift; sourceTree = "<group>"; };
|
||||||
@@ -137,7 +135,6 @@
|
|||||||
CD2026102000000000000101 /* RequestBuilder.swift */,
|
CD2026102000000000000101 /* RequestBuilder.swift */,
|
||||||
CD2026102000000000000102 /* Server.swift */,
|
CD2026102000000000000102 /* Server.swift */,
|
||||||
CD2026102000000000000103 /* Settings.swift */,
|
CD2026102000000000000103 /* Settings.swift */,
|
||||||
CD2026102000000000000104 /* ServerDiscovery.swift */,
|
|
||||||
CD2026102000000000000105 /* SharedVideo.swift */,
|
CD2026102000000000000105 /* SharedVideo.swift */,
|
||||||
CD2026102000000000000106 /* SharePlayback.swift */,
|
CD2026102000000000000106 /* SharePlayback.swift */,
|
||||||
CD2026102000000000000107 /* SharedInput.swift */,
|
CD2026102000000000000107 /* SharedInput.swift */,
|
||||||
@@ -272,7 +269,6 @@
|
|||||||
CD2026102000000000000201 /* RequestBuilder.swift in Sources */,
|
CD2026102000000000000201 /* RequestBuilder.swift in Sources */,
|
||||||
CD2026102000000000000202 /* Server.swift in Sources */,
|
CD2026102000000000000202 /* Server.swift in Sources */,
|
||||||
CD2026102000000000000203 /* Settings.swift in Sources */,
|
CD2026102000000000000203 /* Settings.swift in Sources */,
|
||||||
CD2026102000000000000204 /* ServerDiscovery.swift in Sources */,
|
|
||||||
CD2026102000000000000205 /* SharedVideo.swift in Sources */,
|
CD2026102000000000000205 /* SharedVideo.swift in Sources */,
|
||||||
CD2026102000000000000206 /* SharePlayback.swift in Sources */,
|
CD2026102000000000000206 /* SharePlayback.swift in Sources */,
|
||||||
CD2026102000000000000207 /* SharedInput.swift in Sources */,
|
CD2026102000000000000207 /* SharedInput.swift in Sources */,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Shared Bonjour discovery for the app and share extension.
|
// Bonjour discovery for the main app's Add Server view.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
import Network
|
||||||
|
|||||||
@@ -4,12 +4,8 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSBonjourServices</key>
|
|
||||||
<array>
|
|
||||||
<string>_queuecube._tcp.</string>
|
|
||||||
</array>
|
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<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>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ final class ShareViewController: UIViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
preferredContentSize = CGSize(width: 480, height: 600)
|
preferredContentSize = CGSize(width: 480, height: 600)
|
||||||
let host = UIHostingController(rootView: ShareView(model: model, send: { [weak self] in
|
let host = UIHostingController(rootView: ShareView(model: model, send: { [weak self] server in
|
||||||
self?.send()
|
self?.send(to: server)
|
||||||
}, close: { [weak self] in
|
}, close: { [weak self] in
|
||||||
self?.close()
|
self?.close()
|
||||||
}))
|
}))
|
||||||
@@ -24,7 +24,6 @@ final class ShareViewController: UIViewController {
|
|||||||
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
])
|
])
|
||||||
host.didMove(toParent: self)
|
host.didMove(toParent: self)
|
||||||
model.discovery.start()
|
|
||||||
inputTask = Task { [weak self] in
|
inputTask = Task { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
do {
|
do {
|
||||||
@@ -37,12 +36,10 @@ final class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func send() {
|
private func send(to server: Server) {
|
||||||
guard !model.sending, !model.finished, let url = model.url,
|
guard !model.sending, !model.finished, let url = model.url else { return }
|
||||||
let server = model.selectedServer else { return }
|
|
||||||
model.sending = true
|
model.sending = true
|
||||||
model.message = nil
|
model.message = nil
|
||||||
model.discovery.stop()
|
|
||||||
playbackTask = Task { [weak self] in
|
playbackTask = Task { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
do {
|
do {
|
||||||
@@ -55,7 +52,6 @@ final class ShareViewController: UIViewController {
|
|||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
model.message = error.localizedDescription
|
model.message = error.localizedDescription
|
||||||
model.finished = !error.canRetry
|
model.finished = !error.canRetry
|
||||||
if error.canRetry { model.discovery.start() }
|
|
||||||
} catch {
|
} catch {
|
||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
model.message = "The request could not be confirmed. Check the player in QueueCube before sharing again."
|
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() {
|
private func close() {
|
||||||
inputTask?.cancel()
|
inputTask?.cancel()
|
||||||
playbackTask?.cancel()
|
playbackTask?.cancel()
|
||||||
model.stop()
|
|
||||||
if model.finished {
|
if model.finished {
|
||||||
extensionContext?.completeRequest(returningItems: nil)
|
extensionContext?.completeRequest(returningItems: nil)
|
||||||
} else {
|
} else {
|
||||||
@@ -80,7 +75,6 @@ final class ShareViewController: UIViewController {
|
|||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
inputTask?.cancel()
|
inputTask?.cancel()
|
||||||
playbackTask?.cancel()
|
playbackTask?.cancel()
|
||||||
model.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -91,53 +85,18 @@ final class ShareViewController: UIViewController {
|
|||||||
|
|
||||||
@MainActor @Observable
|
@MainActor @Observable
|
||||||
private final class ShareViewModel {
|
private final class ShareViewModel {
|
||||||
let discovery = ServerDiscovery()
|
let configuredServers = Settings.fromSharedDefaults().configuredServers
|
||||||
let configuredServers: [Server]
|
|
||||||
var selectedServer: Server?
|
|
||||||
var url: URL?
|
var url: URL?
|
||||||
var loading = true
|
var loading = true
|
||||||
var sending = false
|
var sending = false
|
||||||
var finished = false
|
var finished = false
|
||||||
var succeeded = false
|
var succeeded = false
|
||||||
var resolving = false
|
|
||||||
var message: String?
|
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 {
|
private struct ShareView: View {
|
||||||
@Bindable var model: ShareViewModel
|
@Bindable var model: ShareViewModel
|
||||||
let send: () -> Void
|
let send: (Server) -> Void
|
||||||
let close: () -> Void
|
let close: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -149,40 +108,35 @@ private struct ShareView: View {
|
|||||||
} else if let url = model.url {
|
} else if let url = model.url {
|
||||||
Text(url.absoluteString)
|
Text(url.absoluteString)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.lineLimit(3)
|
.lineLimit(1)
|
||||||
|
.truncationMode(.middle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !model.finished {
|
if !model.finished {
|
||||||
Section("Choose player") {
|
Section("Choose player") {
|
||||||
if model.configuredServers.isEmpty {
|
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)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
ForEach(model.configuredServers) { server in
|
ForEach(model.configuredServers) { server in
|
||||||
Button {
|
Button {
|
||||||
model.selectedServer = server
|
send(server)
|
||||||
} label: {
|
} 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)
|
.tint(.primary)
|
||||||
}
|
.disabled(model.url == nil || model.sending)
|
||||||
if let selected = model.selectedServer,
|
|
||||||
!model.configuredServers.contains(where: { $0.id == selected.id }) {
|
|
||||||
playerLabel(selected.displayName, selected: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !model.sending {
|
}
|
||||||
Section("Nearby players") {
|
if model.sending {
|
||||||
if model.discovery.servers.isEmpty {
|
Section {
|
||||||
Text(model.discovery.errorMessage ?? "Looking for QueueCube players on your local network…")
|
ProgressView("Queuing and starting playback…")
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
ForEach(model.discovery.servers) { endpoint in
|
|
||||||
Button(endpoint.displayName) { model.select(endpoint) }
|
|
||||||
.disabled(model.resolving)
|
|
||||||
}
|
|
||||||
if model.resolving { ProgressView("Connecting…") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let message = model.message {
|
if let message = model.message {
|
||||||
@@ -191,22 +145,6 @@ private struct ShareView: View {
|
|||||||
.foregroundStyle(model.succeeded ? Color.green : Color.primary)
|
.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")
|
.navigationTitle("QueueCube")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.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") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
Open QueueCube once after updating to make saved players available to the share
|
Open QueueCube once after updating to make saved players available to the share
|
||||||
extension. In Safari or a video app, share one HTTP(S) link, choose QueueCube,
|
extension. In Safari or a video app, share one HTTP(S) link, choose QueueCube,
|
||||||
select a saved or nearby player, and tap **Queue & Play**. The link is appended
|
and tap a saved player. The link is appended
|
||||||
and played immediately; existing queue entries remain. A successful request
|
and played immediately; existing queue entries remain. A successful request
|
||||||
shows the selected player and a Done button. Video support depends on the
|
shows the selected player and a Done button. Video support depends on the
|
||||||
player's existing mpv/yt-dlp support.
|
player's existing mpv/yt-dlp support.
|
||||||
|
|
||||||
Nearby players use the same `_queuecube._tcp.` Bonjour discovery as Add Server.
|
The extension shows only saved players and does not browse for nearby players.
|
||||||
Resolved IPv4 interface suffixes are removed; scoped IPv6 addresses remain valid.
|
Add players manually or with Bonjour discovery in QueueCube Settings first.
|
||||||
Allow local network access in the containing app first. Players can also be
|
Allow local network access in the containing app to reach local players.
|
||||||
configured manually in QueueCube Settings. The extension handles URL attachments
|
The extension handles URL attachments
|
||||||
(including UTF-8 data and the absolute-URL property-list form observed from
|
(including UTF-8 data and the absolute-URL property-list form observed from
|
||||||
UIKit) and plain text containing one link. URL data keeps the same single-link
|
UIKit) and plain text containing one link. URL data keeps the same single-link
|
||||||
validation; property lists with a nonempty base or properties dictionary and
|
validation; property lists with a nonempty base or properties dictionary and
|
||||||
@@ -25,7 +25,7 @@ The app and `net.buzzert.QueueCube.Share` extension share
|
|||||||
publishes a single versioned settings snapshot on read/save. The extension reads
|
publishes a single versioned settings snapshot on read/save. The extension reads
|
||||||
that snapshot without changing the app's selected player or server list. No
|
that snapshot without changing the app's selected player or server list. No
|
||||||
queued link, credentials store, background transfer, or keychain group is added.
|
queued link, credentials store, background transfer, or keychain group is added.
|
||||||
The extension has Bonjour/local network declarations and the same HTTP transport
|
The extension has a local network usage declaration and the same HTTP transport
|
||||||
compatibility as the existing app; Catalyst adds only app sandbox and outgoing
|
compatibility as the existing app; Catalyst adds only app sandbox and outgoing
|
||||||
network access.
|
network access.
|
||||||
|
|
||||||
@@ -94,8 +94,8 @@ app/extension versions synchronized. Do not apply a single global bundle ID or
|
|||||||
provisioning profile override to both targets.
|
provisioning profile override to both targets.
|
||||||
|
|
||||||
For manual UI validation, use a dedicated simulator or test installation:
|
For manual UI validation, use a dedicated simulator or test installation:
|
||||||
configure a test player in QueueCube, share a URL from Safari, verify saved and
|
configure a test player in QueueCube, share a URL from Safari, verify saved-player
|
||||||
nearby player selection, Queue & Play success, cancellation, invalid text, and
|
tap-to-play, the empty saved-player state, cancellation, invalid text, and
|
||||||
an offline/partially failing player. Confirm the old queue is retained and the
|
an offline/partially failing player. Confirm the old queue is retained and the
|
||||||
new item is current and unpaused. On macOS also confirm QueueCube appears in
|
new item is current and unpaused. On macOS also confirm QueueCube appears in
|
||||||
Share extensions and can be invoked from a host app. Record signed-device and
|
Share extensions and can be invoked from a host app. Record signed-device and
|
||||||
|
|||||||
Reference in New Issue
Block a user