mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-05 05:22:38 +08:00
[feat] add system language support across desktop and mobile
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user