[feat] add configurable landscape entry for mobile sessions

This commit is contained in:
dijunkun
2026-10-24 03:28:23 +08:00
parent 182899d238
commit 4650e48ea3
12 changed files with 257 additions and 44 deletions
+6 -1
View File
@@ -75,7 +75,7 @@ struct ContentView: View {
}
.onAppear {
if !session.sessionVisible {
AppOrientation.update(to: .portrait)
AppOrientation.endSession()
}
if scenePhase == .active {
session.applicationDidBecomeActive()
@@ -840,6 +840,11 @@ private struct ServerSettingsView: View {
}
.pickerStyle(.segmented)
}
Section {
Toggle("连接后自动横屏", isOn: $session.autoLandscape)
} header: {
Text("屏幕方向")
}
Section {
DisclosureGroup(isExpanded: $serverConfigurationExpanded) {
Toggle("使用自定义服务器", isOn: $usesCustomServer)
@@ -12,24 +12,88 @@ final class CrossDeskAppDelegate: NSObject, UIApplicationDelegate {
}
}
@MainActor
enum AppOrientation {
static func update(to orientations: UIInterfaceOrientationMask) {
DispatchQueue.main.async {
// iPad windows can rotate and resize independently of the session.
// Keep all orientations available and let the window manager choose
// the geometry; the phone still locks the home screen to portrait.
guard UIDevice.current.userInterfaceIdiom != .pad else { return }
CrossDeskAppDelegate.supportedOrientations = orientations
nonisolated static let autoLandscapeKey = "crossdesk.mobile.auto-landscape"
private static var sessionActive = false
private static var waitingForLandscape = false
private static var deviceObserver: NSObjectProtocol?
for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
scene.windows.forEach {
$0.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
}
scene.requestGeometryUpdate(
.iOS(interfaceOrientations: orientations)
) { error in
NSLog("CrossDesk orientation update failed: %@", error.localizedDescription)
}
nonisolated static func beginSession(autoLandscape: Bool) {
DispatchQueue.main.async { beginSessionOnMain(autoLandscape: autoLandscape) }
}
private static func beginSessionOnMain(autoLandscape: Bool) {
// Repeated transport-ready callbacks must not lock an existing session again.
guard !sessionActive else { return }
sessionActive = true
waitingForLandscape = autoLandscape && UIDevice.current.userInterfaceIdiom == .phone
if waitingForLandscape {
CrossDeskAppDelegate.supportedOrientations = .landscape
resumeObserving()
} else {
apply(to: .allButUpsideDown)
}
}
nonisolated static func endSession() {
DispatchQueue.main.async {
sessionActive = false
waitingForLandscape = false
stopObserving()
apply(to: .portrait)
}
}
nonisolated static func resume() {
DispatchQueue.main.async { resumeObserving() }
}
private static func resumeObserving() {
guard sessionActive, waitingForLandscape, deviceObserver == nil,
UIApplication.shared.applicationState == .active else { return }
apply(to: .landscape)
deviceObserver = NotificationCenter.default.addObserver(
forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main
) { _ in
MainActor.assumeIsolated { deviceDidRotate() }
}
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
deviceDidRotate()
}
nonisolated static func suspend() {
DispatchQueue.main.async { stopObserving() }
}
private static func stopObserving() {
guard let observer = deviceObserver else { return }
NotificationCenter.default.removeObserver(observer)
deviceObserver = nil
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
private static func deviceDidRotate() {
// UIDevice reports physical posture; window geometry also changes when
// we request landscape and must never release the initial lock itself.
guard sessionActive, waitingForLandscape, deviceObserver != nil,
UIApplication.shared.applicationState == .active,
UIDevice.current.orientation.isLandscape else { return }
waitingForLandscape = false
stopObserving()
apply(to: .allButUpsideDown)
}
private static func apply(to orientations: UIInterfaceOrientationMask) {
// iPad keeps every orientation available, including in resizable windows.
guard UIDevice.current.userInterfaceIdiom != .pad else { return }
CrossDeskAppDelegate.supportedOrientations = orientations
for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
scene.windows.forEach {
$0.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
}
scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations)) { error in
NSLog("CrossDesk orientation update failed: %@", error.localizedDescription)
}
}
}
@@ -312,6 +312,9 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
@Published var videoAdaptationPolicy = VideoAdaptationPolicy.saved {
didSet { videoAdaptationPolicy.save() }
}
@Published var autoLandscape = UserDefaults.standard.object(forKey: AppOrientation.autoLandscapeKey) as? Bool ?? true {
didSet { UserDefaults.standard.set(autoLandscape, forKey: AppOrientation.autoLandscapeKey) }
}
@Published private(set) var videoSettings = RemoteVideoSettingsState()
@Published var remoteID = ""
@Published var password = ""
@@ -437,6 +440,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
func applicationDidBecomeActive() {
appIsActive = true
AppOrientation.resume()
pictureInPicture.didBecomeActive()
appUpdates.setEnabled(hasNetworkConsent)
privacyNotice.didBecomeActive(hasNetworkConsent: hasNetworkConsent)
@@ -447,6 +451,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
func applicationDidEnterBackground() {
appIsActive = false
AppOrientation.suspend()
appUpdates.setEnabled(false)
privacyNotice.didEnterBackground()
suspendVideoForBackground()
@@ -591,6 +596,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
private func beginRemoteConnection(_ identifier: String) {
AppOrientation.endSession()
networkStatistics = RemoteNetworkStatistics()
resetRemoteHostInfo()
cancelVideoRecovery()
@@ -714,7 +720,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
selectedDisplay = 0
connectionStatus = "未连接"
audioPlayer.endSession()
AppOrientation.update(to: .portrait)
AppOrientation.endSession()
appUpdates.checkNow()
}
@@ -1191,7 +1197,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
recordSuccessfulConnectionIfNeeded()
audioPlayer.setEnabled(audioEnabled)
bridge.setAudioEnabled(audioEnabled)
AppOrientation.update(to: .allButUpsideDown)
AppOrientation.beginSession(autoLandscape: autoLandscape)
case 2: connectionStatus = "正在收集 ICE 候选"
case 3: finishConnection(withError: "网络连接已中断,请重新连接。")
case 4: