* commit 'cedd0c6aa23e8004e88b6ea1b530b1dd99c90890': ios: simplify share extension to saved-player tap-to-play
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()
|
||||||
}
|
}
|
||||||
.disabled(model.sending || model.resolving)
|
.foregroundStyle(Color.primary)
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
if let selected = model.selectedServer,
|
.tint(.primary)
|
||||||
!model.configuredServers.contains(where: { $0.id == selected.id }) {
|
.disabled(model.url == nil || model.sending)
|
||||||
playerLabel(selected.displayName, selected: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
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