The host surfaces (design §5.2/§5.2a). A bound host wears a tinted chip that says what a click will do. The card menu grows "Connect with ▸" — a ONE-OFF that never rebinds, with a checkmark on the binding and an explicit "Set Default Profile" as its last item for the users who do want to rebind from there — plus "Pin as Card ▸" and "Copy Link". A pinned host+profile combo becomes its own card next to its host: same record, same live status, the profile as the prominent subtitle, one click to connect. It is an extra grid ENTRY, never a duplicated host record — duplicating would fork pairing, Wake-on-LAN and renames. Its menu carries only connect-shaped actions; edit, pair, forget and remove stay on the primary card, where the thing they act on lives. On the gamepad carousel and tvOS the same pins are tiles, which is the whole point: focus-and-press is what those surfaces do well, and menus are not. The edit sheet binds and pins; both rows vanish when no profiles exist, so a user who never makes one sees exactly today's sheet. A dangling binding renders as "Default settings (profile deleted)" and is cleaned up on save. The speed test finally writes where the tested host reads. Unbound: the global, as before. Bound to a profile that overrides bitrate: that override. Bound to one that inherits it: both are offered, because either is defensible and guessing would silently pick one. Every button names its target, and the probe now runs at the mode that host would actually stream — the measurement is the streaming path. Shortcuts gains `ProfileEntity` over the App-Group catalog and a Profile parameter on Connect, still round-tripping through the URL router rather than opening a second connect path. Connect and Wake drop their iOS wall — AppIntents is real on macOS and tvOS, and "Stream Desktop with Work" from Spotlight was the ask; only the phrases provider stays iOS-gated, since it bundles the LiveActivityIntent. The deep-link observer moved out with them: an intent that posts to nobody is a shortcut that silently does nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54 lines
2.0 KiB
Swift
54 lines
2.0 KiB
Swift
// A settings profile as an App Intents entity — the parameter type for "Stream <host> with
|
|
// <profile>" in Shortcuts (design/client-deep-links.md §6). Lives beside `HostEntity` in the
|
|
// shared module for the same reason: an intent (and, later, a configurable widget) executes
|
|
// outside the app, so the entity and its query must not need PunktfunkKit.
|
|
//
|
|
// The query reads the App-Group catalog blob directly — no Rust core, no app process — exactly
|
|
// like `HostEntityQuery` reads the saved hosts.
|
|
|
|
#if canImport(AppIntents)
|
|
import AppIntents
|
|
import Foundation
|
|
|
|
public struct ProfileEntity: AppEntity, Identifiable {
|
|
public static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Settings Profile")
|
|
public static let defaultQuery = ProfileEntityQuery()
|
|
|
|
/// The catalog id — stable across renames, which is why a shortcut keeps working after the
|
|
/// user renames the profile it points at.
|
|
public let id: String
|
|
public let name: String
|
|
/// `#RRGGBB`, when the profile has been given one. Carried so a future display representation
|
|
/// (and the configurable widget) can tint without a second lookup.
|
|
public let accent: String?
|
|
|
|
public init(id: String, name: String, accent: String? = nil) {
|
|
self.id = id
|
|
self.name = name
|
|
self.accent = accent
|
|
}
|
|
|
|
public init(_ profile: StreamProfile) {
|
|
self.init(id: profile.id, name: profile.name, accent: profile.accent)
|
|
}
|
|
|
|
public var displayRepresentation: DisplayRepresentation {
|
|
DisplayRepresentation(title: "\(name)")
|
|
}
|
|
}
|
|
|
|
public struct ProfileEntityQuery: EntityQuery {
|
|
public init() {}
|
|
|
|
public func entities(for identifiers: [String]) async throws -> [ProfileEntity] {
|
|
ProfileCatalog.load().profiles
|
|
.filter { identifiers.contains($0.id) }
|
|
.map(ProfileEntity.init)
|
|
}
|
|
|
|
public func suggestedEntities() async throws -> [ProfileEntity] {
|
|
ProfileCatalog.load().profiles.map(ProfileEntity.init)
|
|
}
|
|
}
|
|
#endif
|