[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:
+9
View File
@@ -203,6 +203,15 @@ the available space. Settings and reading pages are limited to 720 points.
Resizing preserves the connection fields and active session. iPhone retains its
portrait home screen and two-column recent-connection grid.
On iPhone, Settings → Screen orientation (屏幕方向) → Landscape after connecting
(连接后自动横屏) is enabled by default and saved locally. A new session starts in
landscape and keeps that orientation until the phone is physically held sideways;
it then resumes the existing device-following policy for the rest of the session.
Portrait, flat and unknown device poses do not release the initial landscape hold.
Turning the option off keeps the existing rotation behavior. Leaving the session
restores portrait; foregrounding the same session does not reapply a released hold.
iPad orientation and multitasking are unaffected.
## System keyboard and remote text
The session keyboard button opens the iPhone/iPad's selected system keyboard.