[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
+2
View File
@@ -9,6 +9,8 @@
两者仍为开发版。已完成的模拟器验证及未完成的真机验收见各端说明。
手机控制端的“设置 → 屏幕方向 → 连接后自动横屏”默认开启并保存在本机。连接后先保持横屏,检测到手机实际横过来后恢复跟随设备方向;竖持、平放或斜持不会解除初始横屏。关闭后沿用自动旋转,平板和多窗口不受此选项影响。退出会话恢复首页方向,返回同一后台会话不重复锁定已解锁的方向;重力传感器不可用时沿用自动旋转。
## 构建
在仓库根目录执行,当前构建脚本面向 macOS:
@@ -50,6 +50,7 @@ export class MobileState {
muted: boolean = false;
previews: boolean = false;
videoPreference: number = 1;
autoLandscape: boolean = true;
version: string = '0.1.0';
update: string = '';
updateStatus: string = '';
@@ -122,6 +123,7 @@ export class MobileController {
this.state.consent = this.store.flag('consent'); this.state.server = this.store.text('server', 'api.crossdesk.cn');
this.state.port = this.store.text('port', '9099'); this.state.remote = this.store.text('lastRemote');
this.state.relative = this.store.flag('relative', true); this.state.videoPreference = this.store.number('videoPreference', 1);
this.state.autoLandscape = this.store.flag('autoLandscape', true);
this.state.previews = this.store.flag('previews');
try { this.state.version = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT).versionName; } catch {}
try {
@@ -280,6 +282,7 @@ export class MobileController {
this.settingsTimeout = setTimeout(() => { this.video.expire(id); this.syncVideo(); }, 5000); this.syncVideo();
}
setPreference(value: number): void { this.store.put('videoPreference', value); this.state.videoPreference = value; }
setAutoLandscape(value: boolean): void { this.store.put('autoLandscape', value); this.state.autoLandscape = value; }
setPreviews(value: boolean): void {
this.store.put('previews', value); this.state.previews = value;
if (!value) { this.send({ op: 'cancelPreview' }); this.previewEligible = false; this.store.clearPreviews(); this.history(); }
@@ -427,7 +430,7 @@ export class MobileController {
this.history();
}
}).catch(() => this.toast('密码保存失败,本次连接仍可使用'));
this.platform.connected();
this.platform.connected(this.state.autoLandscape);
if (this.video.supported && this.video.requestId === 0) this.updateVideo(0, this.video.selection.quality);
if (this.receivedHostInfo) this.checkRemote(this.remoteVersion);
}
@@ -4,6 +4,7 @@ import { PiPWindow, window, display } from '@kit.ArkUI';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { picker, fileIo as fs, fileUri } from '@kit.CoreFileKit';
import { http } from '@kit.NetworkKit';
import { sensor } from '@kit.SensorServiceKit';
import { isAnnouncementURL } from './AnnouncementText';
export class MobileLifecycle {
@@ -21,6 +22,19 @@ export class MobilePlatform {
private generation: number = 0;
private backgroundActive: boolean = false;
private active: boolean = false;
private waitingForLandscape: boolean = false;
private observingOrientation: boolean = false;
private orientationRevision: number = 0;
private orientationQueue: Promise<void> = Promise.resolve();
private gravityChanged: (data: sensor.GravityResponse) => void = (data: sensor.GravityResponse) => {
if (!this.active || !this.foreground || !this.waitingForLandscape || !this.observingOrientation) return;
// Gravity is in device coordinates, independent of programmatic window rotation.
// Reject flat and diagonal poses before handing rotation back to the system.
const x = Math.abs(data.x), y = Math.abs(data.y), z = Math.abs(data.z);
if (x >= 6 && x >= y * 1.7 && x >= z) {
this.waitingForLandscape = false; this.stopOrientationSensor(); this.orientation(true);
}
};
private foreground: boolean = true;
choosing: boolean = false;
pipActive: boolean = false;
@@ -37,7 +51,9 @@ export class MobilePlatform {
this.context = context; this.ended = ended; this.release = release; this.feedback = feedback;
try { backgroundTaskManager.on('continuousTaskCancel', this.backgroundCancelled); } catch {}
}
async connected(): Promise<void> {
async connected(autoLandscape: boolean = true): Promise<void> {
if (this.active) return;
this.waitingForLandscape = autoLandscape && deviceInfo.deviceType === 'phone';
this.active = true; const generation = ++this.generation;
this.muted = false; this.orientation(true);
this.backgroundQueue = this.backgroundQueue.then(async () => {
@@ -90,7 +106,7 @@ export class MobilePlatform {
}
videoSize(width: number, height: number): void { try { this.pip?.updateContentSize(width, height); } catch { /* Not supported on this device. */ } }
async background(): Promise<void> {
this.foreground = false; this.release(); this.cancelRequests();
this.foreground = false; this.stopOrientationSensor(); this.release(); this.cancelRequests();
this.onVisibility(this.pipActive);
if (!this.active) return;
const generation = this.generation;
@@ -109,6 +125,7 @@ export class MobilePlatform {
}
dispose(): void { this.disconnect(); try { backgroundTaskManager.off('continuousTaskCancel', this.backgroundCancelled); } catch {} }
disconnect(): void {
this.waitingForLandscape = false; this.stopOrientationSensor();
this.active = false; this.choosing = false; this.generation++; this.cancelRequests();
const pip = this.pip; this.pip = undefined; this.pipActive = false;
if (pip) { try { pip.setAutoStartEnabled(false); pip.stopPiP().catch(() => {}); } catch { /* Already stopped. */ } }
@@ -117,18 +134,46 @@ export class MobilePlatform {
});
this.orientation(false);
}
private async orientation(session: boolean): Promise<void> {
const generation = this.generation;
private stopOrientationSensor(): void {
if (!this.observingOrientation) return;
this.observingOrientation = false;
try { sensor.off(sensor.SensorId.GRAVITY, this.gravityChanged); } catch {}
}
private startOrientationSensor(): void {
if (!this.waitingForLandscape || !this.foreground || this.observingOrientation) return;
try {
const win = await window.getLastWindow(this.context);
if (generation !== this.generation) return;
await win.setWindowKeepScreenOn(session);
if (generation !== this.generation) return;
// Tablets and resizable windows follow the user orientation in both views.
const phone = deviceInfo.deviceType === 'phone';
const narrow = win.getWindowProperties().windowRect.width / display.getDefaultDisplaySync().densityPixels < 600;
await win.setPreferredOrientation(!session && phone && narrow ? window.Orientation.PORTRAIT : window.Orientation.AUTO_ROTATION);
} catch { /* Window may already be going away. */ }
sensor.getSingleSensorSync(sensor.SensorId.GRAVITY);
sensor.on(sensor.SensorId.GRAVITY, this.gravityChanged, { interval: 100000000 });
this.observingOrientation = true;
} catch {
// A phone without a usable sensor keeps the previous system-following policy.
this.waitingForLandscape = false;
}
}
private orientation(session: boolean): void {
const generation = this.generation;
const revision = ++this.orientationRevision;
// Serialize window requests so an earlier landscape request cannot win after
// the sensor has released it or the session has disconnected.
this.orientationQueue = this.orientationQueue.then(async () => {
if (generation !== this.generation || revision !== this.orientationRevision) return;
try {
const win = await window.getLastWindow(this.context);
if (generation !== this.generation || revision !== this.orientationRevision) return;
await win.setWindowKeepScreenOn(session);
if (generation !== this.generation || revision !== this.orientationRevision) return;
// Tablets and resizable windows follow the user orientation in both views.
const phone = deviceInfo.deviceType === 'phone';
const rect = win.getWindowProperties().windowRect;
const narrow = Math.min(rect.width, rect.height) / display.getDefaultDisplaySync().densityPixels < 600;
const fullScreen = win.getWindowStatus() === window.WindowStatusType.FULL_SCREEN;
if (!phone || !narrow || !fullScreen) { this.waitingForLandscape = false; this.stopOrientationSensor(); }
if (session) this.startOrientationSensor();
const orientation = !session && phone && narrow && fullScreen ? window.Orientation.PORTRAIT :
session && this.waitingForLandscape ? window.Orientation.AUTO_ROTATION_LANDSCAPE : window.Orientation.AUTO_ROTATION;
await win.setPreferredOrientation(orientation);
} catch { /* Window may already be going away. */ }
});
}
private setChoosing(value: boolean): void {
this.choosing = value; this.release();
@@ -267,6 +267,15 @@ struct Mobile {
MobileSegment({ labels: ['帧率优先', '画质优先', '平衡'], selection: this.state.videoPreference,
changed: (i: number) => this.controller?.setPreference(i) })
}.padding(12).width('100%').backgroundColor(Color.White).borderRadius(26).border({ width: 0.5, color: MobileTheme.line })
this.SectionTitle('屏幕方向')
Row() {
Text('连接后自动横屏').fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.state.autoLandscape }).selectedColor(MobileTheme.green)
.onChange((value: boolean) => this.controller?.setAutoLandscape(value)).accessibilityText('连接后自动横屏')
}.padding({ left: 16, right: 16, top: 10, bottom: 10 }).width('100%').backgroundColor(Color.White)
.borderRadius(26).border({ width: 0.5, color: MobileTheme.line })
Text('手机连接后先进入横屏,将手机横过来后恢复跟随设备方向。关闭后始终跟随设备方向。平板和多窗口不受影响。')
.fontSize(12).fontColor(MobileTheme.secondary).width('100%').padding({ left: 16, right: 16, top: 8, bottom: 12 })
Column() {
this.SettingRow('服务器', this.customServer ? '自定义' : '默认', () => this.serverExpanded = !this.serverExpanded)
if (this.serverExpanded) {