Files
crossdesk/apps/harmony/entry/src/main/ets/mobile/MobileController.ets
T

454 lines
30 KiB
Plaintext

import bridge from 'libcrossdesk.so';
import { common, bundleManager } from '@kit.AbilityKit';
import { display, promptAction } from '@kit.ArkUI';
import { util } from '@kit.ArkTS';
import { pasteboard, systemDateTime } from '@kit.BasicServicesKit';
import { MobileStore, RecentDevice } from './MobileStore';
import { MobilePlatform, MobileLifecycle } from './MobilePlatform';
import { AnnouncementInbox, AnnouncementItem, AnnouncementRequest, AnnouncementResponse } from './AnnouncementInbox';
import { VideoSettings, VideoValues, NetworkStatistics, NetworkReport, RecentPresence, PresenceMessage, asciiKey } from './ControllerModels';
interface Command {
op: string; session?: number; connection?: number; server?: string; port?: number; identity?: string;
logs?: string; certificates?: string; directory?: string; previewDirectory?: string; remote?: string; password?: string; version?: string;
id?: string; x?: number; y?: number; flag?: number; scroll?: number; code?: number; down?: boolean;
index?: number; muted?: boolean; visible?: boolean; text?: string; message?: Object; quality?: number; frameRate?: number;
preference?: number; request?: number; path?: string; name?: string; body?: string; local?: boolean;
}
interface NativeEvent {
type: string; session: number; connection?: number; status?: number; controller?: boolean; generation?: number;
id?: string; identity?: string; message?: Object; report?: NetworkReport; width?: number; height?: number;
frame?: number; latency?: number; latencyCount?: number; text?: string; reason?: string; name?: string; progress?: number;
sending?: boolean; path?: string; ok?: boolean; request?: number | string; latest?: string;
}
export interface RemoteDisplay { name: string; }
interface HostInfo { host_name: string; platform: string; app_version: string; supports_video_settings: boolean; displays: RemoteDisplay[]; }
interface SettingAck { quality: number; frame_rate: number; preference: number; request_id: number; accepted: boolean; }
interface ServiceStatus { available: boolean; }
interface ControlData { type: number; host_info?: HostInfo; video_settings?: SettingAck; service_status?: ServiceStatus; }
interface PresenceRequest { type: string; devices: string[]; subscribe: boolean; }
interface SignalType { type: string; }
@Observed
export class MobileState {
page: string = 'home';
consent: boolean = false;
privacyAvailable: boolean = false;
modal: string = '';
notice: string = '';
server: string = 'api.crossdesk.cn';
port: string = '9099';
remote: string = '';
password: string = '';
remember: boolean = false;
signal: number = -1;
signalText: string = '未连接';
identity: string = '';
connecting: boolean = false;
ready: boolean = false;
relative: boolean = true;
muted: boolean = false;
previews: boolean = false;
videoPreference: number = 1;
autoLandscape: boolean = true;
version: string = '0.1.0';
update: string = '';
updateStatus: string = '';
recent: RecentDevice[] = [];
width: number = 16;
height: number = 9;
hasFrame: boolean = false;
displays: RemoteDisplay[] = [];
selectedDisplay: number = 0;
viewportReset: number = 0;
platform: string = '';
hostName: string = '';
transfer: string = '';
receivedPath: string = '';
receivedName: string = '';
remoteClipboard: string = '';
clipboardText: string = '';
videoQuality: number = 1;
videoRate: number = 30;
videoSelectedPreference: number = 1;
videoSupported: boolean = false;
videoFeedback: string = '';
networkTime: number = 0;
unread: number = 0;
announcements: AnnouncementItem[] = [];
announcement?: AnnouncementItem;
documentTitle: string = '';
documentText: string = '';
documentBack: string = 'about';
}
export class MobileController {
state: MobileState = new MobileState();
store: MobileStore;
platform: MobilePlatform;
video: VideoSettings = new VideoSettings();
network: NetworkStatistics = new NetworkStatistics();
presence: RecentPresence;
inbox: AnnouncementInbox;
releaseInput: () => void = () => {};
private foreground: boolean = true;
private context: common.UIAbilityContext;
private timer: number = -1;
private seconds: number = 0;
private session: number = 0;
private connection: number = 0;
private started: boolean = false;
private starting: boolean = false;
private signalGeneration: number = -1;
private pendingPassword: string = '';
private connectingId: string = '';
private sendingFile: boolean = false;
private serviceAvailable: boolean = false;
private settingsTimeout: number = -1;
private versionRequest: number = 0;
private localVersionRequest: number = 0;
private remoteVersionRequest: number = 0;
private checkingLocal: boolean = false;
private checkedRemote: boolean = false;
private remoteVersion: string = '';
private receivedHostInfo: boolean = false;
private previewEligible: boolean = false;
private historySaved: boolean = false;
private previewPending: boolean = false;
private previewSaved: boolean = false;
private previewAttempts: number = 0;
private uiEpoch: number = 0;
private privacy: string = '';
constructor(context: common.UIAbilityContext) {
this.context = context; this.store = new MobileStore(context);
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 {
this.privacy = util.TextDecoder.create('utf-8').decodeWithStream(context.resourceManager.getRawFileContentSync('PRIVACY.md'));
this.state.privacyAvailable = this.privacy.trim().length > 0;
} catch { this.state.privacyAvailable = false; }
this.platform = new MobilePlatform(context, (reason: string) => this.end(reason), () => this.release(), (text: string) => this.toast(text));
this.platform.onMute = (muted: boolean) => { if (muted !== this.state.muted) this.mute(); };
this.platform.onVisibility = (visible: boolean) => { if (this.state.ready) this.send({ op: 'visible', visible: visible }); };
this.presence = new RecentPresence((ids: string[]) => {
const request: PresenceRequest = { type: 'recent_connections_presence', devices: ids, subscribe: true };
this.send({ op: 'signal', message: request });
});
this.inbox = new AnnouncementInbox((request: AnnouncementRequest) => this.send({ op: 'signal', message: request }),
(scope: string) => this.store.text(`inbox:${scope}`, '{}'), (scope: string, body: string) => this.store.put(`inbox:${scope}`, body));
this.inbox.changed = () => { this.state.announcements = this.inbox.items.slice(); this.state.unread = this.inbox.unread; };
MobileLifecycle.foreground = () => this.resume(); MobileLifecycle.background = () => this.background(); MobileLifecycle.destroy = () => this.destroy();
this.history(); this.timer = setInterval(() => this.poll(), 50);
if (!this.state.consent) this.state.modal = 'consent'; else this.resume();
}
private send(command: Command): void { bridge.mobileCommand(JSON.stringify(command)); }
scope(): string { return MobileStore.scope(this.state.server, Number(this.state.port)); }
now(): number { return systemDateTime.getUptime(systemDateTime.TimeType.STARTUP, false); }
toast(text: string): void { if (this.foreground) promptAction.showToast({ message: text, duration: 3000 }); else this.state.notice = text; }
async consent(): Promise<void> {
if (!this.state.privacyAvailable) { this.state.notice = '隐私政策无法读取,请重新安装应用'; return; }
try { this.store.put('consent', true); this.state.consent = true; this.state.modal = ''; await this.ensureSignal(); this.checkLocal(); }
catch { this.toast('无法保存联网授权,请重试'); }
}
revoke(): void {
this.end(''); this.stopSignal(); this.state.consent = false; this.state.previews = false;
this.store.put('consent', false); this.store.put('previews', false); this.store.clearPreviews(); this.history();
this.state.update = ''; this.state.updateStatus = ''; this.state.page = 'home'; this.state.modal = 'consent';
}
resume(): void {
this.foreground = true; this.platform.resume();
if (this.state.consent) { this.ensureSignal(); this.checkLocal(); this.inbox.connect(this.state.signal === 1); }
else this.state.modal = 'consent';
}
background(): void {
this.foreground = false; this.uiEpoch++; this.release(); this.platform.background();
this.localVersionRequest = this.remoteVersionRequest = 0; this.checkingLocal = false; this.inbox.connect(false);
if (!this.state.ready && !this.platform.choosing) { if (this.state.connecting) this.end(''); this.stopSignal(); }
}
destroy(): void { this.foreground = false; this.end(''); this.stopSignal(); this.platform.dispose(); if (this.timer >= 0) clearInterval(this.timer); this.timer = -1; }
private async ensureSignal(): Promise<void> {
if (!this.foreground || !this.state.consent || this.started || this.starting) return;
this.starting = true; const session = ++this.session, scope = this.scope();
try {
let identity = await this.store.secret(`identity:${scope}`);
// A public ID alone is not a login credential. Discard legacy incomplete records.
if (identity && !/^[0-9]{9}@.+$/.test(identity)) { await this.store.removeSecret(`identity:${scope}`); identity = ''; }
if (session !== this.session || !this.foreground || !this.state.consent) return;
this.state.signalText = '正在连接…'; this.state.signal = 0; this.started = true;
this.send({ op: 'start', session: session, server: this.state.server, port: Number(this.state.port), identity: identity,
logs: this.context.cacheDir, certificates: '/etc/security/certificates', directory: this.store.directory,
previewDirectory: this.store.previewDirectory, version: this.state.version });
} catch { this.state.signalText = '安全存储不可用,请解锁设备后重试'; this.state.signal = -1; }
finally { if (session === this.session) this.starting = false; }
}
private stopSignal(): void {
this.session++; this.starting = this.started = false; this.signalGeneration = -1;
this.send({ op: 'stop' }); this.state.signal = -1; this.state.identity = ''; this.state.signalText = '未连接';
this.presence.connect(false, this.now()); this.inbox.connect(false); this.platform.cancelRequests();
this.localVersionRequest = this.remoteVersionRequest = 0; this.checkingLocal = false;
}
reconnect(): void { if (!this.state.consent) { this.state.modal = 'consent'; return; } this.stopSignal(); this.ensureSignal(); }
saveSettings(server: string, port: string): void {
server = server.trim(); const number = Number(port);
if (!server || server.length > 253 || /[\s/@?#]/.test(server) || !Number.isInteger(number) || number < 1 || number > 65535) {
this.toast('请输入有效的服务器地址和端口'); return;
}
const changed = server !== this.state.server || port !== this.state.port;
this.store.put('server', server); this.store.put('port', port); this.state.server = server; this.state.port = port;
if (changed) { this.end(''); this.stopSignal(); this.inbox.reset(); this.history(); this.ensureSignal(); }
this.navigate('settings');
}
navigate(page: string): void { this.state.page = page; this.state.modal = ''; }
history(): void {
const values = this.store.recent(this.scope()), now = this.now(); this.presence?.watch(values.map((d: RecentDevice) => d.id), now);
this.state.recent = values.filter((d: RecentDevice) => this.presence?.online(d.id, now))
.concat(values.filter((d: RecentDevice) => !this.presence?.online(d.id, now)));
}
async deleteRecent(id: string): Promise<void> {
try { await this.store.delete(this.scope(), id); this.history(); } catch { this.toast('记录或密码删除失败,请重试'); }
}
async requestConnection(id: string): Promise<void> {
id = id.replace(/\s/g, ''); this.state.remote = id;
if (!/^[0-9]{9}$/.test(id)) { this.toast('请输入 9 位设备 ID'); return; }
if (!this.state.consent) { this.state.modal = 'consent'; return; }
const epoch = ++this.uiEpoch, scope = this.scope();
this.state.password = ''; this.state.notice = '';
this.state.remember = this.store.recent(scope).find((d: RecentDevice) => d.id === id)?.remember ?? false;
try {
const password = await this.store.secret(`password:${scope}:${id}`);
if (epoch !== this.uiEpoch || !this.foreground || !this.state.consent) return;
if (password) { this.state.remember = true; this.begin(password); return; }
} catch { if (epoch === this.uiEpoch) this.state.notice = '已保存密码无法读取,请重新输入'; }
if (epoch === this.uiEpoch) this.state.modal = 'password';
}
begin(password: string): void {
if (!this.state.consent || !/^[0-9]{9}$/.test(this.state.remote) || !password || password.length > 128 || password.includes('@')) { this.toast('请输入有效的访问密码'); return; }
this.joinSent = false; this.connection++; this.connectingId = this.state.remote; this.pendingPassword = password;
this.state.connecting = true; this.state.ready = false; this.state.modal = 'connecting'; this.state.notice = '';
this.state.muted = false; this.state.platform = ''; this.state.hostName = ''; this.state.receivedPath = this.state.receivedName = this.state.transfer = '';
this.state.remoteClipboard = ''; this.state.displays = []; this.state.width = 16; this.state.height = 9; this.state.hasFrame = false;
this.state.selectedDisplay = 0; this.state.viewportReset++; this.remoteVersion = ''; this.receivedHostInfo = false;
this.serviceAvailable = this.checkedRemote = this.sendingFile = false;
this.previewEligible = this.state.previews; this.historySaved = false; this.previewSaved = this.previewPending = false; this.previewAttempts = 0;
this.network = new NetworkStatistics(); let edge = 1920;
try { const screen = display.getDefaultDisplaySync(); edge = Math.max(screen.width, screen.height); } catch {}
this.video = new VideoSettings(this.state.videoPreference, edge); this.syncVideo();
this.store.put('lastRemote', this.state.remote); this.state.password = '';
this.ensureSignal(); this.joinIfReady();
}
private joinSent: boolean = false;
private joinIfReady(): void {
if (!this.state.connecting || this.joinSent || this.state.signal !== 1 || !this.state.identity) return;
this.joinSent = true; this.send({ op: 'connect', connection: this.connection, remote: this.connectingId, password: this.pendingPassword });
}
end(reason: string): void {
this.release(); this.send({ op: 'disconnect' }); this.platform.disconnect();
this.connection++; this.uiEpoch++; this.pendingPassword = ''; this.joinSent = false; this.sendingFile = false;
this.state.connecting = this.state.ready = false; this.state.page = 'home'; this.state.modal = ''; this.state.password = '';
this.state.receivedPath = this.state.receivedName = this.state.remoteClipboard = this.state.clipboardText = ''; this.remoteVersionRequest = 0;
if (this.settingsTimeout >= 0) clearTimeout(this.settingsTimeout); this.settingsTimeout = -1;
this.history(); if (reason) { this.state.notice = reason; this.state.modal = 'notice'; }
if (this.foreground && this.state.consent) this.checkLocal();
}
release(): void { this.releaseInput(); this.send({ op: 'release' }); }
pointer(x: number, y: number, flag: number, scroll: number): void { this.send({ op: 'pointer', x: x, y: y, flag: flag, scroll: scroll }); }
physicalKey(code: number, down: boolean): void { this.send({ op: 'key', code: code, down: down }); }
key(code: number, down: boolean): void { this.send({ op: 'virtualKey', code: code, down: down }); }
chord(code: number, modifiers: number[] = []): void {
modifiers.forEach((key: number) => this.key(key, true)); this.key(code, true); this.key(code, false);
modifiers.slice().reverse().forEach((key: number) => this.key(key, false));
}
shortcut(code: number): void { this.chord(code, [this.state.platform === 'macos' ? 91 : 17]); }
text(text: string): void {
if (text.length > 2048 || Array.from(text).some((c: string) => asciiKey(c) === 0)) { this.toast('中文等文本请通过剪贴板发送,再点击粘贴'); return; }
for (const c of text) { const code = asciiKey(c); this.chord(code & 255, code & 256 ? [16] : []); }
}
setRelative(value: boolean): void { this.release(); this.state.relative = value; this.store.put('relative', value); }
mute(): void { this.state.muted = !this.state.muted; this.platform.setMuted(this.state.muted); this.send({ op: 'mute', muted: this.state.muted }); }
selectDisplay(index: number): void {
if (index < 0 || index >= this.state.displays.length) return;
this.release(); this.state.selectedDisplay = index; this.state.viewportReset++; this.state.hasFrame = false; this.network.reset();
this.send({ op: 'display', index: index }); this.state.modal = '';
}
updateVideo(field: number, value: number): void {
const old = this.video.selection;
const next: VideoValues = { quality: field === 0 ? value : old.quality, frameRate: field === 1 ? value : old.frameRate, preference: field === 2 ? value : old.preference };
const id = this.video.begin(next); if (id < 0) return;
this.send({ op: 'videoSettings', quality: next.quality, frameRate: next.frameRate, preference: next.preference, request: id });
if (this.settingsTimeout >= 0) clearTimeout(this.settingsTimeout);
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(); }
this.state.modal = '';
}
clearPreviews(): void { this.send({ op: 'cancelPreview' }); this.previewEligible = false; this.store.clearPreviews(); this.history(); this.toast('已清除预览'); }
clipboard(): void {
const text = this.state.clipboardText;
if (new util.TextEncoder().encodeInto(text).length > 128 * 1024) { this.toast('文本超过 128 KB'); return; }
if (text) { this.send({ op: 'clipboard', text: text }); this.toast('已发送,请在远端粘贴'); }
}
async copyRemote(): Promise<void> {
if (!this.state.remoteClipboard) { this.toast('尚未收到远端文本'); return; }
try { await pasteboard.getSystemPasteboard().setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.state.remoteClipboard)); this.toast('已复制远端文本'); }
catch { this.toast('复制失败,请重试'); }
}
sas(): void {
if (!this.serviceAvailable) { this.toast(this.state.platform === 'windows' ? '请在远端启用 CrossDesk 系统服务后重试' : 'Ctrl+Alt+Del 需要远端 Windows 系统服务支持'); return; }
this.send({ op: 'sas' });
}
async chooseFile(): Promise<void> {
if (this.sendingFile) { this.toast('请等待当前文件发送完成'); return; }
const connection = this.connection; const file = await this.platform.choose(this.store.directory);
if (connection !== this.connection || !this.state.ready || !file) { if (file) this.store.discardOutgoing(file.path); return; }
this.sendingFile = true; this.state.transfer = `发送 ${file.name} · 0%`;
this.send({ op: 'sendFile', path: file.path, name: file.name });
}
saveFile(): void { if (this.state.receivedPath) this.platform.save(this.state.receivedPath, this.state.receivedName); }
surface(id: string, controller?: XComponentController): void {
this.send({ op: 'surface', id: id }); if (controller) this.platform.surface(controller, this.state.width, this.state.height);
}
async checkLocal(manual: boolean = false): Promise<void> {
if (!this.state.consent || !this.foreground || this.checkingLocal) return;
this.checkingLocal = true; const request = ++this.versionRequest; this.localVersionRequest = request;
const epoch = this.uiEpoch; if (manual) this.state.updateStatus = '正在检查…';
const body = await this.platform.version();
if (request !== this.localVersionRequest || epoch !== this.uiEpoch || !this.foreground || !this.state.consent) return;
this.checkingLocal = false;
if (body) this.send({ op: 'version', body: body, local: true, version: this.state.version, request: request });
else if (manual) this.state.updateStatus = '检查失败,请稍后重试';
}
private async checkRemote(version: string): Promise<void> {
if (this.checkedRemote || !this.foreground || !this.state.consent || !this.state.ready) return;
this.checkedRemote = true;
if (!version || !/^\d+\.\d+\.\d+/.test(version)) { this.state.notice = '被控端版本过低,请升级到最新版本。'; this.state.modal = 'upgrade'; return; }
const request = ++this.versionRequest, connection = this.connection; this.remoteVersionRequest = request;
const body = await this.platform.version();
if (body && request === this.remoteVersionRequest && connection === this.connection && this.foreground && this.state.consent)
this.send({ op: 'version', body: body, version: version, request: request });
}
document(name: string, title: string, back: string = 'about'): void {
try {
this.state.documentText = name === 'PRIVACY.md' ? this.privacy : util.TextDecoder.create('utf-8').decodeWithStream(this.context.resourceManager.getRawFileContentSync(name));
this.state.documentTitle = title; this.state.documentBack = back; this.state.page = 'document'; this.state.modal = '';
} catch { this.toast('无法读取文档'); }
}
private syncVideo(): void {
this.state.videoQuality = this.video.selection.quality; this.state.videoRate = this.video.selection.frameRate;
this.state.videoSelectedPreference = this.video.selection.preference; this.state.videoSupported = this.video.supported;
this.state.videoFeedback = this.video.feedback();
}
private poll(): void {
try { const events = JSON.parse(bridge.mobilePoll()) as NativeEvent[]; events.forEach((event: NativeEvent) => this.event(event)); }
catch { this.state.signalText = '原生通信库不可用,请重新构建并安装'; }
if (++this.seconds % 20 === 0) {
this.state.networkTime = this.now(); this.presence.tick(this.state.networkTime); this.history();
}
}
private event(e: NativeEvent): void {
if (e.type !== 'version' && (e.session !== this.session || (e.connection !== undefined && e.connection !== this.connection))) {
if (e.type === 'preview' && e.path) this.store.discardRaw(e.path); return;
}
if (e.type === 'identity' && e.identity) {
this.state.identity = e.identity.split('@')[0]; const scope = this.scope();
if (e.identity.includes('@')) this.store.saveSecret(`identity:${scope}`, e.identity).catch(() => this.toast('设备身份保存失败,请检查安全存储'));
this.joinIfReady(); this.configureInbox();
} else if (e.type === 'signal' && !e.controller) {
if (e.generation !== this.signalGeneration) { this.signalGeneration = e.generation ?? -1; this.presence.connect(false, this.now()); this.inbox.connect(false); }
this.state.signal = e.status ?? -1; this.state.identity = e.id || this.state.identity;
const labels = ['正在连接…', '已连接', '连接失败', '服务器已断开', '正在重连…', '服务器已断开', '服务器证书校验失败'];
this.state.signalText = labels[this.state.signal] ?? '未连接'; this.presence.connect(this.state.signal === 1, this.now()); this.configureInbox(); this.joinIfReady();
} else if (e.type === 'signalMessage' && e.generation === this.signalGeneration && e.message) {
const type = (e.message as SignalType).type;
if (type === 'announcements_changed') this.inbox.refresh();
else if (type === 'announcements') this.inbox.receive(e.message as AnnouncementResponse);
else this.presence.receive(e.message as PresenceMessage, this.now());
} else if (e.type === 'requestFailed') this.inbox.fail(String(e.request ?? ''));
else if (e.type === 'connection') {
if (e.status === 1 && !this.state.ready) this.connected();
else if (e.status === 6) {
this.end(''); this.state.remote = this.connectingId; this.state.notice = '访问密码错误,请重新输入'; this.state.modal = 'password';
} else if ((e.status ?? 0) >= 3) {
const reasons = ['', '', '', '连接已断开', '连接失败', '远端已关闭连接', '访问密码错误', '找不到该设备 ID', '远端当前不可用'];
this.end(reasons[e.status ?? 0] ?? '连接已结束');
}
} else if (e.type === 'ended') this.end(e.reason ?? '');
else if (e.type === 'data' && e.message) this.data(e.message as ControlData);
else if (e.type === 'frame') {
this.state.hasFrame = true;
this.state.width = e.width ?? 16; this.state.height = e.height ?? 9;
this.network.frame(e.frame ?? 0, e.latency ?? -1, this.state.width, this.state.height, this.now(), e.latencyCount ?? -1);
this.platform.videoSize(this.state.width, this.state.height);
if (this.previewEligible && this.historySaved && !this.previewSaved && !this.previewPending && this.previewAttempts < 3) {
this.previewAttempts++; this.previewPending = true; this.send({ op: 'preview' });
}
} else if (e.type === 'network' && e.report) this.network.receive(e.report, this.now());
else if (e.type === 'clipboard') this.state.remoteClipboard = e.text ?? '';
else if (e.type === 'feedback') this.toast(e.text ?? '');
else if (e.type === 'file') {
if (e.sending && ((e.progress ?? 0) < 0 || (e.progress ?? 0) >= 1)) this.sendingFile = false;
this.state.transfer = (e.progress ?? 0) < 0 ? `${e.name} 传输失败` : `${e.sending ? '发送' : '接收'} ${e.name} · ${(e.progress ?? 0) >= 1 ? '已完成' : Math.floor((e.progress ?? 0) * 100) + '%'}`;
if (e.path) { this.state.receivedPath = e.path; this.state.receivedName = e.name ?? 'received-file'; }
} else if (e.type === 'settingsFailed') { this.video.expire(Number(e.request)); this.syncVideo(); }
else if (e.type === 'preview') {
this.previewPending = false;
if (e.ok && e.path && this.previewEligible && this.state.previews) {
this.previewSaved = true; this.store.savePreview(this.scope(), this.connectingId, e.path).then(() => this.history()).catch(() => this.previewSaved = false);
} else if (e.path) this.store.discardRaw(e.path);
} else if (e.type === 'version') {
if (e.request === this.localVersionRequest && this.foreground && this.state.consent) {
if (e.ok) { this.state.update = e.latest ?? ''; this.state.updateStatus = this.state.update ? `发现新版本 ${this.state.update}` : '已是最新版本'; }
else this.state.updateStatus = '暂无鸿蒙版本信息,请稍后重试';
} else if (e.request === this.remoteVersionRequest && e.ok && e.latest && this.foreground && this.state.ready) {
this.state.notice = '被控端版本过低,请升级到最新版本。'; this.state.modal = 'upgrade';
}
} else if (e.type === 'error') {
this.state.signalText = e.text ?? '连接失败'; if (this.state.connecting) { this.end(e.text ?? '连接失败'); this.stopSignal(); }
else if (!this.state.ready) { const text = e.text ?? '操作失败'; this.stopSignal(); this.state.signalText = text; } else this.toast(e.text ?? '操作失败');
}
}
private configureInbox(): void {
if (this.state.identity) this.inbox.configure(`${this.scope()}:${this.state.identity}`);
this.inbox.connect(this.state.signal === 1 && this.foreground && this.state.consent);
}
private connected(): void {
this.state.ready = true; this.state.connecting = false; this.state.modal = ''; this.state.page = 'session';
const device: RecentDevice = { id: this.connectingId, name: this.state.hostName || this.connectingId,
platform: this.state.platform, remember: this.state.remember, preview: '' };
const password = this.pendingPassword; this.pendingPassword = '';
const scope = this.scope(), connection = this.connection;
this.store.saveRecent(scope, device, password).then(() => {
if (connection === this.connection && scope === this.scope()) {
this.historySaved = true;
if (this.state.hostName) this.store.metadata(scope, device.id, this.state.hostName, this.state.platform);
this.history();
}
}).catch(() => this.toast('密码保存失败,本次连接仍可使用'));
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);
}
private data(message: ControlData): void {
if (message.type === 5) { this.serviceAvailable = message.service_status?.available === true; return; }
if (message.type === 12 && message.video_settings) {
const v = message.video_settings;
this.video.receive({ quality: v.quality, frameRate: v.frame_rate, preference: v.preference }, v.request_id, v.accepted === true);
if (!this.video.pending && this.settingsTimeout >= 0) clearTimeout(this.settingsTimeout); this.syncVideo(); return;
}
if (message.host_info) {
const info = message.host_info; this.state.platform = info.platform; this.state.hostName = info.host_name;
this.state.displays = Array.isArray(info.displays) ? info.displays.slice(0, 8) : [];
this.store.metadata(this.scope(), this.connectingId, info.host_name, info.platform);
const supported = this.video.supported; this.video.support(info.supports_video_settings === true); this.syncVideo();
if (this.state.ready && this.video.supported && !supported) this.updateVideo(0, this.video.selection.quality);
this.receivedHostInfo = true; this.remoteVersion = info.app_version; this.checkRemote(info.app_version);
}
}
}