feat(apple): tvOS client — third app target, first-lit in the Apple TV simulator
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
The same app now runs on tvOS (target Punktfunk-tvOS, bundle io.unom.punktfunk.tvos), validated live against the box: vkcube at 1280x720@60, 60 fps in the Apple TV 4K simulator, glass HUD with a focusable Disconnect button. - PunktfunkCore.xcframework grows tvOS device + universal-simulator slices. These are TIER-3 Rust targets (no prebuilt std): BUILD_TVOS=1 builds them with nightly and -Zbuild-std from rust-src — the full quic stack (quinn/rustls-ring/tokio) compiles for tvOS unchanged. - The UIKit stream view covers iOS AND tvOS, with pointer interaction, pointer lock, touch forwarding and InputCapture gated to iOS — tvOS is view-only until gamepad capture lands (the natural tvOS input). - SessionAudio on tvOS: .playback session, no mic (no app-accessible microphone). - App chrome gates: keyboardShortcut/textSelection/controlSize/statusBarHidden are iOS/macOS-only; host cards use the focus-native .card button style on tvOS; the Audio settings section hides (system-routed); mode seeding works from the TV screen (1920x1080@60). - Package platforms += .tvOS(.v17); new Xcode target + shared scheme (TARGETED_DEVICE_FAMILY 3, local-network usage description included). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import PackageDescription
|
|||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "PunktfunkKit",
|
name: "PunktfunkKit",
|
||||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "PunktfunkKit", targets: ["PunktfunkKit"]),
|
.library(name: "PunktfunkKit", targets: ["PunktfunkKit"]),
|
||||||
.executable(name: "PunktfunkClient", targets: ["PunktfunkClient"]),
|
.executable(name: "PunktfunkClient", targets: ["PunktfunkClient"]),
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
AA0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = AA0000000000000000000006 /* PunktfunkKit */; };
|
AA0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = AA0000000000000000000006 /* PunktfunkKit */; };
|
||||||
BB0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = BB0000000000000000000006 /* PunktfunkKit */; };
|
BB0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = BB0000000000000000000006 /* PunktfunkKit */; };
|
||||||
|
CC0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = CC0000000000000000000006 /* PunktfunkKit */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
AA0000000000000000000001 /* Punktfunk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Punktfunk.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
AA0000000000000000000001 /* Punktfunk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Punktfunk.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
BB0000000000000000000001 /* Punktfunk-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Punktfunk-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
BB0000000000000000000001 /* Punktfunk-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Punktfunk-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
CC0000000000000000000001 /* Punktfunk-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Punktfunk-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
@@ -46,6 +48,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
CC0000000000000000000004 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CC0000000000000000000005 /* PunktfunkKit in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -63,6 +73,7 @@
|
|||||||
children = (
|
children = (
|
||||||
AA0000000000000000000001 /* Punktfunk.app */,
|
AA0000000000000000000001 /* Punktfunk.app */,
|
||||||
BB0000000000000000000001 /* Punktfunk-iOS.app */,
|
BB0000000000000000000001 /* Punktfunk-iOS.app */,
|
||||||
|
CC0000000000000000000001 /* Punktfunk-tvOS.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -118,6 +129,30 @@
|
|||||||
productReference = BB0000000000000000000001 /* Punktfunk-iOS.app */;
|
productReference = BB0000000000000000000001 /* Punktfunk-iOS.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
CC0000000000000000000009 /* Punktfunk-tvOS */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = CC000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk-tvOS" */;
|
||||||
|
buildPhases = (
|
||||||
|
CC000000000000000000000B /* Sources */,
|
||||||
|
CC0000000000000000000004 /* Frameworks */,
|
||||||
|
CC000000000000000000000C /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
AA0000000000000000000002 /* App */,
|
||||||
|
AA0000000000000000000003 /* Sources/PunktfunkClient */,
|
||||||
|
);
|
||||||
|
name = "Punktfunk-tvOS";
|
||||||
|
packageProductDependencies = (
|
||||||
|
CC0000000000000000000006 /* PunktfunkKit */,
|
||||||
|
);
|
||||||
|
productName = "Punktfunk-tvOS";
|
||||||
|
productReference = CC0000000000000000000001 /* Punktfunk-tvOS.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -150,6 +185,7 @@
|
|||||||
targets = (
|
targets = (
|
||||||
AA0000000000000000000009 /* Punktfunk */,
|
AA0000000000000000000009 /* Punktfunk */,
|
||||||
BB0000000000000000000009 /* Punktfunk-iOS */,
|
BB0000000000000000000009 /* Punktfunk-iOS */,
|
||||||
|
CC0000000000000000000009 /* Punktfunk-tvOS */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -169,6 +205,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
CC000000000000000000000C /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -186,6 +229,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
CC000000000000000000000B /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@@ -434,9 +484,70 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
CC0000000000000000000012 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Punktfunkempfänger";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Punktfunk connects directly to your punktfunk host on the local network to stream video, audio, and input.";
|
||||||
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 0.1;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.tvos;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = appletvos;
|
||||||
|
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CC0000000000000000000013 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Punktfunkempfänger";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Punktfunk connects directly to your punktfunk host on the local network to stream video, audio, and input.";
|
||||||
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 0.1;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.tvos;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = appletvos;
|
||||||
|
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
CC000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk-tvOS" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CC0000000000000000000012 /* Debug */,
|
||||||
|
CC0000000000000000000013 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk" */ = {
|
AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Punktfunk" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
@@ -482,6 +593,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = PunktfunkKit;
|
productName = PunktfunkKit;
|
||||||
};
|
};
|
||||||
|
CC0000000000000000000006 /* PunktfunkKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = PunktfunkKit;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = AA000000000000000000000D /* Project object */;
|
rootObject = AA000000000000000000000D /* Project object */;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2650"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CC0000000000000000000009"
|
||||||
|
BuildableName = "Punktfunk-tvOS.app"
|
||||||
|
BlueprintName = "Punktfunk-tvOS"
|
||||||
|
ReferencedContainer = "container:Punktfunk.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CC0000000000000000000009"
|
||||||
|
BuildableName = "Punktfunk-tvOS.app"
|
||||||
|
BlueprintName = "Punktfunk-tvOS"
|
||||||
|
ReferencedContainer = "container:Punktfunk.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CC0000000000000000000009"
|
||||||
|
BuildableName = "Punktfunk-tvOS.app"
|
||||||
|
BlueprintName = "Punktfunk-tvOS"
|
||||||
|
ReferencedContainer = "container:Punktfunk.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -21,7 +21,9 @@ struct AddHostSheet: View {
|
|||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
HStack {
|
HStack {
|
||||||
Button("Cancel", role: .cancel) { dismiss() }
|
Button("Cancel", role: .cancel) { dismiss() }
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.cancelAction)
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Add Host") {
|
Button("Add Host") {
|
||||||
onAdd(StoredHost(
|
onAdd(StoredHost(
|
||||||
@@ -31,7 +33,9 @@ struct AddHostSheet: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
|
#endif
|
||||||
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct ContentView: View {
|
|||||||
@AppStorage("punktfunk.compositor") private var compositor = 0
|
@AppStorage("punktfunk.compositor") private var compositor = 0
|
||||||
@State private var showAddHost = false
|
@State private var showAddHost = false
|
||||||
@State private var pairingTarget: StoredHost?
|
@State private var pairingTarget: StoredHost?
|
||||||
#if os(iOS)
|
#if !os(macOS)
|
||||||
@State private var showSettings = false
|
@State private var showSettings = false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -88,13 +88,16 @@ struct ContentView: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 640, minHeight: 360)
|
.frame(minWidth: 640, minHeight: 360)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
#else
|
#elseif os(iOS)
|
||||||
// Streaming is immersive: edge-to-edge under the status bar and home
|
// Streaming is immersive: edge-to-edge under the status bar and home
|
||||||
// indicator, both hidden for the session (they return with the hosts grid).
|
// indicator, both hidden for the session (they return with the hosts grid).
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.statusBarHidden(true)
|
.statusBarHidden(true)
|
||||||
.persistentSystemOverlays(.hidden)
|
.persistentSystemOverlays(.hidden)
|
||||||
|
#else
|
||||||
|
.background(Color.black)
|
||||||
|
.ignoresSafeArea()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +121,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle("Punktfunkempfänger")
|
.navigationTitle("Punktfunkempfänger")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
#if os(iOS)
|
#if !os(macOS)
|
||||||
// Adjacent trailing items share one glass pill (the system default).
|
// Adjacent trailing items share one glass pill (the system default).
|
||||||
ToolbarItem(placement: .topBarTrailing) { settingsButton }
|
ToolbarItem(placement: .topBarTrailing) { settingsButton }
|
||||||
ToolbarItem(placement: .topBarTrailing) { addHostButton }
|
ToolbarItem(placement: .topBarTrailing) { addHostButton }
|
||||||
@@ -142,7 +145,7 @@ struct ContentView: View {
|
|||||||
.sheet(isPresented: $showAddHost) {
|
.sheet(isPresented: $showAddHost) {
|
||||||
AddHostSheet { store.add($0) }
|
AddHostSheet { store.add($0) }
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if !os(macOS)
|
||||||
.sheet(isPresented: $showSettings) {
|
.sheet(isPresented: $showSettings) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
@@ -185,7 +188,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if !os(macOS)
|
||||||
private var settingsButton: some View {
|
private var settingsButton: some View {
|
||||||
Button {
|
Button {
|
||||||
showSettings = true
|
showSettings = true
|
||||||
@@ -270,7 +273,11 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.buttonStyle(.card)
|
||||||
|
#else
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
#endif
|
||||||
.disabled(model.isBusy)
|
.disabled(model.isBusy)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Pair with PIN…") {
|
Button("Pair with PIN…") {
|
||||||
@@ -289,7 +296,7 @@ struct ContentView: View {
|
|||||||
/// compiled-in AppStorage defaults only apply until any value is saved; macOS keeps
|
/// compiled-in AppStorage defaults only apply until any value is saved; macOS keeps
|
||||||
/// 1080p — a desktop window is not the screen.)
|
/// 1080p — a desktop window is not the screen.)
|
||||||
private func seedDefaultModeIfNeeded() {
|
private func seedDefaultModeIfNeeded() {
|
||||||
#if os(iOS)
|
#if !os(macOS)
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
guard defaults.object(forKey: "punktfunk.width") == nil else { return }
|
guard defaults.object(forKey: "punktfunk.width") == nil else { return }
|
||||||
let bounds = UIScreen.main.nativeBounds // portrait-oriented pixels
|
let bounds = UIScreen.main.nativeBounds // portrait-oriented pixels
|
||||||
@@ -332,19 +339,25 @@ struct ContentView: View {
|
|||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
Text(Self.format(fingerprint: fingerprint))
|
Text(Self.format(fingerprint: fingerprint))
|
||||||
.font(.system(.callout, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
|
#if !os(tvOS)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
#endif
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
|
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button("Cancel", role: .cancel) { model.rejectTrust() }
|
Button("Cancel", role: .cancel) { model.rejectTrust() }
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.cancelAction)
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
Button("Trust & Connect") {
|
Button("Trust & Connect") {
|
||||||
if let fp = model.confirmTrust(), let host = model.activeHost {
|
if let fp = model.confirmTrust(), let host = model.activeHost {
|
||||||
store.pin(host.id, fingerprint: fp)
|
store.pin(host.id, fingerprint: fp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
@@ -419,7 +432,7 @@ struct ContentView: View {
|
|||||||
: "Click the stream to capture input")
|
: "Click the stream to capture input")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
#else
|
#elseif os(iOS)
|
||||||
// Touch always plays directly; ⌘⎋ (hardware keyboard) toggles kb/mouse.
|
// Touch always plays directly; ⌘⎋ (hardware keyboard) toggles kb/mouse.
|
||||||
Text(model.mouseCaptured
|
Text(model.mouseCaptured
|
||||||
? "⌘⎋ releases keyboard & mouse"
|
? "⌘⎋ releases keyboard & mouse"
|
||||||
@@ -429,7 +442,9 @@ struct ContentView: View {
|
|||||||
#endif
|
#endif
|
||||||
Button("Disconnect (⌘D)") { model.disconnect() }
|
Button("Disconnect (⌘D)") { model.disconnect() }
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut("d", modifiers: .command)
|
.keyboardShortcut("d", modifiers: .command)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ struct PairSheet: View {
|
|||||||
token.cancelled = true
|
token.cancelled = true
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.cancelAction)
|
.keyboardShortcut(.cancelAction)
|
||||||
|
#endif
|
||||||
Spacer()
|
Spacer()
|
||||||
if busy {
|
if busy {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
@@ -80,7 +82,9 @@ struct PairSheet: View {
|
|||||||
}
|
}
|
||||||
Button("Pair & Connect") { runCeremony() }
|
Button("Pair & Connect") { runCeremony() }
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
|
#if !os(tvOS)
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
|
#endif
|
||||||
.disabled(busy || pin.trimmingCharacters(in: .whitespaces).isEmpty)
|
.disabled(busy || pin.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ struct SettingsView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
#if !os(tvOS)
|
||||||
Section {
|
Section {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Picker("Speaker", selection: $speakerUID) {
|
Picker("Speaker", selection: $speakerUID) {
|
||||||
@@ -55,7 +56,9 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#if !os(tvOS)
|
||||||
Toggle("Send microphone to the host", isOn: $micEnabled)
|
Toggle("Send microphone to the host", isOn: $micEnabled)
|
||||||
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Picker("Microphone", selection: $micUID) {
|
Picker("Microphone", selection: $micUID) {
|
||||||
Text("System default").tag("")
|
Text("System default").tag("")
|
||||||
@@ -78,6 +81,7 @@ struct SettingsView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
Section {
|
Section {
|
||||||
Picker("Compositor", selection: $compositor) {
|
Picker("Compositor", selection: $compositor) {
|
||||||
Text("Automatic").tag(0)
|
Text("Automatic").tag(0)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import AppKit
|
import AppKit
|
||||||
#endif
|
#endif
|
||||||
#if os(iOS)
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|||||||
@@ -161,8 +161,18 @@ public final class SessionAudio {
|
|||||||
} catch {
|
} catch {
|
||||||
log.warning("AVAudioSession setup failed: \(error.localizedDescription)")
|
log.warning("AVAudioSession setup failed: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
#elseif os(tvOS)
|
||||||
|
do {
|
||||||
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
} catch {
|
||||||
|
log.warning("AVAudioSession setup failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
startPlayback(speakerUID: speakerUID)
|
startPlayback(speakerUID: speakerUID)
|
||||||
|
#if os(tvOS)
|
||||||
|
// No app-accessible microphone input on tvOS — playback only.
|
||||||
|
#else
|
||||||
guard micEnabled else { return }
|
guard micEnabled else { return }
|
||||||
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
||||||
case .authorized:
|
case .authorized:
|
||||||
@@ -177,6 +187,7 @@ public final class SessionAudio {
|
|||||||
default:
|
default:
|
||||||
log.warning("microphone access denied — mic uplink disabled (System Settings → Privacy)")
|
log.warning("microphone access denied — mic uplink disabled (System Settings → Privacy)")
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop both directions. Safe from any thread; waits the drain thread out (≤ its
|
/// Stop both directions. Safe from any thread; waits the drain thread out (≤ its
|
||||||
@@ -199,7 +210,7 @@ public final class SessionAudio {
|
|||||||
if wasDraining {
|
if wasDraining {
|
||||||
_ = drainDone.wait(timeout: .now() + .milliseconds(400))
|
_ = drainDone.wait(timeout: .now() + .milliseconds(400))
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if !os(macOS)
|
||||||
// Release the session so audio we interrupted (Music, podcasts) gets its
|
// Release the session so audio we interrupted (Music, podcasts) gets its
|
||||||
// resume cue.
|
// resume cue.
|
||||||
do {
|
do {
|
||||||
@@ -310,6 +321,7 @@ public final class SessionAudio {
|
|||||||
|
|
||||||
// MARK: - Mic (mic → host)
|
// MARK: - Mic (mic → host)
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
private func startCapture(micUID: String) {
|
private func startCapture(micUID: String) {
|
||||||
let engine = AVAudioEngine()
|
let engine = AVAudioEngine()
|
||||||
let input = engine.inputNode
|
let input = engine.inputNode
|
||||||
@@ -408,6 +420,7 @@ public final class SessionAudio {
|
|||||||
stateLock.unlock()
|
stateLock.unlock()
|
||||||
log.info("mic uplink started (\(micUID.isEmpty ? "default input" : micUID))")
|
log.info("mic uplink started (\(micUID.isEmpty ? "default input" : micUID))")
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private static func setDevice(_ id: AudioDeviceID, on unit: AudioUnit) -> Bool {
|
private static func setDevice(_ id: AudioDeviceID, on unit: AudioUnit) -> Bool {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
// The public type is named StreamView like its macOS twin (each is platform-gated), so
|
// The public type is named StreamView like its macOS twin (each is platform-gated), so
|
||||||
// the SwiftUI app layer is identical on both platforms.
|
// the SwiftUI app layer is identical on both platforms.
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS) || os(tvOS)
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import GameController
|
import GameController
|
||||||
import PunktfunkCore
|
import PunktfunkCore
|
||||||
@@ -66,20 +66,24 @@ public struct StreamView: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class StreamViewController: UIViewController, UIPointerInteractionDelegate {
|
public final class StreamViewController: UIViewController {
|
||||||
public private(set) var connection: PunktfunkConnection?
|
public private(set) var connection: PunktfunkConnection?
|
||||||
private var pump: StreamPump?
|
private var pump: StreamPump?
|
||||||
private var inputCapture: InputCapture?
|
|
||||||
private var captured = false
|
|
||||||
private var observers: [NSObjectProtocol] = []
|
private var observers: [NSObjectProtocol] = []
|
||||||
|
#if os(iOS)
|
||||||
|
private var inputCapture: InputCapture?
|
||||||
|
fileprivate var captured = false
|
||||||
private var pointerInteraction: UIPointerInteraction?
|
private var pointerInteraction: UIPointerInteraction?
|
||||||
|
#endif
|
||||||
|
|
||||||
var onCaptureChange: ((Bool) -> Void)?
|
var onCaptureChange: ((Bool) -> Void)?
|
||||||
|
|
||||||
var captureEnabled = true {
|
var captureEnabled = true {
|
||||||
didSet {
|
didSet {
|
||||||
guard captureEnabled != oldValue else { return }
|
guard captureEnabled != oldValue else { return }
|
||||||
|
#if os(iOS)
|
||||||
setCaptured(captureEnabled)
|
setCaptured(captureEnabled)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
|
|
||||||
public override func loadView() {
|
public override func loadView() {
|
||||||
view = StreamLayerUIView()
|
view = StreamLayerUIView()
|
||||||
|
#if os(iOS)
|
||||||
// Hide the iPadOS cursor while it hovers the video: the host renders its own
|
// Hide the iPadOS cursor while it hovers the video: the host renders its own
|
||||||
// cursor from our raw deltas, so the local one only diverges from it. (True
|
// cursor from our raw deltas, so the local one only diverges from it. (True
|
||||||
// pointer LOCK — prefersPointerLocked — isn't consulted through
|
// pointer LOCK — prefersPointerLocked — isn't consulted through
|
||||||
@@ -97,16 +102,13 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
let interaction = UIPointerInteraction(delegate: self)
|
let interaction = UIPointerInteraction(delegate: self)
|
||||||
view.addInteraction(interaction)
|
view.addInteraction(interaction)
|
||||||
pointerInteraction = interaction
|
pointerInteraction = interaction
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pointerInteraction(
|
#if os(iOS)
|
||||||
_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion
|
|
||||||
) -> UIPointerStyle? {
|
|
||||||
captured ? .hidden() : nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var prefersPointerLocked: Bool { captured }
|
public override var prefersPointerLocked: Bool { captured }
|
||||||
public override var prefersHomeIndicatorAutoHidden: Bool { true }
|
public override var prefersHomeIndicatorAutoHidden: Bool { true }
|
||||||
|
#endif
|
||||||
|
|
||||||
func start(
|
func start(
|
||||||
connection: PunktfunkConnection,
|
connection: PunktfunkConnection,
|
||||||
@@ -116,6 +118,7 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
stop()
|
stop()
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
|
#if os(iOS)
|
||||||
// Read the LIVE mode per touch batch — an accepted requestMode() mid-stream
|
// Read the LIVE mode per touch batch — an accepted requestMode() mid-stream
|
||||||
// changes the letterbox, and touches must follow it.
|
// changes the letterbox, and touches must follow it.
|
||||||
streamView.currentHostMode = { [weak connection] in
|
streamView.currentHostMode = { [weak connection] in
|
||||||
@@ -137,6 +140,7 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
}
|
}
|
||||||
capture.start()
|
capture.start()
|
||||||
inputCapture = capture
|
inputCapture = capture
|
||||||
|
#endif
|
||||||
|
|
||||||
let pump = StreamPump()
|
let pump = StreamPump()
|
||||||
pump.start(
|
pump.start(
|
||||||
@@ -144,6 +148,7 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
onFrame: onFrame, onSessionEnd: onSessionEnd)
|
onFrame: onFrame, onSessionEnd: onSessionEnd)
|
||||||
self.pump = pump
|
self.pump = pump
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
// GC only delivers while active; everything held is flushed by InputCapture's
|
// GC only delivers while active; everything held is flushed by InputCapture's
|
||||||
// own resign observer — here we just mirror the capture state for the HUD and
|
// own resign observer — here we just mirror the capture state for the HUD and
|
||||||
// the pointer lock.
|
// the pointer lock.
|
||||||
@@ -156,21 +161,25 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
if captureEnabled {
|
if captureEnabled {
|
||||||
setCaptured(true) // entering a session is the deliberate "capture me" moment
|
setCaptured(true) // entering a session is the deliberate "capture me" moment
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
setCaptured(false)
|
|
||||||
observers.forEach(NotificationCenter.default.removeObserver(_:))
|
observers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||||
observers.removeAll()
|
observers.removeAll()
|
||||||
|
#if os(iOS)
|
||||||
|
setCaptured(false)
|
||||||
inputCapture?.stop()
|
inputCapture?.stop()
|
||||||
inputCapture = nil
|
inputCapture = nil
|
||||||
|
streamView.onTouchEvent = nil
|
||||||
|
streamView.currentHostMode = nil
|
||||||
|
#endif
|
||||||
pump?.stop()
|
pump?.stop()
|
||||||
pump = nil
|
pump = nil
|
||||||
connection = nil
|
connection = nil
|
||||||
streamView.onTouchEvent = nil
|
|
||||||
streamView.currentHostMode = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
private func setCaptured(_ on: Bool) {
|
private func setCaptured(_ on: Bool) {
|
||||||
if on {
|
if on {
|
||||||
guard captureEnabled, !captured, pump != nil else { return }
|
guard captureEnabled, !captured, pump != nil else { return }
|
||||||
@@ -187,6 +196,7 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
let captured = captured
|
let captured = captured
|
||||||
DispatchQueue.main.async { onCaptureChange?(captured) }
|
DispatchQueue.main.async { onCaptureChange?(captured) }
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
observers.forEach(NotificationCenter.default.removeObserver(_:))
|
observers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||||
@@ -194,6 +204,16 @@ public final class StreamViewController: UIViewController, UIPointerInteractionD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
extension StreamViewController: UIPointerInteractionDelegate {
|
||||||
|
public func pointerInteraction(
|
||||||
|
_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion
|
||||||
|
) -> UIPointerStyle? {
|
||||||
|
captured ? .hidden() : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// The layer-backed video surface + touch source. Touches are mapped through the
|
/// The layer-backed video surface + touch source. Touches are mapped through the
|
||||||
/// aspect-fit letterbox into host-mode pixels (surface == host mode, so the host-side
|
/// aspect-fit letterbox into host-mode pixels (surface == host mode, so the host-side
|
||||||
/// rescale is the identity); touches outside the video area are clamped onto its edge.
|
/// rescale is the identity); touches outside the video area are clamped onto its edge.
|
||||||
@@ -204,23 +224,28 @@ final class StreamLayerUIView: UIView {
|
|||||||
layer as! AVSampleBufferDisplayLayer
|
layer as! AVSampleBufferDisplayLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
/// Reads the LIVE negotiated mode in pixels (the touch coordinate space).
|
/// Reads the LIVE negotiated mode in pixels (the touch coordinate space).
|
||||||
var currentHostMode: (() -> CGSize)?
|
var currentHostMode: (() -> CGSize)?
|
||||||
var onTouchEvent: ((PunktfunkInputEvent) -> Void)?
|
var onTouchEvent: ((PunktfunkInputEvent) -> Void)?
|
||||||
|
|
||||||
/// Wire touch ids per active UITouch; ids are reused after the touch ends.
|
/// Wire touch ids per active UITouch; ids are reused after the touch ends.
|
||||||
private var touchIDs: [ObjectIdentifier: UInt32] = [:]
|
private var touchIDs: [ObjectIdentifier: UInt32] = [:]
|
||||||
|
#endif
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
displayLayer.videoGravity = .resizeAspect
|
displayLayer.videoGravity = .resizeAspect
|
||||||
|
#if os(iOS)
|
||||||
isMultipleTouchEnabled = true
|
isMultipleTouchEnabled = true
|
||||||
|
#endif
|
||||||
backgroundColor = .black
|
backgroundColor = .black
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
required init?(coder: NSCoder) { fatalError("not used") }
|
required init?(coder: NSCoder) { fatalError("not used") }
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
forward(touches, kind: .down)
|
forward(touches, kind: .down)
|
||||||
}
|
}
|
||||||
@@ -277,11 +302,14 @@ final class StreamLayerUIView: UIView {
|
|||||||
while touchIDs.values.contains(id) { id += 1 }
|
while touchIDs.values.contains(id) { id += 1 }
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
extension CGFloat {
|
extension CGFloat {
|
||||||
fileprivate func clamped(to range: ClosedRange<CGFloat>) -> CGFloat {
|
fileprivate func clamped(to range: ClosedRange<CGFloat>) -> CGFloat {
|
||||||
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ cd "$(dirname "$0")/.."
|
|||||||
|
|
||||||
TARGETS_MAC=(aarch64-apple-darwin x86_64-apple-darwin)
|
TARGETS_MAC=(aarch64-apple-darwin x86_64-apple-darwin)
|
||||||
BUILD_IOS="${BUILD_IOS:-0}" # BUILD_IOS=1 adds iOS device + simulator slices (rustup targets aarch64-apple-ios{,-sim})
|
BUILD_IOS="${BUILD_IOS:-0}" # BUILD_IOS=1 adds iOS device + simulator slices (rustup targets aarch64-apple-ios{,-sim})
|
||||||
|
BUILD_TVOS="${BUILD_TVOS:-0}" # BUILD_TVOS=1 adds tvOS slices — TIER-3 Rust targets: needs `rustup toolchain install nightly` + `rustup component add rust-src --toolchain nightly`
|
||||||
|
|
||||||
# Deployment targets must match Package.swift's platforms, or every consumer link emits
|
# Deployment targets must match Package.swift's platforms, or every consumer link emits
|
||||||
# "object file was built for newer macOS version" warnings.
|
# "object file was built for newer macOS version" warnings.
|
||||||
@@ -23,6 +24,15 @@ if [[ "$BUILD_IOS" == "1" ]]; then
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo build --release -p punktfunk-core --features quic --target aarch64-apple-ios-sim
|
IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo build --release -p punktfunk-core --features quic --target aarch64-apple-ios-sim
|
||||||
IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo build --release -p punktfunk-core --features quic --target x86_64-apple-ios
|
IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo build --release -p punktfunk-core --features quic --target x86_64-apple-ios
|
||||||
fi
|
fi
|
||||||
|
if [[ "$BUILD_TVOS" == "1" ]]; then
|
||||||
|
# Tier-3 targets: no prebuilt std — nightly + -Zbuild-std compiles it from rust-src.
|
||||||
|
TVOS_DEPLOYMENT_TARGET=17.0 cargo +nightly build --release -p punktfunk-core --features quic \
|
||||||
|
-Z build-std=std,panic_abort --target aarch64-apple-tvos
|
||||||
|
TVOS_DEPLOYMENT_TARGET=17.0 cargo +nightly build --release -p punktfunk-core --features quic \
|
||||||
|
-Z build-std=std,panic_abort --target aarch64-apple-tvos-sim
|
||||||
|
TVOS_DEPLOYMENT_TARGET=17.0 cargo +nightly build --release -p punktfunk-core --features quic \
|
||||||
|
-Z build-std=std,panic_abort --target x86_64-apple-tvos
|
||||||
|
fi
|
||||||
|
|
||||||
STAGE="$(mktemp -d)"
|
STAGE="$(mktemp -d)"
|
||||||
trap 'rm -rf "$STAGE"' EXIT
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
@@ -59,6 +69,15 @@ if [[ "$BUILD_IOS" == "1" ]]; then
|
|||||||
ARGS+=(-library target/aarch64-apple-ios/release/libpunktfunk_core.a -headers "$STAGE/include")
|
ARGS+=(-library target/aarch64-apple-ios/release/libpunktfunk_core.a -headers "$STAGE/include")
|
||||||
ARGS+=(-library "$STAGE/iossim/libpunktfunk_core.a" -headers "$STAGE/include")
|
ARGS+=(-library "$STAGE/iossim/libpunktfunk_core.a" -headers "$STAGE/include")
|
||||||
fi
|
fi
|
||||||
|
if [[ "$BUILD_TVOS" == "1" ]]; then
|
||||||
|
mkdir -p "$STAGE/tvossim"
|
||||||
|
lipo -create \
|
||||||
|
target/aarch64-apple-tvos-sim/release/libpunktfunk_core.a \
|
||||||
|
target/x86_64-apple-tvos/release/libpunktfunk_core.a \
|
||||||
|
-output "$STAGE/tvossim/libpunktfunk_core.a"
|
||||||
|
ARGS+=(-library target/aarch64-apple-tvos/release/libpunktfunk_core.a -headers "$STAGE/include")
|
||||||
|
ARGS+=(-library "$STAGE/tvossim/libpunktfunk_core.a" -headers "$STAGE/include")
|
||||||
|
fi
|
||||||
|
|
||||||
# Cargo does NOT fingerprint MACOSX_DEPLOYMENT_TARGET — units cached from a build without
|
# Cargo does NOT fingerprint MACOSX_DEPLOYMENT_TARGET — units cached from a build without
|
||||||
# it keep their old minos forever. Refuse to ship anything newer than the package floor
|
# it keep their old minos forever. Refuse to ship anything newer than the package floor
|
||||||
|
|||||||
Reference in New Issue
Block a user