mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 00:33:06 +08:00
[feat] add system language support across desktop and mobile
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { i18n } from '@kit.LocalizationKit';
|
||||
import native from 'libslint_desktop.so';
|
||||
import { TextInputHost } from './TextInputHost';
|
||||
import { TrayHost } from './TrayHost';
|
||||
@@ -128,7 +129,7 @@ export class DesktopHost {
|
||||
if (event.op === 'clipboardChanged') DesktopHost.clipboardChanged();
|
||||
else DesktopHost.send(event);
|
||||
}, hostPermissions);
|
||||
native.start(JSON.stringify({ filesDir: context.filesDir, cacheDir: context.cacheDir,
|
||||
native.start(JSON.stringify({ language: i18n.System.getSystemLanguage(), filesDir: context.filesDir, cacheDir: context.cacheDir,
|
||||
width: screen.width, height: screen.height, density: screen.densityPixels,
|
||||
displayId: screen.id, displays: screens, name: 'Harmony PC',
|
||||
certificates: certificateManager.getCertificateStorePath({ certType: certificateManager.CertType.CA_CERT_SYSTEM }) }));
|
||||
|
||||
@@ -307,6 +307,8 @@ napi_value Start(napi_env env, napi_callback_info info) {
|
||||
stopping = false;
|
||||
gui_thread = std::thread([] {
|
||||
try {
|
||||
const auto language = Environment().value("language", "en");
|
||||
setenv("LANGUAGE", language.c_str(), 1);
|
||||
setenv("SLINT_DEFAULT_FONT", "/system/fonts/HarmonyOS_Sans.ttf", 1);
|
||||
setenv("SLINT_FONT_PATH", "/system/fonts/HarmonyOS_Sans_SC.ttf", 1);
|
||||
const auto certificates = Environment().value("certificates", "");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tr, currentLanguage } from '../mobile/Localization';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { MobileController, MobileState, RemoteDisplay } from '../mobile/MobileController';
|
||||
import { MobileRemoteView } from '../mobile/MobileRemoteView';
|
||||
@@ -69,9 +70,9 @@ struct Mobile {
|
||||
}
|
||||
private title(): string {
|
||||
switch (this.state.page) {
|
||||
case 'settings': return '设置'; case 'privacy': return '隐私与授权';
|
||||
case 'about': return '关于 CrossDesk'; case 'licenses': return '开源组件'; case 'component': return this.component?.name ?? '';
|
||||
case 'source': return '源码与构建说明'; case 'announcements': return '通知公告'; case 'announcement': return '公告详情';
|
||||
case 'settings': return tr("设置"); case 'privacy': return tr("隐私与授权");
|
||||
case 'about': return tr("关于 CrossDesk"); case 'licenses': return tr("开源组件"); case 'component': return this.component?.name ?? '';
|
||||
case 'source': return tr("源码与构建说明"); case 'announcements': return tr("通知公告"); case 'announcement': return tr("公告详情");
|
||||
case 'document': return this.state.documentTitle; default: return '';
|
||||
}
|
||||
}
|
||||
@@ -80,7 +81,9 @@ struct Mobile {
|
||||
private policyParagraphs(body: string): string[] {
|
||||
const text = body.replace(/\r\n/g, '\n');
|
||||
const start = text.indexOf('## 中文\n'), end = start < 0 ? -1 : text.indexOf('## English', start);
|
||||
const localized = start >= 0 && end > start ? text.substring(start + '## 中文\n'.length, end) : text;
|
||||
const chinese = currentLanguage() === 0 || currentLanguage() === 3;
|
||||
const localized = chinese && start >= 0 && end > start ? text.substring(start + '## 中文\n'.length, end) :
|
||||
!chinese && end >= 0 ? text.substring(end + '## English'.length) : text;
|
||||
return localized.split(/\n\s*\n/).map((paragraph: string) => paragraph.trim()).filter((paragraph: string) => !!paragraph && paragraph !== '---');
|
||||
}
|
||||
private openAnnouncement(item: AnnouncementItem): void { this.controller?.inbox.markRead(item); this.state.announcement = item; this.navigate('announcement'); }
|
||||
@@ -141,16 +144,16 @@ struct Mobile {
|
||||
@Builder
|
||||
ConnectionCard() {
|
||||
Column({ space: 16 }) {
|
||||
Text('远程桌面').fontSize(20).fontWeight(600).fontColor(MobileTheme.ink).width('100%')
|
||||
Text(tr("远程桌面")).fontSize(20).fontWeight(600).fontColor(MobileTheme.ink).width('100%')
|
||||
Row({ space: 10 }) {
|
||||
TextInput({ text: this.formatId(this.state.remote), placeholder: '对端 ID' }).type(InputType.Number).maxLength(11)
|
||||
TextInput({ text: this.formatId(this.state.remote), placeholder: tr("对端 ID") }).type(InputType.Number).maxLength(11)
|
||||
.fontSize(20).fontWeight(500).fontColor(MobileTheme.ink).placeholderColor(MobileTheme.secondary)
|
||||
.height(54).layoutWeight(1).backgroundColor(MobileTheme.background).borderRadius(12)
|
||||
.border({ width: this.idFocused ? 2 : 1, color: this.idFocused ? MobileTheme.blue : MobileTheme.line })
|
||||
.padding({ left: 14, right: 14 }).onFocus(() => this.idFocused = true).onBlur(() => this.idFocused = false)
|
||||
.onChange((value: string) => this.state.remote = this.deviceId(value)).onSubmit(() => this.controller?.requestConnection(this.state.remote))
|
||||
Column() {
|
||||
MobilePrimaryButton({ label: '连接', buttonHeight: 54, available: this.state.remote.length > 0,
|
||||
MobilePrimaryButton({ label: tr("连接"), buttonHeight: 54, available: this.state.remote.length > 0,
|
||||
action: () => this.controller?.requestConnection(this.state.remote) })
|
||||
}.width(96)
|
||||
}.width('100%')
|
||||
@@ -161,7 +164,7 @@ struct Mobile {
|
||||
RecentCard(device: RecentDevice) {
|
||||
Column() {
|
||||
Stack() {
|
||||
Text(device.platform === 'windows' ? 'Windows' : device.platform === 'macos' ? 'macOS' : device.platform === 'linux' ? 'Linux' : '远程电脑')
|
||||
Text(device.platform === 'windows' ? 'Windows' : device.platform === 'macos' ? 'macOS' : device.platform === 'linux' ? 'Linux' : tr("远程电脑"))
|
||||
.fontSize(19).fontWeight(600).fontColor(Color.White).width('100%').height('100%').textAlign(TextAlign.Center)
|
||||
if (this.state.previews && device.preview) { Image(`file://${device.preview}`).objectFit(ImageFit.Cover).width('100%').height('100%') }
|
||||
}.aspectRatio(16 / 9).width('100%').backgroundColor(device.platform === 'windows' ? '#316AB7' : device.platform === 'macos' ? '#8057B9' : device.platform === 'linux' ? '#AE642B' : '#677587')
|
||||
@@ -169,8 +172,8 @@ struct Mobile {
|
||||
Text(device.name || device.id).fontSize(12).fontWeight(600).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
|
||||
Row({ space: 4 }) {
|
||||
Text(`ID ${device.id}`).fontSize(10).fontColor(MobileTheme.secondary).layoutWeight(1)
|
||||
if (device.remember) { MobileIcon({ name: 'key', iconExtent: 11, color: MobileTheme.secondary }).accessibilityText('已保存密码') }
|
||||
Text(this.controller?.presence.online(device.id, this.controller?.now() ?? 0) ? '在线' : '离线').fontSize(10)
|
||||
if (device.remember) { MobileIcon({ name: 'key', iconExtent: 11, color: MobileTheme.secondary }).accessibilityText(tr("已保存密码")) }
|
||||
Text(this.controller?.presence.online(device.id, this.controller?.now() ?? 0) ? tr("在线") : tr("离线")).fontSize(10)
|
||||
.fontColor(this.controller?.presence.online(device.id, this.controller?.now() ?? 0) ? MobileTheme.green : MobileTheme.secondary)
|
||||
}.width('100%')
|
||||
}.padding(9).width('100%')
|
||||
@@ -182,14 +185,14 @@ struct Mobile {
|
||||
Recent() {
|
||||
Column({ space: 14 }) {
|
||||
Row() {
|
||||
Text('最近连接').fontSize(20).fontWeight(600).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
if (this.state.recent.length) { Text(`${this.state.recent.length} 台设备`).fontSize(12).fontColor(MobileTheme.secondary) }
|
||||
Text(tr("最近连接")).fontSize(20).fontWeight(600).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
if (this.state.recent.length) { Text(tr("{0} 台设备", [String(this.state.recent.length)])).fontSize(12).fontColor(MobileTheme.secondary) }
|
||||
}.width('100%')
|
||||
if (this.state.recent.length === 0) {
|
||||
Column() {
|
||||
MobileIcon({ name: 'history', iconExtent: 34, color: '#B6B6BB' })
|
||||
Text('还没有连接记录').fontSize(17).fontWeight(600).fontColor(MobileTheme.ink).margin({ top: 12, bottom: 8 })
|
||||
Text('成功连接后,这里会显示最近的远程设备。').fontSize(13).lineHeight(19).fontColor(MobileTheme.secondary).textAlign(TextAlign.Center)
|
||||
Text(tr("还没有连接记录")).fontSize(17).fontWeight(600).fontColor(MobileTheme.ink).margin({ top: 12, bottom: 8 })
|
||||
Text(tr("成功连接后,这里会显示最近的远程设备。")).fontSize(13).lineHeight(19).fontColor(MobileTheme.secondary).textAlign(TextAlign.Center)
|
||||
}.layoutWeight(1).width('100%').justifyContent(FlexAlign.Center).padding({ left: 6, right: 6 })
|
||||
} else {
|
||||
Scroll() {
|
||||
@@ -212,7 +215,7 @@ struct Mobile {
|
||||
}.padding({ left: 11, right: 11 }).height(34).backgroundColor(Color.White).borderRadius(17).onClick(() => this.controller?.reconnect())
|
||||
Blank()
|
||||
Stack({ alignContent: Alignment.TopEnd }) {
|
||||
MobileIconButton({ icon: 'envelope', label: '通知公告', action: () => this.navigate('announcements') })
|
||||
MobileIconButton({ icon: 'envelope', label: tr("通知公告"), action: () => this.navigate('announcements') })
|
||||
if (this.state.unread > 0) {
|
||||
Text(this.state.unread > 99 ? '99+' : `${this.state.unread}`).fontSize(10).fontWeight(600).fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center).padding({ left: 4, right: 4 }).constraintSize({ minWidth: 16 }).height(16)
|
||||
@@ -220,7 +223,7 @@ struct Mobile {
|
||||
}
|
||||
}
|
||||
Stack({ alignContent: Alignment.TopEnd }) {
|
||||
MobileIconButton({ icon: 'settings', label: '设置', action: () => this.navigate('settings') })
|
||||
MobileIconButton({ icon: 'settings', label: tr("设置"), action: () => this.navigate('settings') })
|
||||
if (this.state.update) {
|
||||
Text('!').fontSize(10).fontWeight(600).fontColor(Color.White).textAlign(TextAlign.Center).width(16).height(16)
|
||||
.backgroundColor(MobileTheme.red).borderRadius(8).translate({ x: 5, y: -3 }).hitTestBehavior(HitTestMode.None)
|
||||
@@ -246,72 +249,72 @@ struct Mobile {
|
||||
@Builder
|
||||
PreviewRow() {
|
||||
Row({ space: 12 }) {
|
||||
Text('保存远程画面预览').fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
Text(tr("保存远程画面预览")).fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.previewToggle }).selectedColor(MobileTheme.green)
|
||||
.onChange((value: boolean) => {
|
||||
this.previewToggle = value;
|
||||
if (value && !this.state.previews) this.state.modal = 'previews';
|
||||
else if (!value) this.controller?.setPreviews(false);
|
||||
}).accessibilityText('保存远程画面预览')
|
||||
}).accessibilityText(tr("保存远程画面预览"))
|
||||
}.constraintSize({ minHeight: 52 }).padding({ left: 16, right: 16, top: 8, bottom: 8 }).width('100%')
|
||||
}
|
||||
@Builder
|
||||
Settings() {
|
||||
Column() {
|
||||
this.SectionTitle('鼠标控制')
|
||||
this.SectionTitle(tr("鼠标控制"))
|
||||
Column() {
|
||||
MobileSegment({ labels: ['相对位置', '绝对位置'], selection: this.state.relative ? 0 : 1,
|
||||
MobileSegment({ labels: [tr("相对位置"), tr("绝对位置")], selection: this.state.relative ? 0 : 1,
|
||||
changed: (i: number) => this.controller?.setRelative(i === 0) })
|
||||
}.padding(12).width('100%').backgroundColor(Color.White).borderRadius(26).border({ width: 0.5, color: MobileTheme.line })
|
||||
this.SectionTitle('画面偏好')
|
||||
this.SectionTitle(tr("画面偏好"))
|
||||
Column() {
|
||||
MobileSegment({ labels: ['帧率优先', '画质优先', '平衡'], selection: this.state.videoPreference,
|
||||
MobileSegment({ labels: [tr("帧率优先"), tr("画质优先"), tr("平衡")], selection: this.state.videoPreference,
|
||||
changed: (i: number) => this.controller?.setPreference(i) })
|
||||
}.padding(12).width('100%').backgroundColor(Color.White).borderRadius(26).border({ width: 0.5, color: MobileTheme.line })
|
||||
this.SectionTitle('屏幕方向')
|
||||
this.SectionTitle(tr("屏幕方向"))
|
||||
Row() {
|
||||
Text('连接建立后锁定横屏').fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
Text(tr("连接建立后锁定横屏")).fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.state.autoLandscape }).selectedColor(MobileTheme.green)
|
||||
.onChange((value: boolean) => this.controller?.setAutoLandscape(value)).accessibilityText('连接建立后锁定横屏')
|
||||
.onChange((value: boolean) => this.controller?.setAutoLandscape(value)).accessibilityText(tr("连接建立后锁定横屏"))
|
||||
}.padding({ left: 16, right: 16, top: 10, bottom: 10 }).width('100%').backgroundColor(Color.White)
|
||||
.borderRadius(26).border({ width: 0.5, color: MobileTheme.line })
|
||||
Text('默认关闭。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受影响。')
|
||||
Text(tr("默认关闭。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受影响。"))
|
||||
.fontSize(12).fontColor(MobileTheme.secondary).width('100%').padding({ left: 16, right: 16, top: 8, bottom: 12 })
|
||||
Column() {
|
||||
this.SettingRow('服务器', this.customServer ? '自定义' : '默认', () => this.serverExpanded = !this.serverExpanded)
|
||||
this.SettingRow(tr("服务器"), this.customServer ? tr("自定义") : tr("默认"), () => this.serverExpanded = !this.serverExpanded)
|
||||
if (this.serverExpanded) {
|
||||
this.Rule()
|
||||
Row() {
|
||||
Text('使用自定义服务器').fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
Text(tr("使用自定义服务器")).fontSize(16).fontColor(MobileTheme.ink).layoutWeight(1)
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.customServer }).selectedColor(MobileTheme.green).onChange((value: boolean) => {
|
||||
this.customServer = value;
|
||||
if (!value) { this.editingServer = 'api.crossdesk.cn'; this.editingPort = '9099'; this.controller?.saveSettings(this.editingServer, this.editingPort); }
|
||||
}).accessibilityText('使用自定义服务器')
|
||||
}).accessibilityText(tr("使用自定义服务器"))
|
||||
}.padding({ left: 16, right: 16, top: 8, bottom: 8 }).width('100%')
|
||||
if (this.customServer) {
|
||||
Column({ space: 8 }) {
|
||||
TextInput({ text: this.editingServer, placeholder: '服务器地址' }).fontSize(17).height(48).borderRadius(12)
|
||||
TextInput({ text: this.editingServer, placeholder: tr("服务器地址") }).fontSize(17).height(48).borderRadius(12)
|
||||
.backgroundColor(MobileTheme.background).onChange((v: string) => this.editingServer = v)
|
||||
TextInput({ text: this.editingPort, placeholder: '服务器端口' }).fontSize(17).height(48).borderRadius(12)
|
||||
TextInput({ text: this.editingPort, placeholder: tr("服务器端口") }).fontSize(17).height(48).borderRadius(12)
|
||||
.backgroundColor(MobileTheme.background).type(InputType.Number).onChange((v: string) => this.editingPort = v)
|
||||
this.Action('完成', () => { this.saveServer(); })
|
||||
this.Action(tr("完成"), () => { this.saveServer(); })
|
||||
}.padding({ left: 12, right: 12, bottom: 12 }).width('100%')
|
||||
}
|
||||
}
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line }).margin({ top: 28 })
|
||||
Text(this.state.identity ? `本机 ID ${this.formatId(this.state.identity)}` : this.state.signalText).fontSize(12).fontColor(MobileTheme.secondary)
|
||||
Text(this.state.identity ? tr("本机 ID {0}", [String(this.formatId(this.state.identity))]) : this.state.signalText).fontSize(12).fontColor(MobileTheme.secondary)
|
||||
.textAlign(TextAlign.Center).padding({ left: 16, right: 16, top: 12, bottom: 16 }).width('100%').copyOption(CopyOptions.LocalDevice)
|
||||
this.SectionTitle('隐私')
|
||||
this.SectionTitle(tr("隐私"))
|
||||
Column() {
|
||||
this.SettingRow('隐私与授权', this.state.consent ? '已授权' : '未授权', () => this.navigate('privacy'))
|
||||
this.SettingRow(tr("隐私与授权"), this.state.consent ? tr("已授权") : tr("未授权"), () => this.navigate('privacy'))
|
||||
this.Rule()
|
||||
this.PreviewRow()
|
||||
this.Rule()
|
||||
Button({ type: ButtonType.Normal }) { Text('清除预览图').fontSize(16).fontColor(MobileTheme.red).width('100%').textAlign(TextAlign.Start) }
|
||||
Button({ type: ButtonType.Normal }) { Text(tr("清除预览图")).fontSize(16).fontColor(MobileTheme.red).width('100%').textAlign(TextAlign.Start) }
|
||||
.height(48).width('100%').padding({ left: 16 }).backgroundColor(Color.White)
|
||||
.onClick(() => this.state.modal = 'clearPreviews')
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line })
|
||||
Column() { this.SettingRow('关于', this.state.update ? '新版本 ●' : '', () => this.navigate('about')) }
|
||||
Column() { this.SettingRow(tr("关于"), this.state.update ? tr("新版本 ●") : '', () => this.navigate('about')) }
|
||||
.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line }).margin({ top: 28 })
|
||||
}.width('100%')
|
||||
}
|
||||
@@ -319,45 +322,45 @@ struct Mobile {
|
||||
Privacy() {
|
||||
Column() {
|
||||
Column() {
|
||||
this.ValueRow('授权状态', this.state.consent ? '已授权' : '未授权')
|
||||
this.ValueRow(tr("授权状态"), this.state.consent ? tr("已授权") : tr("未授权"))
|
||||
this.Rule()
|
||||
this.SettingRow('隐私政策', '', () => this.controller?.document('PRIVACY.md', '隐私政策', 'privacy'))
|
||||
this.SettingRow(tr("隐私政策"), '', () => this.controller?.document('PRIVACY.md', tr("隐私政策"), 'privacy'))
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(12).clip(true).border({ width: 0.5, color: MobileTheme.line })
|
||||
this.Footer('设备身份及选择保存的访问密码加密保存在本机。画面预览默认关闭,开启后从下一次连接起保存。剪贴板内容在结束会话时清除。离开应用后通过画中画或后台任务保持会话,可随时返回应用断开。')
|
||||
if (this.state.consent) { this.Action('撤回联网授权', () => this.state.modal = 'revoke', true) }
|
||||
else { MobilePrimaryButton({ label: '阅读并授权', action: () => this.state.modal = 'consent' }) }
|
||||
this.Footer(tr("设备身份及选择保存的访问密码加密保存在本机。画面预览默认关闭,开启后从下一次连接起保存。剪贴板内容在结束会话时清除。离开应用后通过画中画或后台任务保持会话,可随时返回应用断开。"))
|
||||
if (this.state.consent) { this.Action(tr("撤回联网授权"), () => this.state.modal = 'revoke', true) }
|
||||
else { MobilePrimaryButton({ label: tr("阅读并授权"), action: () => this.state.modal = 'consent' }) }
|
||||
}.width('100%')
|
||||
}
|
||||
@Builder
|
||||
About() {
|
||||
Column() {
|
||||
Column() {
|
||||
this.ValueRow('版本', this.state.version)
|
||||
if (this.state.update) { this.Rule(); this.SettingRow('新版本可用', `v${this.state.update}`, () => this.controller?.platform.openLink('https://crossdesk.cn')) }
|
||||
this.ValueRow(tr("版本"), this.state.version)
|
||||
if (this.state.update) { this.Rule(); this.SettingRow(tr("新版本可用"), `v${this.state.update}`, () => this.controller?.platform.openLink('https://crossdesk.cn')) }
|
||||
if (this.state.updateStatus) { this.Footer(this.state.updateStatus) }
|
||||
this.Rule()
|
||||
this.Action('检查更新', () => this.controller?.checkLocal(true))
|
||||
this.Action(tr("检查更新"), () => this.controller?.checkLocal(true))
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(12).clip(true).border({ width: 0.5, color: MobileTheme.line })
|
||||
Column() {
|
||||
this.SettingRow('软件许可', 'GPL-3.0-only', () => this.controller?.document('LICENSE', '软件许可'))
|
||||
this.SettingRow(tr("软件许可"), 'GPL-3.0-only', () => this.controller?.document('LICENSE', tr("软件许可")))
|
||||
this.Rule()
|
||||
this.SettingRow('开源软件权利说明', '', () => this.controller?.document('OPEN_SOURCE_NOTICE.txt', '开源软件权利说明'))
|
||||
this.SettingRow(tr("开源软件权利说明"), '', () => this.controller?.document('OPEN_SOURCE_NOTICE.txt', tr("开源软件权利说明")))
|
||||
this.Rule()
|
||||
this.SettingRow('开源组件', '', () => this.navigate('licenses'))
|
||||
this.SettingRow(tr("开源组件"), '', () => this.navigate('licenses'))
|
||||
this.Rule()
|
||||
this.SettingRow('源码与构建说明', '', () => this.navigate('source'))
|
||||
this.SettingRow(tr("源码与构建说明"), '', () => this.navigate('source'))
|
||||
}.backgroundColor(Color.White).borderRadius(12).clip(true).width('100%').border({ width: 0.5, color: MobileTheme.line }).margin({ top: 28 })
|
||||
this.Footer('许可与版权声明可离线查阅。')
|
||||
this.Footer(tr("许可与版权声明可离线查阅。"))
|
||||
}.width('100%')
|
||||
}
|
||||
@Builder
|
||||
Announcements() {
|
||||
Column({ space: 12 }) {
|
||||
if (!this.state.consent) { Text('同意联网后可查看公告').fontSize(14); this.Action('查看联网说明', () => this.state.modal = 'consent') }
|
||||
if (this.controller?.inbox.failed) { this.Action('加载失败,点击重试', () => this.controller?.inbox.refresh()) }
|
||||
if (!this.state.consent) { Text(tr("同意联网后可查看公告")).fontSize(14); this.Action(tr("查看联网说明"), () => this.state.modal = 'consent') }
|
||||
if (this.controller?.inbox.failed) { this.Action(tr("加载失败,点击重试"), () => this.controller?.inbox.refresh()) }
|
||||
if (this.controller?.inbox.loading) { LoadingProgress().width(28).height(28) }
|
||||
if (this.controller?.inbox.saveFailed) { Text('已读或删除状态保存失败,请重试').fontSize(12).fontColor(MobileTheme.red) }
|
||||
if (!this.state.announcements.length && this.controller?.inbox.loaded && !this.controller?.inbox.loading) { Text('暂无公告').fontSize(16).fontColor(MobileTheme.secondary).padding(40) }
|
||||
if (this.controller?.inbox.saveFailed) { Text(tr("已读或删除状态保存失败,请重试")).fontSize(12).fontColor(MobileTheme.red) }
|
||||
if (!this.state.announcements.length && this.controller?.inbox.loaded && !this.controller?.inbox.loading) { Text(tr("暂无公告")).fontSize(16).fontColor(MobileTheme.secondary).padding(40) }
|
||||
ForEach(this.state.announcements, (item: AnnouncementItem) => {
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 8 }) { if (!item.read) { Row() {}.width(6).height(6).borderRadius(3).backgroundColor(MobileTheme.blue) } Text(item.title).fontSize(17).fontWeight(600).layoutWeight(1) }.width('100%')
|
||||
@@ -366,7 +369,7 @@ struct Mobile {
|
||||
}.padding(18).width('100%').backgroundColor(Color.White).borderRadius(18).border({ width: 0.5, color: MobileTheme.line })
|
||||
.onClick(() => this.openAnnouncement(item)).gesture(LongPressGesture().onAction(() => { this.state.announcement = item; this.state.modal = 'deleteAnnouncement'; }))
|
||||
}, (item: AnnouncementItem) => `${item.id}:${item.revision}:${item.read}`)
|
||||
if ((this.controller?.inbox.total ?? 0) > this.state.announcements.length) { this.Action('加载更多', () => this.controller?.inbox.more()) }
|
||||
if ((this.controller?.inbox.total ?? 0) > this.state.announcements.length) { this.Action(tr("加载更多"), () => this.controller?.inbox.more()) }
|
||||
}.width('100%')
|
||||
}
|
||||
@Builder
|
||||
@@ -385,10 +388,10 @@ struct Mobile {
|
||||
} else { Span(part.text) }
|
||||
})
|
||||
}.fontSize(16).lineHeight(25).width('100%').copyOption(CopyOptions.LocalDevice)
|
||||
this.Action('删除公告', () => this.state.modal = 'deleteAnnouncement', true)
|
||||
this.Action(tr("删除公告"), () => this.state.modal = 'deleteAnnouncement', true)
|
||||
}.width('100%')
|
||||
} else if (this.state.page === 'document') {
|
||||
if (this.state.documentTitle === '隐私政策') { this.PolicyText(this.state.documentText) }
|
||||
if (this.state.documentTitle === tr("隐私政策")) { this.PolicyText(this.state.documentText) }
|
||||
else {
|
||||
Column({ space: 12 }) {
|
||||
ForEach(this.state.documentText.match(/[\s\S]{1,4000}/g) ?? [], (paragraph: string) => {
|
||||
@@ -398,8 +401,8 @@ struct Mobile {
|
||||
}
|
||||
} else if (this.state.page === 'licenses') {
|
||||
Column() {
|
||||
this.SectionTitle('OpenFEC 告知')
|
||||
Column() { this.Footer('OpenFEC 提供前向纠错能力,遵循 CeCILL-C 1.0 许可。其许可规定了有限保证及责任限制,请阅读组件详情。') }
|
||||
this.SectionTitle(tr("OpenFEC 告知"))
|
||||
Column() { this.Footer(tr("OpenFEC 提供前向纠错能力,遵循 CeCILL-C 1.0 许可。其许可规定了有限保证及责任限制,请阅读组件详情。")) }
|
||||
.width('100%').backgroundColor(Color.White).borderRadius(12).border({ width: 0.5, color: MobileTheme.line })
|
||||
Column() {
|
||||
ForEach(this.licenses, (component: LicenseComponent, index: number) => {
|
||||
@@ -415,18 +418,18 @@ struct Mobile {
|
||||
}.width('100%').padding(0).backgroundColor(Color.White).onClick(() => { this.component = component; this.navigate('component'); })
|
||||
}, (component: LicenseComponent) => component.id)
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(12).clip(true).border({ width: 0.5, color: MobileTheme.line }).margin({ top: 28 })
|
||||
this.Footer('各组件遵循所列开源许可证。许可与版权声明可离线查阅,查看源码需要网络连接。')
|
||||
this.Footer(tr("各组件遵循所列开源许可证。许可与版权声明可离线查阅,查看源码需要网络连接。"))
|
||||
}.width('100%')
|
||||
} else if (this.state.page === 'component') {
|
||||
Column() {
|
||||
Column() {
|
||||
this.ValueRow('版本', this.component?.version ?? '')
|
||||
this.ValueRow(tr("版本"), this.component?.version ?? '')
|
||||
this.Rule()
|
||||
this.ValueRow('许可', this.component?.license ?? '')
|
||||
this.ValueRow(tr("许可"), this.component?.license ?? '')
|
||||
this.Rule()
|
||||
this.SettingRow('查看源码', '', () => this.controller?.platform.openLink(this.component?.sourceURL ?? ''))
|
||||
this.SettingRow(tr("查看源码"), '', () => this.controller?.platform.openLink(this.component?.sourceURL ?? ''))
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(12).clip(true).border({ width: 0.5, color: MobileTheme.line })
|
||||
this.SectionTitle('许可与版权声明')
|
||||
this.SectionTitle(tr("许可与版权声明"))
|
||||
Column() {
|
||||
ForEach(this.component?.documents ?? [], (document: LicenseDocument, index: number) => {
|
||||
if (index > 0) { this.Rule() }
|
||||
@@ -436,16 +439,16 @@ struct Mobile {
|
||||
}.width('100%')
|
||||
} else if (this.state.page === 'source') {
|
||||
Column() {
|
||||
this.SectionTitle('本版本源码与构建说明')
|
||||
this.SectionTitle(tr("本版本源码与构建说明"))
|
||||
Column() {
|
||||
this.ValueRow('源码版本', (this.source?.revision ?? '').substring(0, 12))
|
||||
this.ValueRow(tr("源码版本"), (this.source?.revision ?? '').substring(0, 12))
|
||||
this.Rule()
|
||||
this.ValueRow('MiniRTC', (this.source?.miniRTCRevision ?? '').substring(0, 12))
|
||||
if (this.source?.isModified) { this.Rule(); this.Footer('开发版本包含尚未发布的修改,以下链接为基础版本源码。') }
|
||||
if (this.source?.isModified) { this.Rule(); this.Footer(tr("开发版本包含尚未发布的修改,以下链接为基础版本源码。")) }
|
||||
this.Rule()
|
||||
this.SettingRow('查看源码', '', () => this.controller?.platform.openLink(this.source?.sourceURL ?? ''))
|
||||
this.SettingRow(tr("查看源码"), '', () => this.controller?.platform.openLink(this.source?.sourceURL ?? ''))
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(12).clip(true).border({ width: 0.5, color: MobileTheme.line })
|
||||
this.SectionTitle('构建文档')
|
||||
this.SectionTitle(tr("构建文档"))
|
||||
Column() {
|
||||
ForEach(this.source?.buildDocuments ?? [], (document: LicenseDocument, index: number) => {
|
||||
if (index > 0) { this.Rule() }
|
||||
@@ -462,7 +465,7 @@ struct Mobile {
|
||||
Text(title).fontSize(22).fontWeight(600).fontColor(MobileTheme.ink).width('100%')
|
||||
if (subtitle) { Text(subtitle).fontSize(15).fontColor(MobileTheme.secondary).width('100%') }
|
||||
}.layoutWeight(1)
|
||||
MobileIconButton({ icon: 'close', label: '关闭', iconExtent: 36, iconSize: 20, action: () => this.dismissModal() })
|
||||
MobileIconButton({ icon: 'close', label: tr("关闭"), iconExtent: 36, iconSize: 20, action: () => this.dismissModal() })
|
||||
}.width('100%')
|
||||
}
|
||||
@Builder
|
||||
@@ -470,24 +473,24 @@ struct Mobile {
|
||||
Scroll() {
|
||||
Column() {
|
||||
Row() {}.width(36).height(5).backgroundColor('#C6C6CB').borderRadius(3).margin({ bottom: 20 })
|
||||
this.SheetHeader('连接远程桌面', `对端 ID ${this.formatId(this.state.remote)}`)
|
||||
this.SheetHeader(tr("连接远程桌面"), tr("对端 ID {0}", [String(this.formatId(this.state.remote))]))
|
||||
Row() {
|
||||
TextInput({ text: this.state.password, placeholder: '访问密码' }).type(this.showPassword ? InputType.Normal : InputType.Password)
|
||||
TextInput({ text: this.state.password, placeholder: tr("访问密码") }).type(this.showPassword ? InputType.Normal : InputType.Password)
|
||||
.showPasswordIcon(false).maxLength(128).fontSize(17).height(50).layoutWeight(1).backgroundColor(Color.Transparent)
|
||||
.padding({ left: 14, right: 4 }).onChange((value: string) => this.state.password = value).onSubmit(() => this.controller?.begin(this.state.password))
|
||||
MobileIconButton({ icon: this.showPassword ? 'eye_off' : 'eye', label: this.showPassword ? '隐藏密码' : '显示密码', iconExtent: 44,
|
||||
MobileIconButton({ icon: this.showPassword ? 'eye_off' : 'eye', label: this.showPassword ? tr("隐藏密码") : tr("显示密码"), iconExtent: 44,
|
||||
action: () => this.showPassword = !this.showPassword })
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(10).border({ width: 0.5, color: MobileTheme.line }).margin({ top: 18 })
|
||||
if (this.state.notice) { Text(this.state.notice).fontSize(13).fontColor(MobileTheme.red).width('100%').margin({ top: 8 }) }
|
||||
Row({ space: 12 }) {
|
||||
Column({ space: 3 }) {
|
||||
Text('保存密码').fontSize(15).fontWeight(600).fontColor(MobileTheme.ink).width('100%')
|
||||
Text('密码将加密保存在此设备上').fontSize(11).fontColor(MobileTheme.secondary).width('100%')
|
||||
Text(tr("保存密码")).fontSize(15).fontWeight(600).fontColor(MobileTheme.ink).width('100%')
|
||||
Text(tr("密码将加密保存在此设备上")).fontSize(11).fontColor(MobileTheme.secondary).width('100%')
|
||||
}.layoutWeight(1)
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.state.remember }).selectedColor(MobileTheme.green)
|
||||
.onChange((value: boolean) => this.state.remember = value).accessibilityText('保存密码')
|
||||
.onChange((value: boolean) => this.state.remember = value).accessibilityText(tr("保存密码"))
|
||||
}.width('100%').margin({ top: 18, bottom: 18 })
|
||||
MobilePrimaryButton({ label: '连接', available: this.state.password.length > 0, action: () => this.controller?.begin(this.state.password) })
|
||||
MobilePrimaryButton({ label: tr("连接"), available: this.state.password.length > 0, action: () => this.controller?.begin(this.state.password) })
|
||||
}.padding({ left: 22, right: 22, top: 10, bottom: 22 }).width('100%')
|
||||
}.width(this.viewWidth < 600 ? '100%' : Math.min(520, this.viewWidth - 48)).constraintSize({ maxHeight: this.viewHeight * 0.92 })
|
||||
.backgroundColor(MobileTheme.background).borderRadius(24).scrollBar(BarState.Off)
|
||||
@@ -506,11 +509,11 @@ struct Mobile {
|
||||
@Builder
|
||||
ConsentSheet() {
|
||||
Column() {
|
||||
Text('隐私政策').fontSize(17).fontWeight(600).fontColor(MobileTheme.ink).height(56).width('100%').textAlign(TextAlign.Center)
|
||||
Text(tr("隐私政策")).fontSize(17).fontWeight(600).fontColor(MobileTheme.ink).height(56).width('100%').textAlign(TextAlign.Center)
|
||||
this.Rule()
|
||||
Scroll() {
|
||||
Column({ space: 12 }) {
|
||||
if (!this.state.privacyAvailable) { Text('隐私政策无法读取,请重新安装应用').fontSize(14).fontColor(MobileTheme.red) }
|
||||
if (!this.state.privacyAvailable) { Text(tr("隐私政策无法读取,请重新安装应用")).fontSize(14).fontColor(MobileTheme.red) }
|
||||
this.PolicyText(this.privacyText)
|
||||
}.padding(20).width('100%')
|
||||
}.layoutWeight(1).width('100%').align(Alignment.Top).scrollBar(BarState.Off)
|
||||
@@ -518,11 +521,11 @@ struct Mobile {
|
||||
Column({ space: 12 }) {
|
||||
Row({ space: 8 }) {
|
||||
Checkbox({ name: 'consent' }).width(20).height(20).margin(0).select(this.accepted).selectedColor(MobileTheme.blue)
|
||||
.onChange((value: boolean) => this.accepted = value).accessibilityText('我已阅读并同意隐私政策')
|
||||
Text('我已阅读并同意《隐私政策》').fontSize(14).fontColor(MobileTheme.ink).layoutWeight(1).onClick(() => this.accepted = !this.accepted)
|
||||
.onChange((value: boolean) => this.accepted = value).accessibilityText(tr("我已阅读并同意隐私政策"))
|
||||
Text(tr("我已阅读并同意《隐私政策》")).fontSize(14).fontColor(MobileTheme.ink).layoutWeight(1).onClick(() => this.accepted = !this.accepted)
|
||||
}.width('100%').constraintSize({ minHeight: 44 })
|
||||
MobilePrimaryButton({ label: '同意并继续', available: this.accepted && this.state.privacyAvailable, action: () => this.controller?.consent() })
|
||||
Button('暂不同意', { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.secondary).height(44).width('100%')
|
||||
MobilePrimaryButton({ label: tr("同意并继续"), available: this.accepted && this.state.privacyAvailable, action: () => this.controller?.consent() })
|
||||
Button(tr("暂不同意"), { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.secondary).height(44).width('100%')
|
||||
.backgroundColor(Color.Transparent).onClick(() => this.dismissModal())
|
||||
}.padding({ left: 20, right: 20, top: 12, bottom: 12 }).width('100%')
|
||||
}.width(this.viewWidth < 600 ? '100%' : Math.min(640, this.viewWidth - 48)).height(Math.min(760, this.viewHeight * 0.9))
|
||||
@@ -533,42 +536,42 @@ struct Mobile {
|
||||
if (this.state.modal === 'connecting') {
|
||||
Column({ space: 14 }) {
|
||||
LoadingProgress().width(34).height(34).color(MobileTheme.blue)
|
||||
Text('正在连接').fontSize(20).fontWeight(600).fontColor(MobileTheme.ink)
|
||||
Text(this.state.signal === 1 ? '正在建立安全连接…' : this.state.signalText).fontSize(14).fontColor(MobileTheme.secondary).textAlign(TextAlign.Center).constraintSize({ minHeight: 36 })
|
||||
Text(tr("正在连接")).fontSize(20).fontWeight(600).fontColor(MobileTheme.ink)
|
||||
Text(this.state.signal === 1 ? tr("正在建立安全连接…") : this.state.signalText).fontSize(14).fontColor(MobileTheme.secondary).textAlign(TextAlign.Center).constraintSize({ minHeight: 36 })
|
||||
}.padding({ left: 26, right: 26, top: 24, bottom: 20 }).width('100%')
|
||||
this.Rule()
|
||||
this.Action('取消连接', () => this.controller?.end(''), true)
|
||||
this.Action(tr("取消连接"), () => this.controller?.end(''), true)
|
||||
} else if (this.state.modal === 'privacyUnlock') {
|
||||
Column({ space: 12 }) {
|
||||
Text('隐私屏已被解锁').fontSize(20).fontWeight(600).fontColor(MobileTheme.ink)
|
||||
Text('被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。')
|
||||
Text(tr("隐私屏已被解锁")).fontSize(20).fontWeight(600).fontColor(MobileTheme.ink)
|
||||
Text(tr("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。"))
|
||||
.fontSize(14).lineHeight(21).fontColor(MobileTheme.secondary).width('100%')
|
||||
if (this.state.privacyFeedback) { Text(this.state.privacyFeedback).fontSize(12).fontColor(MobileTheme.secondary) }
|
||||
}.padding(24).width('100%')
|
||||
this.Rule()
|
||||
Row() {
|
||||
Button('确认', { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||
Button(tr("确认"), { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||
.backgroundColor(Color.White).onClick(() => this.dismissModal())
|
||||
Divider().vertical(true).height(50).strokeWidth(0.5).color(MobileTheme.line)
|
||||
Button('锁定', { type: ButtonType.Normal }).fontSize(16).fontWeight(600).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||
Button(tr("锁定"), { type: ButtonType.Normal }).fontSize(16).fontWeight(600).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||
.backgroundColor(Color.White).enabled(this.state.privacyCanLock).onClick(() => this.controller?.lockPrivacy())
|
||||
}.width('100%')
|
||||
} else if (this.state.modal === 'clipboard') {
|
||||
Column({ space: 16 }) {
|
||||
this.SheetHeader('剪贴板')
|
||||
Text('发送文本后,点击“粘贴到远端”使用。').fontSize(13).fontColor(MobileTheme.secondary).width('100%')
|
||||
TextArea({ text: this.state.clipboardText, placeholder: '要发送到远端的文本' }).fontSize(15).height(120).borderRadius(12).barState(BarState.Off)
|
||||
this.SheetHeader(tr("剪贴板"))
|
||||
Text(tr("发送文本后,点击“粘贴到远端”使用。")).fontSize(13).fontColor(MobileTheme.secondary).width('100%')
|
||||
TextArea({ text: this.state.clipboardText, placeholder: tr("要发送到远端的文本") }).fontSize(15).height(120).borderRadius(12).barState(BarState.Off)
|
||||
.backgroundColor(MobileTheme.background).onChange((value: string) => this.state.clipboardText = value)
|
||||
MobilePrimaryButton({ label: '发送文本', action: () => this.controller?.clipboard() })
|
||||
MobilePrimaryButton({ label: tr("发送文本"), action: () => this.controller?.clipboard() })
|
||||
Row({ space: 8 }) {
|
||||
Column() { this.Action('粘贴到远端', () => this.controller?.shortcut(86)) }.layoutWeight(1)
|
||||
Column() { this.Action('复制远端文本', () => this.controller?.copyRemote()) }.layoutWeight(1)
|
||||
Column() { this.Action(tr("粘贴到远端"), () => this.controller?.shortcut(86)) }.layoutWeight(1)
|
||||
Column() { this.Action(tr("复制远端文本"), () => this.controller?.copyRemote()) }.layoutWeight(1)
|
||||
}.width('100%')
|
||||
}.padding(20).width('100%')
|
||||
} else if (this.state.modal === 'displays') {
|
||||
Column({ space: 12 }) {
|
||||
this.SheetHeader('显示器')
|
||||
if (!this.state.displays.length) { this.Footer('等待远端屏幕信息') }
|
||||
this.SheetHeader(tr("显示器"))
|
||||
if (!this.state.displays.length) { this.Footer(tr("等待远端屏幕信息")) }
|
||||
ForEach(this.state.displays, (screen: RemoteDisplay, index: number) => {
|
||||
Button({ type: ButtonType.Normal }) {
|
||||
Row({ space: 12 }) {
|
||||
@@ -584,31 +587,31 @@ struct Mobile {
|
||||
Text(this.state.recent.find((d: RecentDevice) => d.id === this.deleteId)?.name || this.formatId(this.deleteId))
|
||||
.fontSize(17).fontWeight(600).fontColor(MobileTheme.ink).padding(20).width('100%').textAlign(TextAlign.Center)
|
||||
this.Rule()
|
||||
this.Action('连接', () => { this.dismissModal(); this.controller?.requestConnection(this.deleteId); })
|
||||
this.Action(tr("连接"), () => { this.dismissModal(); this.controller?.requestConnection(this.deleteId); })
|
||||
this.Rule()
|
||||
this.Action('删除记录', () => this.state.modal = 'delete', true)
|
||||
this.Action(tr("删除记录"), () => this.state.modal = 'delete', true)
|
||||
this.Rule()
|
||||
this.Action('取消', () => this.dismissModal())
|
||||
this.Action(tr("取消"), () => this.dismissModal())
|
||||
} else {
|
||||
Column({ space: 12 }) {
|
||||
Text(this.state.modal === 'notice' ? '连接提示' : this.state.modal === 'upgrade' ? '升级提示' : this.state.modal === 'disconnect' ? '断开远程连接?' :
|
||||
this.state.modal === 'revoke' ? '撤回联网授权?' : this.state.modal === 'previews' ? '保存远程画面预览?' : this.state.modal === 'clearPreviews' ? '清除预览图?' : this.state.modal === 'deleteAnnouncement' ? '删除公告?' : '删除记录?')
|
||||
Text(this.state.modal === 'notice' ? tr("连接提示") : this.state.modal === 'upgrade' ? tr("升级提示") : this.state.modal === 'disconnect' ? tr("断开远程连接?") :
|
||||
this.state.modal === 'revoke' ? tr("撤回联网授权?") : this.state.modal === 'previews' ? tr("保存远程画面预览?") : this.state.modal === 'clearPreviews' ? tr("清除预览图?") : this.state.modal === 'deleteAnnouncement' ? tr("删除公告?") : tr("删除记录?"))
|
||||
.fontSize(20).fontWeight(600).fontColor(MobileTheme.ink).textAlign(TextAlign.Center).width('100%')
|
||||
Text(this.state.modal === 'notice' || this.state.modal === 'upgrade' ? this.state.notice : this.state.modal === 'disconnect' ? '断开后将返回首页。' :
|
||||
this.state.modal === 'previews' ? '预览可能包含远程设备上的敏感内容。仅保存在本机,从下一次连接起生效,可随时关闭并清除。' :
|
||||
this.state.modal === 'clearPreviews' ? '将清除此设备保存的所有远程画面预览,保留连接记录和访问密码。' :
|
||||
this.state.modal === 'revoke' ? '将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。' :
|
||||
this.state.modal === 'delete' ? '同时删除该设备保存的密码和预览。' : '此公告将从列表中移除。')
|
||||
Text(this.state.modal === 'notice' || this.state.modal === 'upgrade' ? this.state.notice : this.state.modal === 'disconnect' ? tr("断开后将返回首页。") :
|
||||
this.state.modal === 'previews' ? tr("预览可能包含远程设备上的敏感内容。仅保存在本机,从下一次连接起生效,可随时关闭并清除。") :
|
||||
this.state.modal === 'clearPreviews' ? tr("将清除此设备保存的所有远程画面预览,保留连接记录和访问密码。") :
|
||||
this.state.modal === 'revoke' ? tr("将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。") :
|
||||
this.state.modal === 'delete' ? tr("同时删除该设备保存的密码和预览。") : tr("此公告将从列表中移除。"))
|
||||
.fontSize(14).lineHeight(21).fontColor(MobileTheme.secondary).textAlign(TextAlign.Center).width('100%')
|
||||
}.padding(24).width('100%')
|
||||
this.Rule()
|
||||
if (this.state.modal === 'notice' || this.state.modal === 'upgrade') { this.Action('知道了', () => this.dismissModal()) }
|
||||
if (this.state.modal === 'notice' || this.state.modal === 'upgrade') { this.Action(tr("知道了"), () => this.dismissModal()) }
|
||||
else {
|
||||
Row() {
|
||||
Button('取消', { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||
Button(tr("取消"), { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
|
||||
.backgroundColor(Color.White).onClick(() => this.dismissModal())
|
||||
Divider().vertical(true).height(50).strokeWidth(0.5).color(MobileTheme.line)
|
||||
Button(this.state.modal === 'previews' ? '开启' : this.state.modal === 'disconnect' ? '断开连接' : this.state.modal === 'revoke' ? '撤回授权' : this.state.modal === 'clearPreviews' ? '清除' : '删除', { type: ButtonType.Normal })
|
||||
Button(this.state.modal === 'previews' ? tr("开启") : this.state.modal === 'disconnect' ? tr("断开连接") : this.state.modal === 'revoke' ? tr("撤回授权") : this.state.modal === 'clearPreviews' ? tr("清除") : tr("删除"), { type: ButtonType.Normal })
|
||||
.fontSize(16).fontWeight(600).fontColor(this.state.modal === 'previews' ? MobileTheme.blue : MobileTheme.red).height(50).layoutWeight(1)
|
||||
.backgroundColor(Color.White).onClick(() => this.confirmModal())
|
||||
}.width('100%')
|
||||
@@ -637,11 +640,11 @@ struct Mobile {
|
||||
else {
|
||||
Column() {
|
||||
Row({ space: 0 }) {
|
||||
MobileIconButton({ icon: 'back', label: '返回', iconExtent: 44, iconSize: 22, action: () => this.back() })
|
||||
MobileIconButton({ icon: 'back', label: tr("返回"), iconExtent: 44, iconSize: 22, action: () => this.back() })
|
||||
Text(this.title()).fontSize(17).fontWeight(600).fontColor(MobileTheme.ink).layoutWeight(1).textAlign(TextAlign.Center)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
if (this.state.page === 'announcements') {
|
||||
MobileIconButton({ icon: 'refresh', label: '刷新公告', iconExtent: 44, iconSize: 22, action: () => this.controller?.inbox.refresh() })
|
||||
MobileIconButton({ icon: 'refresh', label: tr("刷新公告"), iconExtent: 44, iconSize: 22, action: () => this.controller?.inbox.refresh() })
|
||||
} else { Blank().width(44) }
|
||||
}.width('100%').constraintSize({ maxWidth: 720 }).height(56).padding({ left: 12, right: 12 })
|
||||
if (this.state.page === 'announcements') {
|
||||
|
||||
Reference in New Issue
Block a user