[feat] add system language support across desktop and mobile

This commit is contained in:
dijunkun
2026-10-27 12:35:47 +08:00
parent b4acf0b597
commit b07f457e76
105 changed files with 21711 additions and 1627 deletions
@@ -1,3 +1,4 @@
import { tr } from './Localization';
// Platform-independent controller rules ported from the Android client.
// Keep gesture geometry, acknowledgement ordering and expiry behavior identical.
export function clamp(value: number, low: number, high: number): number {
@@ -96,7 +97,7 @@ export class VideoSettings {
this.pending = false; this.failed = !accepted; this.selection = this.applied;
}
expire(id: number): void { if (this.pending && id === this.requestId) { this.pending = false; this.failed = true; this.selection = this.applied; } }
feedback(): string { return !this.supported ? '连接建立后可调整,需被控端支持。' : this.failed ? '调整失败,请重试。' : this.pending ? '等待电脑端确认…' : ''; }
feedback(): string { return !this.supported ? tr("连接建立后可调整,需被控端支持。") : this.failed ? tr("调整失败,请重试。") : this.pending ? tr("等待电脑端确认…") : ''; }
}
export interface Traffic { inbound: number; outbound: number; loss: number; }
export interface NetworkReport { rtt: number; mode: number; srtp: boolean; decoder?: string; video: Traffic; audio: Traffic; data: Traffic; total: Traffic; }
@@ -133,7 +134,7 @@ export class NetworkStatistics {
return count ? valid.reduce((n: number, f: FrameSample) => n + f.latency * f.latencyCount, 0) / count : -1;
}
current(now: number): NetworkReport | undefined { return now - this.reportTime < 3000 ? this.report : undefined; }
mode(now: number): string { const report = this.current(now); return !report ? '—' : report.mode === 0 ? 'P2P 直连' : report.mode === 1 ? 'TURN 中继' : '—'; }
mode(now: number): string { const report = this.current(now); return !report ? '—' : report.mode === 0 ? tr("P2P 直连") : report.mode === 1 ? tr("TURN 中继") : '—'; }
static rate(n: number): string { return !Number.isFinite(n) || n < 0 ? '—' : n >= 1000000 ? `${(n / 1000000).toFixed(1)} Mbps` : n >= 1000 ? `${(n / 1000).toFixed(0)} Kbps` : `${n} bps`; }
static loss(n: number): string { return Number.isFinite(n) && n >= 0 ? `${(n * 100).toFixed(1)}%` : '—'; }
static delay(n: number): string { return !Number.isFinite(n) || n < 0 ? '—' : n < 1 ? '<1 ms' : `${n.toFixed(0)} ms`; }
@@ -1,3 +1,5 @@
import { i18n } from '@kit.LocalizationKit';
import { setLanguageProvider } from './Localization';
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
@@ -5,6 +7,7 @@ import { MobileLifecycle } from './MobilePlatform';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(stage: window.WindowStage): void {
setLanguageProvider(() => i18n.System.getSystemLanguage());
stage.loadContent('pages/Mobile', (error) => {
if (error.code) hilog.error(0, 'CrossDesk', 'Unable to load the main page: %{public}d', error.code);
});
@@ -0,0 +1,37 @@
import { translations } from './LocalizationData';
let languageProvider: (() => string) | undefined = undefined;
export function setLanguageProvider(provider: () => string): void {
languageProvider = provider;
}
export function languageIndex(tag: string): number {
const value = tag.toLowerCase().replace(/_/g, '-').split('.')[0].split('@')[0];
const base = value.split('-')[0];
if (base === 'zh') {
const parts = '-' + value + '-';
if (parts.includes('-hans-')) return 0;
return ['-hant-', '-tw-', '-hk-', '-mo-'].some((part: string) => parts.includes(part)) ? 3 : 0;
}
const codes = ['zh', 'en', 'ru', 'zh-hant', 'ja', 'ko', 'de', 'fr', 'es', 'pt'];
const index = codes.indexOf(base);
return index < 0 ? 1 : index;
}
export function currentLanguage(): number {
try { return languageProvider ? languageIndex(languageProvider()) : 1; }
catch { return 1; }
}
export function tr(key: string, args: string[] = []): string {
// The pure models can also run before an ability installs its locale provider.
const values = translations[key];
let text = languageProvider && values ? values[currentLanguage()] || values[1] : key;
// A single replacement pass avoids interpreting placeholder-like user data.
text = text.replace(/\{(\d+)\}/g, (match: string, index: string) => {
const value = args[Number(index)];
return value === undefined ? match : value;
});
return text;
}
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import { tr } from './Localization';
import { RemotePrivacyScreen, PrivacyStatus } from './RemotePrivacyScreen';
import bridge from 'libcrossdesk.so';
import { common, bundleManager } from '@kit.AbilityKit';
@@ -44,7 +45,7 @@ export class MobileState {
password: string = '';
remember: boolean = false;
signal: number = -1;
signalText: string = '未连接';
signalText: string = tr("未连接");
identity: string = '';
connecting: boolean = false;
ready: boolean = false;
@@ -76,7 +77,7 @@ export class MobileState {
videoSupported: boolean = false;
videoFeedback: string = '';
privacyStatus: string = '';
privacyButtonTitle: string = '隐私屏';
privacyButtonTitle: string = tr("隐私屏");
privacyFeedback: string = '';
privacyEnabled: boolean = false;
privacyActive: boolean = false;
@@ -160,9 +161,9 @@ export class MobileController {
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; }
if (!this.state.privacyAvailable) { this.state.notice = tr("隐私政策无法读取,请重新安装应用"); return; }
try { this.store.put('consent', true); this.state.consent = true; this.state.modal = ''; await this.ensureSignal(); this.checkLocal(); }
catch { this.toast('无法保存联网授权,请重试'); }
catch { this.toast(tr("无法保存联网授权,请重试")); }
}
revoke(): void {
this.end(''); this.stopSignal(); this.state.consent = false; this.state.previews = false;
@@ -188,16 +189,16 @@ export class MobileController {
// 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.state.signalText = tr("正在连接…"); 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; }
} catch { this.state.signalText = tr("安全存储不可用,请解锁设备后重试"); 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.send({ op: 'stop' }); this.state.signal = -1; this.state.identity = ''; this.state.signalText = tr("未连接");
this.presence.connect(false, this.now()); this.inbox.connect(false); this.platform.cancelRequests();
this.localVersionRequest = this.remoteVersionRequest = 0; this.checkingLocal = false;
}
@@ -205,7 +206,7 @@ export class MobileController {
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;
this.toast(tr("请输入有效的服务器地址和端口")); 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;
@@ -219,11 +220,11 @@ export class MobileController {
.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('记录或密码删除失败,请重试'); }
try { await this.store.delete(this.scope(), id); this.history(); } catch { this.toast(tr("记录或密码删除失败,请重试")); }
}
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 (!/^[0-9]{9}$/.test(id)) { this.toast(tr("请输入 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 = '';
@@ -232,11 +233,11 @@ export class MobileController {
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 = '已保存密码无法读取,请重新输入'; }
} catch { if (epoch === this.uiEpoch) this.state.notice = tr("已保存密码无法读取,请重新输入"); }
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; }
if (!this.state.consent || !/^[0-9]{9}$/.test(this.state.remote) || !password || password.length > 128 || password.includes('@')) { this.toast(tr("请输入有效的访问密码")); 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 = '';
@@ -276,7 +277,7 @@ export class MobileController {
}
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; }
if (text.length > 2048 || Array.from(text).some((c: string) => asciiKey(c) === 0)) { this.toast(tr("中文等文本请通过剪贴板发送,再点击粘贴")); 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); }
@@ -301,27 +302,27 @@ export class MobileController {
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('已清除预览'); }
clearPreviews(): void { this.send({ op: 'cancelPreview' }); this.previewEligible = false; this.store.clearPreviews(); this.history(); this.toast(tr("已清除预览")); }
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('已发送,请在远端粘贴'); }
if (new util.TextEncoder().encodeInto(text).length > 128 * 1024) { this.toast(tr("文本超过 128 KB")); return; }
if (text) { this.send({ op: 'clipboard', text: text }); this.toast(tr("已发送,请在远端粘贴")); }
}
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('复制失败,请重试'); }
if (!this.state.remoteClipboard) { this.toast(tr("尚未收到远端文本")); return; }
try { await pasteboard.getSystemPasteboard().setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.state.remoteClipboard)); this.toast(tr("已复制远端文本")); }
catch { this.toast(tr("复制失败,请重试")); }
}
sas(): void {
if (this.state.platform !== 'windows') return;
if (!this.serviceAvailable) { this.toast('请在远端启用 CrossDesk 系统服务后重试'); return; }
if (!this.serviceAvailable) { this.toast(tr("请在远端启用 CrossDesk 系统服务后重试")); return; }
this.send({ op: 'sas' });
}
async chooseFile(): Promise<void> {
if (this.sendingFile) { this.toast('请等待当前文件发送完成'); return; }
if (this.sendingFile) { this.toast(tr("请等待当前文件发送完成")); 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.sendingFile = true; this.state.transfer = tr("发送 {0} · 0%", [String(file.name)]);
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); }
@@ -331,17 +332,17 @@ export class MobileController {
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 epoch = this.uiEpoch; if (manual) this.state.updateStatus = tr("正在检查…");
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 = '检查失败,请稍后重试';
else if (manual) this.state.updateStatus = tr("检查失败,请稍后重试");
}
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; }
if (!version || !/^\d+\.\d+\.\d+/.test(version)) { this.state.notice = tr("被控端版本过低,请升级到最新版本。"); 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)
@@ -351,7 +352,7 @@ export class MobileController {
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('无法读取文档'); }
} catch { this.toast(tr("无法读取文档")); }
}
dismissPrivacyUnlock(): void {
this.privacyScreen.unlockNotice = false;
@@ -389,7 +390,7 @@ export class MobileController {
}
private poll(): void {
try { const events = JSON.parse(bridge.mobilePoll()) as NativeEvent[]; events.forEach((event: NativeEvent) => this.event(event)); }
catch { this.state.signalText = '原生通信库不可用,请重新构建并安装'; }
catch { this.state.signalText = tr("原生通信库不可用,请重新构建并安装"); }
if (++this.seconds % 20 === 0) {
if (this.state.ready) this.syncPrivacy();
this.state.networkTime = this.now(); this.presence.tick(this.state.networkTime); this.history();
@@ -401,13 +402,13 @@ export class MobileController {
}
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('设备身份保存失败,请检查安全存储'));
if (e.identity.includes('@')) this.store.saveSecret(`identity:${scope}`, e.identity).catch(() => this.toast(tr("设备身份保存失败,请检查安全存储")));
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();
const labels = [tr("正在连接…"), tr("已连接"), tr("连接失败"), tr("服务器已断开"), tr("正在重连…"), tr("服务器已断开"), tr("服务器证书校验失败")];
this.state.signalText = labels[this.state.signal] ?? tr("未连接"); 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();
@@ -417,10 +418,10 @@ export class MobileController {
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';
this.end(''); this.state.remote = this.connectingId; this.state.notice = tr("访问密码错误,请重新输入"); this.state.modal = 'password';
} else if ((e.status ?? 0) >= 3) {
const reasons = ['', '', '', '连接已断开', '连接失败', '远端已关闭连接', '访问密码错误', '找不到该设备 ID', '远端当前不可用'];
this.end(reasons[e.status ?? 0] ?? '连接已结束');
const reasons = ['', '', '', tr("连接已断开"), tr("连接失败"), tr("远端已关闭连接"), tr("访问密码错误"), tr("找不到该设备 ID"), tr("远端当前不可用")];
this.end(reasons[e.status ?? 0] ?? tr("连接已结束"));
}
} else if (e.type === 'ended') this.end(e.reason ?? '');
else if (e.type === 'data' && e.message) this.data(e.message as ControlData);
@@ -437,7 +438,7 @@ export class MobileController {
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) + '%'}`;
this.state.transfer = (e.progress ?? 0) < 0 ? tr("{0} 传输失败", [String(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 === 'privacyFailed') { this.privacyScreen.failed(Number(e.request)); this.syncPrivacy(); }
else if (e.type === 'settingsFailed') { this.video.expire(Number(e.request)); this.syncVideo(); }
@@ -448,14 +449,14 @@ export class MobileController {
} 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 = '暂无鸿蒙版本信息,请稍后重试';
if (e.ok) { this.state.update = e.latest ?? ''; this.state.updateStatus = this.state.update ? tr("发现新版本 {0}", [String(this.state.update)]) : tr("已是最新版本"); }
else this.state.updateStatus = tr("暂无鸿蒙版本信息,请稍后重试");
} else if (e.request === this.remoteVersionRequest && e.ok && e.latest && this.foreground && this.state.ready) {
this.state.notice = '被控端版本过低,请升级到最新版本。'; this.state.modal = 'upgrade';
this.state.notice = tr("被控端版本过低,请升级到最新版本。"); 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 ?? '操作失败');
this.state.signalText = e.text ?? tr("连接失败"); if (this.state.connecting) { this.end(e.text ?? tr("连接失败")); this.stopSignal(); }
else if (!this.state.ready) { const text = e.text ?? tr("操作失败"); this.stopSignal(); this.state.signalText = text; } else this.toast(e.text ?? tr("操作失败"));
}
}
private configureInbox(): void {
@@ -474,7 +475,7 @@ export class MobileController {
if (this.state.hostName) this.store.metadata(scope, device.id, this.state.hostName, this.state.platform);
this.history();
}
}).catch(() => this.toast('密码保存失败,本次连接仍可使用'));
}).catch(() => this.toast(tr("密码保存失败,本次连接仍可使用")));
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);
@@ -1,3 +1,4 @@
import { tr } from './Localization';
import { deviceInfo } from '@kit.BasicServicesKit';
import { common, wantAgent } from '@kit.AbilityKit';
import { PiPWindow, window, display } from '@kit.ArkUI';
@@ -34,7 +35,7 @@ export class MobilePlatform {
private feedback: (message: string) => void;
private backgroundCancelled: () => void = () => {
this.backgroundActive = false;
if (this.active && !this.foreground && !this.pipActive && !this.choosing) this.ended('系统已停止后台会话');
if (this.active && !this.foreground && !this.pipActive && !this.choosing) this.ended(tr("系统已停止后台会话"));
};
constructor(context: common.UIAbilityContext, ended: (reason: string) => void, release: () => void, feedback: (message: string) => void) {
this.context = context; this.ended = ended; this.release = release; this.feedback = feedback;
@@ -82,7 +83,7 @@ export class MobilePlatform {
if (state === PiPWindow.PiPState.STOPPED) {
this.pipActive = false;
this.onVisibility(this.foreground);
if (!this.foreground && !this.choosing && this.active) this.ended('画中画已关闭');
if (!this.foreground && !this.choosing && this.active) this.ended(tr("画中画已关闭"));
}
});
} catch { /* A continuous data-transfer task remains the fallback. */ }
@@ -104,7 +105,7 @@ export class MobilePlatform {
}
await this.backgroundQueue;
if (generation === this.generation && !this.foreground && this.active && !this.backgroundActive && !this.pipActive && !this.choosing)
this.ended('系统未允许后台会话,连接已断开');
this.ended(tr("系统未允许后台会话,连接已断开"));
}
resume(): void { this.foreground = true; this.dismissPiP(); this.orientation(this.active); this.onVisibility(true); }
private async dismissPiP(): Promise<void> {
@@ -164,7 +165,7 @@ export class MobilePlatform {
try { await fs.copyFile(input.fd, path); } finally { await fs.close(input); }
if (generation !== this.generation || !this.active) { await fs.unlink(path); return undefined; }
return { path: path, name: name };
} catch { if (path) try { await fs.unlink(path); } catch {} this.feedback('无法读取所选文件或存储空间不足'); return undefined; }
} catch { if (path) try { await fs.unlink(path); } catch {} this.feedback(tr("无法读取所选文件或存储空间不足")); return undefined; }
finally { if (generation === this.generation) this.setChoosing(false); }
}
async save(path: string, name: string): Promise<void> {
@@ -177,8 +178,8 @@ export class MobilePlatform {
if (!uris.length || generation !== this.generation || !this.active) return;
const output = await fs.open(uris[0], fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC);
try { await fs.copyFile(path, output.fd); } finally { await fs.close(output); }
if (generation === this.generation) this.feedback('文件已保存');
} catch { if (generation === this.generation) this.feedback('文件保存失败,请重试'); }
if (generation === this.generation) this.feedback(tr("文件已保存"));
} catch { if (generation === this.generation) this.feedback(tr("文件保存失败,请重试")); }
finally { if (generation === this.generation) this.setChoosing(false); }
}
async version(): Promise<string | undefined> {
@@ -196,7 +197,7 @@ export class MobilePlatform {
cancelRequests(): void { for (const request of this.requests) request.destroy(); this.requests = []; }
openLink(url: string): void {
if (!isAnnouncementURL(url)) return;
this.context.startAbility({ action: 'ohos.want.action.viewData', uri: url }).catch(() => this.feedback('无法打开网页'));
this.context.startAbility({ action: 'ohos.want.action.viewData', uri: url }).catch(() => this.feedback(tr("无法打开网页")));
}
}
export interface PreparedFile { path: string; name: string; }
@@ -1,3 +1,4 @@
import { tr } from './Localization';
import { MobileController, MobileState } from './MobileController';
import { RemoteViewport, MouseInput, TouchInput, TouchPoint, clamp, NetworkStatistics, Traffic, asciiKey } from './ControllerModels';
import { MobileTheme, MobileSegment, MobileIcon, MobileIconButton } from './MobileWidgets';
@@ -136,7 +137,7 @@ export struct MobileRemoteView {
if (index === 10) this.controller?.togglePrivacy();
}
private labels(): string[] {
return ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板', this.state.privacyButtonTitle];
return [tr("键盘"), this.state.relative ? tr("相对鼠标") : tr("绝对鼠标"), this.mouseVisible ? tr("收起鼠标") : tr("虚拟鼠标"), tr("显示器"), tr("画面设置"), this.state.muted ? tr("静音") : tr("声音"), tr("发送文件"), tr("网络状态"), 'Ctrl+Alt+Del', tr("剪贴板"), this.state.privacyButtonTitle];
}
private controlIds(): number[] {
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter((index: number) =>
@@ -155,22 +156,33 @@ export struct MobileRemoteView {
return { x: clamp(x, 8, this.viewWidth - 148), y: clamp(y, 8, this.viewHeight - 56) };
}
private keyboardWidth(): number { return Math.max(1, Math.min(this.computerKeyboard ? 520 : 440, this.viewWidth - 16)); }
private keyboardTitle(key: string): string {
switch (key) {
case 'copy': return tr("复制");
case 'paste': return tr("粘贴");
case 'select_all': return tr("全选");
case 'space': return tr("空格");
case 'computer': return tr("电脑");
case 'clipboard': return tr("剪贴板");
default: return this.shift && key.length === 1 ? key.toUpperCase() : key;
}
}
private keyboardRows(): KeyRow[] {
return this.computerKeyboard ? [
{ keys: ['Esc', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6'] }, { keys: ['Tab', 'Ctrl', 'Alt', 'Win', '↑', '⌫'] },
{ keys: ['复制', '粘贴', '全选', '←', '↓', '→'] }, { keys: ['ABC', '空格', 'Enter'] }
{ keys: ['copy', 'paste', 'select_all', '←', '↓', '→'] }, { keys: ['ABC', 'space', 'Enter'] }
] : [{ keys: ['1','2','3','4','5','6','7','8','9','0'] }, { keys: ['q','w','e','r','t','y','u','i','o','p'] },
{ keys: ['a','s','d','f','g','h','j','k','l'] }, { keys: ['⇧','z','x','c','v','b','n','m','⌫'] },
{ keys: ['电脑','剪贴板','空格','.','Enter'] }];
{ keys: ['computer','clipboard','space','.','Enter'] }];
}
private keyboardKey(value: string): void {
if (value === '⇧') { this.shift = !this.shift; return; }
if (value === '电脑' || value === 'ABC') { this.computerKeyboard = value === '电脑'; return; }
if (value === '剪贴板') { this.state.modal = 'clipboard'; return; }
if (value === 'computer' || value === 'ABC') { this.computerKeyboard = value === 'computer'; return; }
if (value === 'clipboard') { this.state.modal = 'clipboard'; return; }
const modifier = value === 'Ctrl' ? 17 : value === 'Alt' ? 18 : value === 'Win' ? 91 : 0;
if (modifier) { this.modifiers = this.modifiers.includes(modifier) ? this.modifiers.filter((m: number) => m !== modifier) : [...this.modifiers, modifier]; return; }
if (value === '复制' || value === '粘贴' || value === '全选') { this.controller?.shortcut(value === '复制' ? 67 : value === '粘贴' ? 86 : 65); return; }
const names = ['空格','⌫','Enter','Esc','Tab','↑','↓','←','→']; const codes = [32,8,13,27,9,38,40,37,39];
if (value === 'copy' || value === 'paste' || value === 'select_all') { this.controller?.shortcut(value === 'copy' ? 67 : value === 'paste' ? 86 : 65); return; }
const names = ['space','⌫','Enter','Esc','Tab','↑','↓','←','→']; const codes = [32,8,13,27,9,38,40,37,39];
let code = names.includes(value) ? codes[names.indexOf(value)] : /^F[1-6]$/.test(value) ? 111 + Number(value.substring(1)) : 0;
if (!code && this.modifiers.length) { const key = asciiKey(this.shift ? value.toUpperCase() : value); code = key & 255; if (key & 256) this.modifiers = [...this.modifiers, 16]; }
if (code) { this.controller?.chord(code, this.modifiers); this.modifiers = []; }
@@ -183,11 +195,11 @@ export struct MobileRemoteView {
@Builder
NetworkRow(title: string, traffic: Traffic | undefined) {
Row({ space: 4 }) {
Text(title).fontSize(11).fontWeight(title === '合计' ? 600 : 400).fontColor(MobileTheme.ink).width(28)
Text(title).fontSize(11).minFontSize(8).maxFontSize(11).maxLines(1).fontWeight(title === tr("合计") ? 600 : 400).fontColor(MobileTheme.ink).width(48)
Text(NetworkStatistics.rate(traffic?.inbound ?? -1)).fontSize(11).minFontSize(8).maxLines(1).layoutWeight(1).textAlign(TextAlign.End)
Text(NetworkStatistics.rate(traffic?.outbound ?? -1)).fontSize(11).minFontSize(8).maxLines(1).layoutWeight(1).textAlign(TextAlign.End)
Text(NetworkStatistics.loss(traffic?.loss ?? -1)).fontSize(11).width(46).textAlign(TextAlign.End)
}.padding({ left: 4, right: 4 }).height(26).width('100%').borderRadius(5).backgroundColor(title === '合计' ? '#122E7ADB' : Color.Transparent)
}.padding({ left: 4, right: 4 }).height(26).width('100%').borderRadius(5).backgroundColor(title === tr("合计") ? '#122E7ADB' : Color.Transparent)
}
@Builder
NetworkDetail(label: string, value: string, encrypted: boolean = false) {
@@ -210,12 +222,12 @@ export struct MobileRemoteView {
Menu() {
Column({ space: 10 }) {
Row({ space: 6 }) {
if (this.menu !== 'controls') { MobileIconButton({ icon: 'back', label: '返回控制栏', iconExtent: 30, iconSize: 19, action: () => this.menu = 'controls' }) }
if (this.menu !== 'controls') { MobileIconButton({ icon: 'back', label: tr("返回控制栏"), iconExtent: 30, iconSize: 19, action: () => this.menu = 'controls' }) }
Row({ space: 6 }) {
if (this.menu === 'controls') { Row() {}.width(6).height(6).borderRadius(3).backgroundColor(MobileTheme.green) }
Text(this.menu === 'controls' ? '已连接' : this.menu === 'video' ? '画面设置' : '网络状态').fontSize(14).fontWeight(600).fontColor(MobileTheme.ink)
Text(this.menu === 'controls' ? tr("已连接") : this.menu === 'video' ? tr("画面设置") : tr("网络状态")).fontSize(14).fontWeight(600).fontColor(MobileTheme.ink)
}.layoutWeight(1)
MobileIconButton({ icon: 'close', label: '关闭控制栏', iconExtent: 30, iconSize: 20, action: () => this.menu = '' })
MobileIconButton({ icon: 'close', label: tr("关闭控制栏"), iconExtent: 30, iconSize: 20, action: () => this.menu = '' })
}.width('100%')
Scroll() {
Column({ space: 8 }) {
@@ -225,7 +237,7 @@ export struct MobileRemoteView {
GridCol() {
Column({ space: 4 }) {
MobileIcon({ name: this.icons()[index], iconExtent: 19 })
Text(this.labels()[index]).fontSize(10).fontColor(MobileTheme.ink).maxLines(1)
Text(this.labels()[index]).fontSize(10).minFontSize(8).maxFontSize(10).fontColor(MobileTheme.ink).maxLines(2).textAlign(TextAlign.Center)
}
.height(52).width('100%').justifyContent(FlexAlign.Center)
.backgroundColor('#F0F0F2').borderRadius(10)
@@ -238,44 +250,44 @@ export struct MobileRemoteView {
}
if (this.state.privacyFeedback) { Text(this.state.privacyFeedback).fontSize(11).fontColor(MobileTheme.secondary).width('100%') }
if (this.state.transfer) { Text(this.state.transfer).fontSize(11).fontColor(MobileTheme.secondary).width('100%') }
if (this.state.receivedPath) { Button('保存收到的文件', { type: ButtonType.Normal }).fontSize(14).fontColor(MobileTheme.blue).backgroundColor(Color.White).height(42).width('100%').onClick(() => this.controller?.saveFile()) }
if (this.state.receivedPath) { Button(tr("保存收到的文件"), { type: ButtonType.Normal }).fontSize(14).fontColor(MobileTheme.blue).backgroundColor(Color.White).height(42).width('100%').onClick(() => this.controller?.saveFile()) }
} else if (this.menu === 'video') {
Text(this.state.videoFeedback).fontSize(12).fontColor(MobileTheme.secondary).width('100%')
Text('画面质量').fontSize(12).width('100%')
MobileSegment({ labels: ['低', '中', '高'], selection: this.state.videoQuality,
Text(tr("画面质量")).fontSize(12).width('100%')
MobileSegment({ labels: [tr("低"), tr("中"), tr("高")], selection: this.state.videoQuality,
available: this.state.videoSupported, changed: (i: number) => this.controller?.updateVideo(0, i) })
Text('画面采集帧率').fontSize(12).width('100%')
Text(tr("画面采集帧率")).fontSize(12).width('100%')
MobileSegment({ labels: ['30 fps', '60 fps'], selection: this.state.videoRate === 60 ? 1 : 0,
available: this.state.videoSupported, changed: (i: number) => this.controller?.updateVideo(1, i ? 60 : 30) })
Text('画面偏好').fontSize(12).width('100%')
MobileSegment({ labels: ['帧率优先', '画质优先', '平衡'], selection: this.state.videoSelectedPreference,
Text(tr("画面偏好")).fontSize(12).width('100%')
MobileSegment({ labels: [tr("帧率优先"), tr("画质优先"), tr("平衡")], selection: this.state.videoSelectedPreference,
available: this.state.videoSupported, changed: (i: number) => this.controller?.updateVideo(2, i) })
} else {
Row({ space: 4 }) {
Text('').width(28)
Text('接收').fontSize(11).fontColor(MobileTheme.secondary).layoutWeight(1).textAlign(TextAlign.End)
Text('发送').fontSize(11).fontColor(MobileTheme.secondary).layoutWeight(1).textAlign(TextAlign.End)
Text('丢包率').fontSize(11).fontColor(MobileTheme.secondary).width(46).textAlign(TextAlign.End)
Text('').width(48)
Text(tr("接收")).fontSize(11).fontColor(MobileTheme.secondary).layoutWeight(1).textAlign(TextAlign.End)
Text(tr("发送")).fontSize(11).fontColor(MobileTheme.secondary).layoutWeight(1).textAlign(TextAlign.End)
Text(tr("丢包率")).fontSize(11).minFontSize(8).maxFontSize(11).maxLines(2).fontColor(MobileTheme.secondary).width(46).textAlign(TextAlign.End)
}.padding({ left: 4, right: 4 }).height(24).width('100%')
Column() {
this.NetworkRow('视频', this.controller?.network.current(this.state.networkTime)?.video)
this.NetworkRow('音频', this.controller?.network.current(this.state.networkTime)?.audio)
this.NetworkRow('数据', this.controller?.network.current(this.state.networkTime)?.data)
this.NetworkRow('合计', this.controller?.network.current(this.state.networkTime)?.total)
this.NetworkRow(tr("视频"), this.controller?.network.current(this.state.networkTime)?.video)
this.NetworkRow(tr("音频"), this.controller?.network.current(this.state.networkTime)?.audio)
this.NetworkRow(tr("数据"), this.controller?.network.current(this.state.networkTime)?.data)
this.NetworkRow(tr("合计"), this.controller?.network.current(this.state.networkTime)?.total)
}.width('100%')
Divider().strokeWidth(0.5).color(MobileTheme.line).margin({ top: 2, bottom: 2 })
this.NetworkDetail('帧率', `${this.controller?.network.fps(this.state.networkTime) ?? '—'} FPS`)
this.NetworkDetail('分辨率', this.state.hasFrame ? `${this.state.width} × ${this.state.height}` : '—')
this.NetworkDetail('画面延时', NetworkStatistics.delay(this.controller?.network.latency(this.state.networkTime) ?? -1))
this.NetworkDetail('连接延时(RTT)', NetworkStatistics.delay(this.state.networkTime - (this.controller?.network.rttTime ?? 0) < 3000 ? this.controller?.network.rtt ?? -1 : -1))
this.NetworkDetail('连接方式', this.controller?.network.mode(this.state.networkTime) ?? '—')
this.NetworkDetail('媒体加密', this.controller?.network.current(this.state.networkTime) === undefined ? '—' : this.controller?.network.report?.srtp ? 'SRTP 已启用' : '未启用', this.controller?.network.current(this.state.networkTime)?.srtp === true)
this.NetworkCodec('被控端编码器', this.controller?.network.remoteEncoder || '—')
this.NetworkCodec('控制端解码器', this.controller?.network.report?.decoder || '—')
this.NetworkDetail(tr("帧率"), `${this.controller?.network.fps(this.state.networkTime) ?? '—'} FPS`)
this.NetworkDetail(tr("分辨率"), this.state.hasFrame ? `${this.state.width} × ${this.state.height}` : '—')
this.NetworkDetail(tr("画面延时"), NetworkStatistics.delay(this.controller?.network.latency(this.state.networkTime) ?? -1))
this.NetworkDetail(tr("连接延时(RTT)"), NetworkStatistics.delay(this.state.networkTime - (this.controller?.network.rttTime ?? 0) < 3000 ? this.controller?.network.rtt ?? -1 : -1))
this.NetworkDetail(tr("连接方式"), this.controller?.network.mode(this.state.networkTime) ?? '—')
this.NetworkDetail(tr("媒体加密"), this.controller?.network.current(this.state.networkTime) === undefined ? '—' : this.controller?.network.report?.srtp ? tr("SRTP 已启用") : tr("未启用"), this.controller?.network.current(this.state.networkTime)?.srtp === true)
this.NetworkCodec(tr("被控端编码器"), this.controller?.network.remoteEncoder || '—')
this.NetworkCodec(tr("控制端解码器"), this.controller?.network.report?.decoder || '—')
}
}.width('100%')
}.constraintSize({ maxHeight: Math.max(48, this.viewHeight - 132) }).width('100%').scrollBar(BarState.Off)
Button('断开连接', { type: ButtonType.Normal }).fontSize(14).fontColor(Color.White).width('100%').height(34).borderRadius(8).backgroundColor(MobileTheme.red)
Button(tr("断开连接"), { type: ButtonType.Normal }).fontSize(14).fontColor(Color.White).width('100%').height(34).borderRadius(8).backgroundColor(MobileTheme.red)
.onClick(() => { this.release(); this.state.modal = 'disconnect'; this.menu = ''; this.keyboard = false; })
}.padding(12).width(this.menuWidth()).backgroundColor(Color.White).borderRadius(18)
.border({ width: 0.5, color: MobileTheme.line }).shadow({ radius: 24, color: '#33000000', offsetY: 6 })
@@ -286,21 +298,21 @@ export struct MobileRemoteView {
Keyboard() {
Column({ space: 3 }) {
Row() {
Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }.layoutWeight(1).height(28).accessibilityText('拖动键盘')
Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }.layoutWeight(1).height(28).accessibilityText(tr("拖动键盘"))
.gesture(PanGesture().onActionStart(() => { this.keyboardOriginX = this.keyboardX; this.keyboardOriginY = this.keyboardY; })
.onActionUpdate((e: GestureEvent) => this.keyboardDrag(e)))
MobileIconButton({ icon: 'close', label: '收起键盘', iconExtent: 28, iconSize: 18, action: () => { this.keyboard = false; this.modifiers = []; } })
MobileIconButton({ icon: 'close', label: tr("收起键盘"), iconExtent: 28, iconSize: 18, action: () => { this.keyboard = false; this.modifiers = []; } })
}.width('100%')
Scroll() {
Column({ space: 3 }) {
ForEach(this.keyboardRows(), (row: KeyRow) => {
Row({ space: 3 }) {
ForEach(row.keys, (key: string) => {
Text(this.shift && key.length === 1 ? key.toUpperCase() : key).fontSize(key.length > 2 ? 11 : 14)
.height(31).layoutWeight(key === '空格' ? 3 : 1).textAlign(TextAlign.Center).borderRadius(5).fontColor(MobileTheme.ink)
Text(this.keyboardTitle(key)).fontSize(key.length > 2 ? 11 : 14).minFontSize(8).maxFontSize(key.length > 2 ? 11 : 14).maxLines(1)
.height(31).layoutWeight(key === 'space' ? 3 : 1).textAlign(TextAlign.Center).borderRadius(5).fontColor(MobileTheme.ink)
.shadow({ radius: 0.5, color: '#33000000', offsetY: 1 })
.backgroundColor((key === '⇧' && this.shift) || this.modifiers.includes(key === 'Ctrl' ? 17 : key === 'Alt' ? 18 : key === 'Win' ? 91 : -1) ? '#9BC7F7' : key.length > 1 || key === '⇧' ? '#AFB8C5' : '#FFFFFF')
.accessibilityText(key).accessibilityLevel('yes').onClick(() => this.keyboardKey(key))
.accessibilityText(this.keyboardTitle(key)).accessibilityLevel('yes').onClick(() => this.keyboardKey(key))
}, (key: string) => key)
}.width('100%')
})
@@ -324,7 +336,7 @@ export struct MobileRemoteView {
} else { MobileIcon({ name: button === 0 ? 'mouse_left' : 'mouse_right', iconExtent: 22 }) }
}.width(44).height(44).borderRadius(7).clip(true)
.backgroundColor(button === 0 && this.leftHeld || button === 2 && this.rightHeld ? '#9BC7F7' : '#E9E9EE')
.accessibilityText(button === 0 ? '鼠标左键,按住拖动' : button === 1 ? '滚轮,上下滑动' : '鼠标右键,按住拖动')
.accessibilityText(button === 0 ? tr("鼠标左键,按住拖动") : button === 1 ? tr("滚轮,上下滑动") : tr("鼠标右键,按住拖动"))
.onTouch((event: TouchEvent) => this.barTouch(event, button))
})
}.padding(2).width(140).height(48).backgroundColor('#FFFFFF').borderRadius(10)
@@ -339,7 +351,7 @@ export struct MobileRemoteView {
if (!this.state.hasFrame) {
Column({ space: 12 }) {
LoadingProgress().width(32).height(32).color(Color.White)
Text('等待远端画面…').fontSize(14).fontColor(Color.White)
Text(tr("等待远端画面…")).fontSize(14).fontColor(Color.White)
}.padding(20).constraintSize({ minWidth: 200 }).backgroundColor('#B0202024').borderRadius(14)
.position({ x: (this.viewWidth - 200) / 2, y: (this.viewHeight - 104) / 2 }).hitTestBehavior(HitTestMode.None)
}
@@ -348,7 +360,7 @@ export struct MobileRemoteView {
.onAxisEvent((event: AxisEvent) => { const delta = event.getVerticalAxisValue(); if (delta) this.mouse.scroll(-delta); })
.onKeyEvent((e: KeyEvent) => { if (e.type === KeyType.Down || e.type === KeyType.Up) { this.controller?.physicalKey(e.keyCode, e.type === KeyType.Down); return true; } return false; })
.onBlur(() => this.controller?.release())
.accessibilityText('远程桌面:轻触点击、长按右键、双指捏合缩放、放大后双指拖动画面,原始比例下双指滚动')
.accessibilityText(tr("远程桌面:轻触点击、长按右键、双指捏合缩放、放大后双指拖动画面,原始比例下双指滚动"))
MobileIcon({ name: 'cursor', iconExtent: 24, color: '#000000' })
.position({ x: this.cursorX - 1, y: this.cursorY - 1 }).hitTestBehavior(HitTestMode.None)
if (this.menu) { Column() {}.width('100%').height('100%').onClick(() => this.menu = ''); this.Menu() }
@@ -357,7 +369,7 @@ export struct MobileRemoteView {
Stack() {
Image($r('app.media.mobile_brand')).width(46).height(46).objectFit(ImageFit.Cover).borderRadius(23).draggable(false).hitTestBehavior(HitTestMode.None)
}.width(54).height(54).backgroundColor('#F5F5F7').borderRadius(27).shadow({ radius: 14, color: '#33000000', offsetY: 4 })
.position({ x: this.orbX, y: this.orbY }).accessibilityText('远程控制菜单,拖动可移动').accessibilityLevel('yes')
.position({ x: this.orbX, y: this.orbY }).accessibilityText(tr("远程控制菜单,拖动可移动")).accessibilityLevel('yes')
.onTouch((e: TouchEvent) => this.orbTouch(e))
}.width('100%').height('100%').backgroundColor(Color.Black).clip(true)
.onAreaChange((_old: Area, next: Area) => {
@@ -1,3 +1,4 @@
import { tr } from './Localization';
import { common } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
import { asset } from '@kit.AssetStoreKit';
@@ -42,7 +43,7 @@ export class MobileStore {
return value instanceof Uint8Array ? util.TextDecoder.create('utf-8').decodeWithStream(value) : '';
} catch (error) {
if ((error as ErrorWithCode).code === asset.ErrorCode.NOT_FOUND) return '';
throw new Error('安全存储不可用,请解锁设备后重试');
throw new Error(tr("安全存储不可用,请解锁设备后重试"));
}
}
async saveSecret(key: string, value: string): Promise<void> {
@@ -56,7 +57,7 @@ export class MobileStore {
}
async removeSecret(key: string): Promise<void> {
try { await asset.remove(await this.query(key)); }
catch (error) { if ((error as ErrorWithCode).code !== asset.ErrorCode.NOT_FOUND) throw new Error('无法删除已保存的凭据'); }
catch (error) { if ((error as ErrorWithCode).code !== asset.ErrorCode.NOT_FOUND) throw new Error(tr("无法删除已保存的凭据")); }
}
recent(scope: string): RecentDevice[] {
try {
@@ -1,3 +1,4 @@
import { tr } from './Localization';
import { LengthMetricsUnit } from '@kit.ArkUI';
export class MobileTheme {
@@ -86,7 +87,7 @@ export struct MobileIconButton {
}
@Component
export struct MobilePrimaryButton {
label: string = '连接';
label: string = tr("连接");
@Prop available: boolean = true;
arrow: boolean = false;
buttonHeight: number = 48;
@@ -94,7 +95,7 @@ export struct MobilePrimaryButton {
build() {
Button({ type: ButtonType.Normal, stateEffect: true }) {
Row({ space: 8 }) {
Text(this.label).fontSize(16).fontWeight(600).fontColor(Color.White)
Text(this.label).fontSize(16).minFontSize(11).maxFontSize(16).maxLines(2).textAlign(TextAlign.Center).fontWeight(600).fontColor(Color.White)
if (this.arrow) { MobileIcon({ name: 'arrow', iconExtent: 18, color: '#FFFFFF' }) }
}.justifyContent(FlexAlign.Center).width('100%')
}.height(this.buttonHeight).width('100%').padding({ left: 12, right: 12 }).borderRadius(12)
@@ -118,7 +119,7 @@ export struct MobileSegment {
.backgroundColor(Color.White).borderRadius(7).shadow({ radius: 2, color: '#18000000', offsetY: 1 }).translate({ x: 2 + this.selected * Math.max(0, this.viewWidth - 4) / Math.max(1, this.labels.length) })
Row() {
ForEach(this.labels, (label: string, index: number) => {
Text(label).fontSize(12).fontWeight(this.selection === index ? 600 : 400).fontColor(MobileTheme.ink)
Text(label).fontSize(12).minFontSize(9).maxFontSize(12).maxLines(2).fontWeight(this.selection === index ? 600 : 400).fontColor(MobileTheme.ink)
.textAlign(TextAlign.Center).height(32).layoutWeight(1).enabled(this.available)
.accessibilityLevel('yes').accessibilityText(label).accessibilitySelected(this.selection === index)
.onClick(() => this.changed(index))
@@ -1,3 +1,4 @@
import { tr } from './Localization';
export interface PrivacyStatus {
state: number; supported: boolean; input_block_supported: boolean;
overlay_active: boolean; input_blocked: boolean; remote_paused: boolean;
@@ -44,10 +45,10 @@ export class RemotePrivacyScreen {
return s !== undefined && this.fresh(now) && s.state === 3 && s.overlay_active && !s.remote_paused;
}
buttonTitle(now: number): string {
if (this.enabled(now)) return '隐私屏已开启';
if (this.enabled(now)) return tr("隐私屏已开启");
const s = this.snapshot;
if (this.fresh(now) && s?.supported && s.state === 1 && !this.active()) return '隐私屏已关闭';
return '隐私屏';
if (this.fresh(now) && s?.supported && s.state === 1 && !this.active()) return tr("隐私屏已关闭");
return tr("隐私屏");
}
feedback(now: number): string {
if (!this.desktopAvailable) return '';
@@ -73,26 +74,26 @@ export class RemotePrivacyScreen {
return this.requestId;
}
failed(id: number): void {
if (this.pending && this.requestId === id) { this.pending = false; this.failure = '指令发送失败,请重试'; }
if (this.pending && this.requestId === id) { this.pending = false; this.failure = tr("指令发送失败,请重试"); }
}
tick(now: number): void {
if (this.pending && now - this.requestedAt >= 15000) {
this.pending = false; this.failure = '请求超时,尚未确认电脑的隐私屏状态';
this.pending = false; this.failure = tr("请求超时,尚未确认电脑的隐私屏状态");
}
}
status(now: number): string {
if (this.failure) return this.failure;
const s = this.snapshot;
if (!s) return '等待电脑返回隐私屏状态;旧版电脑端可能不支持';
if (!this.fresh(now)) return '隐私屏状态已过期,等待电脑确认';
if (s.state === 2) return '正在开启隐私屏…';
if (s.state === 4) return '正在关闭隐私屏…';
if (this.pending) return '指令已发送,等待电脑确认…';
if (s.state === 5) return '隐私屏异常';
if (!s.supported || s.state === 0) return '隐私屏暂不可用';
if (!s) return tr("等待电脑返回隐私屏状态;旧版电脑端可能不支持");
if (!this.fresh(now)) return tr("隐私屏状态已过期,等待电脑确认");
if (s.state === 2) return tr("正在开启隐私屏…");
if (s.state === 4) return tr("正在关闭隐私屏…");
if (this.pending) return tr("指令已发送,等待电脑确认…");
if (s.state === 5) return tr("隐私屏异常");
if (!s.supported || s.state === 0) return tr("隐私屏暂不可用");
if (s.state === 3 && s.overlay_active && !s.remote_paused)
return '隐私屏已开启';
return s.state === 1 && !this.active() ? '隐私屏已关闭' : '隐私屏状态待确认';
return tr("隐私屏已开启");
return s.state === 1 && !this.active() ? tr("隐私屏已关闭") : tr("隐私屏状态待确认");
}
detail(now: number): string {
const s = this.snapshot;