mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 00:33:06 +08:00
Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25fb6ee051 | ||
|
|
20ede65038 | ||
|
|
bfb2e4a84e | ||
|
|
99bb63f641 | ||
|
|
4506c27e7c | ||
|
|
b07f457e76 | ||
|
|
b4acf0b597 | ||
|
|
9e7848b9dd | ||
|
|
ea3a2e1faa | ||
|
|
d1e4a31efb | ||
|
|
a6658452c2 | ||
|
|
e123d5f2cc | ||
|
|
1687816bde | ||
|
|
a9e3178042 | ||
|
|
0e052e6d7a | ||
|
|
922cd41f70 | ||
|
|
b15925f857 | ||
|
|
1e25ec15e1 | ||
|
|
9f7411e5c0 | ||
|
|
8fd7ca1e8d | ||
|
|
c3ea4eb8fc | ||
|
|
38d0ce7728 | ||
|
|
778c3575cd | ||
|
|
dad8384666 | ||
|
|
9496bc8f1c | ||
|
|
e551e04e8f | ||
|
|
e3b3f78b06 | ||
|
|
5db213d126 | ||
|
|
bdd04603c7 | ||
|
|
f3d8689b55 | ||
|
|
0fb4b031c7 | ||
|
|
7fc21f01bc | ||
|
|
86bae6e1f4 | ||
|
|
c03c8a63d4 | ||
|
|
ec9ac6a7cd | ||
|
|
2a0b14035d | ||
|
|
652378afa8 | ||
|
|
0cbdbb0c96 | ||
|
|
63bc5648c0 | ||
|
|
859aaa29d4 | ||
|
|
cc48eb651f | ||
|
|
ae6f81e83a | ||
|
|
2445f79f92 | ||
|
|
bd7f9505d9 | ||
|
|
4650e48ea3 | ||
|
|
182899d238 | ||
|
|
1d465239cf | ||
|
|
80ea75ff78 | ||
|
|
ff7b954949 | ||
|
|
0fbefd309f | ||
|
|
f269c56e5b | ||
|
|
c6a2f6a35b | ||
|
|
193dcc4be0 | ||
|
|
f8f4aa3cea | ||
|
|
c25e7266e5 | ||
|
|
5e494bfa3b | ||
|
|
f6a2637540 | ||
|
|
621f2ff205 | ||
|
|
5ac6e153c8 | ||
|
|
dd5c572852 | ||
|
|
33d4199f7c | ||
|
|
a6f4df405b | ||
|
|
97960f3a6b | ||
|
|
53c657aa55 | ||
|
|
941de898fb | ||
|
|
6abcafc205 | ||
|
|
171e559b0d | ||
|
|
41f1e29583 | ||
|
|
2ea462764d | ||
|
|
ef02ef2e6c | ||
|
|
e45350319c | ||
|
|
cf98775754 | ||
|
|
1aa23126d2 | ||
|
|
cb2ae207c4 | ||
|
|
a625890b28 | ||
|
|
62ba3693af | ||
|
|
4e5bf66734 | ||
|
|
086da155f0 | ||
|
|
8a7de23c7f | ||
|
|
e7a49361f7 | ||
|
|
9661d56ed5 | ||
|
|
28451e3e03 | ||
|
|
44f19a595b | ||
|
|
9a4661e327 | ||
|
|
7df0b6118e | ||
|
|
b2d3834944 | ||
|
|
7662b80995 | ||
|
|
0b7273d79d | ||
|
|
695dacf87a | ||
|
|
1ef372d27c | ||
|
|
19bbd3b596 | ||
|
|
1b21bb8640 | ||
|
|
fc5fe9ac61 | ||
|
|
a7adf342d7 | ||
|
|
c8d03d9a5e | ||
|
|
d8fe1053c2 | ||
|
|
fe93c7e0e4 | ||
|
|
c61cf176cb | ||
|
|
65a5288986 | ||
|
|
36f7213162 | ||
|
|
acd56dfc29 | ||
|
|
b948422bfe | ||
|
|
42856be8b8 | ||
|
|
02c3952be0 | ||
|
|
34eb2b1fe3 | ||
|
|
0901d61533 | ||
|
|
40670de954 | ||
|
|
33cbfa5b8a | ||
|
|
81430d78d9 | ||
|
|
aed6ad99c2 | ||
|
|
1e2520d472 | ||
|
|
697843cccd | ||
|
|
45bf6ec4ee | ||
|
|
43a0789c1e | ||
|
|
c4756b298b | ||
|
|
47e6385f9a | ||
|
|
9f610bfc9b |
@@ -0,0 +1,3 @@
|
||||
github: kunkundi
|
||||
ko_fi: kunkundi
|
||||
custom: ["https://afdian.com/a/kunkundi"]
|
||||
@@ -0,0 +1,38 @@
|
||||
name: 问题反馈 / Bug report
|
||||
description: 报告 CrossDesk 的错误或异常行为。Report a bug or unexpected behavior in CrossDesk.
|
||||
title: "[Bug] "
|
||||
labels: [bug]
|
||||
assignees: [kunkundi]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
只需描述问题,其余选填。中文或英文均可。
|
||||
Only a problem description is required. Chinese or English is welcome.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 问题描述 / Problem description
|
||||
description: 遇到了什么问题?如方便,也请说明触发操作或错误提示。What went wrong? If possible, include what you were doing or any error message.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: environment
|
||||
attributes:
|
||||
label: 使用环境(选填) / Environment (optional)
|
||||
description: 操作系统和 CrossDesk 版本;连接问题可填写两端信息。OS and CrossDesk version; include both devices for connection issues.
|
||||
placeholder: Windows 11 → macOS 15; CrossDesk x.y.z
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: attachments
|
||||
attributes:
|
||||
label: 截图或日志(选填) / Screenshots or logs (optional)
|
||||
description: |
|
||||
可直接拖入截图、录屏或日志文件,请隐去密码等敏感信息。
|
||||
Drag in screenshots, recordings, or logs. Hide passwords and other sensitive information.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,27 @@
|
||||
name: 需求建议 / Feature request
|
||||
description: 提出 CrossDesk 的新功能或改进建议。Suggest a new feature or improvement for CrossDesk.
|
||||
title: "[Feature] "
|
||||
labels: [enhancement]
|
||||
assignees: [kunkundi]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
只需描述你的建议,其余选填。中文或英文均可。
|
||||
Only a description of your idea is required. Chinese or English is welcome.
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: 你的建议 / Your idea
|
||||
description: 希望新增或改进什么?如方便,也请说明使用场景。What would you like to add or improve? If possible, describe when you would use it.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: references
|
||||
attributes:
|
||||
label: 补充说明(选填) / Additional context (optional)
|
||||
description: 可附截图、参考链接或其他说明。Add screenshots, links, or any other details.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 常见问题 / FAQ
|
||||
url: https://github.com/kunkundi/crossdesk/blob/deskhub/docs/FAQ.md
|
||||
about: 连接、黑屏、系统权限与安装排查。Troubleshoot connections, blank video, permissions, and installation.
|
||||
- name: 使用指南(中文)
|
||||
url: https://github.com/kunkundi/crossdesk/blob/deskhub/docs/USER_GUIDE.md
|
||||
about: 查看连接、桌面墙、移动端、无人值守和自托管的使用说明。
|
||||
- name: User guide (English)
|
||||
url: https://github.com/kunkundi/crossdesk/blob/deskhub/docs/USER_GUIDE_EN.md
|
||||
about: Read about connections, DeskHub, mobile clients, unattended access, and self-hosting.
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: 问题反馈
|
||||
about: 请在此提交问题报告,以便持续优化产品。
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: kunkundi
|
||||
|
||||
---
|
||||
|
||||
**描述问题**
|
||||
清晰简洁地描述遇到的错误。
|
||||
|
||||
**复现步骤**
|
||||
复现该问题的步骤:
|
||||
1. 前往 '...'
|
||||
2. 点击 '....'
|
||||
3. 出现错误
|
||||
|
||||
**预期行为**
|
||||
清晰简洁地描述你期望发生的行为。
|
||||
|
||||
**截图**
|
||||
如果适用,请添加截图以帮助说明问题。
|
||||
|
||||
**桌面端信息(请填写以下内容):**
|
||||
- 操作系统: [例如 Windows 11]
|
||||
- 版本: [例如 v1.1.10]
|
||||
|
||||
**移动端信息(请填写以下内容):**
|
||||
- 设备: [例如 iPhone 17]
|
||||
- 操作系统: [例如 iOS 26.1]
|
||||
- 浏览器: [例如 系统浏览器、Safari]
|
||||
|
||||
**补充信息**
|
||||
在此添加与问题相关的其他上下文内容。
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 请在此提交功能需求或改进建议,以便后续迭代参考。
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: kunkundi
|
||||
|
||||
---
|
||||
|
||||
**功能/改进建议描述**
|
||||
清晰简洁地描述希望新增的功能或改进的内容。
|
||||
|
||||
**使用场景 / 背景**
|
||||
说明该功能或改进的使用场景,以及解决后带来的价值。
|
||||
|
||||
**预期效果**
|
||||
描述你认为最理想的功能表现或改进效果。
|
||||
|
||||
**参考示例(可选)**
|
||||
提供类似功能截图、参考链接或其他说明,帮助更好理解需求。
|
||||
|
||||
**优先级(可选)**
|
||||
- [ ] 高
|
||||
- [ ] 中
|
||||
- [ ] 低
|
||||
|
||||
**补充信息(可选)**
|
||||
其他相关信息或特殊要求。
|
||||
@@ -6,6 +6,10 @@ import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
DOWNLOAD_BASE = "https://downloads.crossdesk.cn/"
|
||||
@@ -22,8 +26,31 @@ ARTIFACTS = {
|
||||
"android-arm64": ("android-arm64(?:-unsigned)?", "apk"),
|
||||
}
|
||||
|
||||
def sign_update(payload, private_key, expected_public_key):
|
||||
"""Sign exact UTF-8 JSON bytes using a PEM Ed25519 key, never shell text."""
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", expected_public_key):
|
||||
raise ValueError("CROSSDESK_UPDATE_PUBLIC_KEY must contain 64 lowercase hex characters")
|
||||
with tempfile.TemporaryDirectory(prefix="crossdesk-sign-") as directory:
|
||||
root = Path(directory)
|
||||
key = root / "key.pem"
|
||||
key.write_text(private_key, encoding="utf-8")
|
||||
key.chmod(0o600)
|
||||
public = subprocess.run(["openssl", "pkey", "-in", str(key), "-pubout", "-outform", "DER"],
|
||||
check=True, capture_output=True).stdout
|
||||
# RFC 8410 SubjectPublicKeyInfo prefix for an Ed25519 public key.
|
||||
if public != bytes.fromhex("302a300506032b6570032100" + expected_public_key):
|
||||
raise ValueError("Update signing key does not match the public key built into clients")
|
||||
data = root / "payload.json"
|
||||
data.write_bytes(payload.encode("utf-8"))
|
||||
signature = subprocess.run(
|
||||
["openssl", "pkeyutl", "-sign", "-rawin", "-inkey", str(key), "-in", str(data)],
|
||||
check=True, capture_output=True).stdout
|
||||
if len(signature) != 64:
|
||||
raise ValueError("Expected an Ed25519 signature")
|
||||
return {"payload": payload, "signature": signature.hex()}
|
||||
|
||||
def version_info(version, fallback_date=None):
|
||||
|
||||
def version_fields(version):
|
||||
version = version.removeprefix("v")
|
||||
base = r"[0-9]+(?:\.[0-9]+){1,3}"
|
||||
for pattern in (
|
||||
@@ -33,20 +60,45 @@ def version_info(version, fallback_date=None):
|
||||
):
|
||||
match = re.fullmatch(pattern, version)
|
||||
if match:
|
||||
fields = match.groupdict()
|
||||
date = fields.get("date")
|
||||
release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat()
|
||||
if date else fallback_date)
|
||||
if not release_date:
|
||||
raise ValueError(f"Release date is missing for {version}")
|
||||
datetime.date.fromisoformat(release_date)
|
||||
return {"latest_version": version, "version": version,
|
||||
"releaseDate": release_date, "patch": int(fields.get("patch") or 0)}
|
||||
return match.groupdict()
|
||||
raise ValueError(f"Invalid release version: {version}")
|
||||
|
||||
|
||||
def version_info(version, fallback_date=None):
|
||||
fields = version_fields(version)
|
||||
version = version.removeprefix("v")
|
||||
date = fields.get("date")
|
||||
release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat()
|
||||
if date else fallback_date)
|
||||
if not release_date:
|
||||
raise ValueError(f"Release date is missing for {version}")
|
||||
datetime.date.fromisoformat(release_date)
|
||||
return {"latest_version": version, "version": version,
|
||||
"releaseDate": release_date, "patch": int(fields.get("patch") or 0)}
|
||||
|
||||
|
||||
def release_order(metadata):
|
||||
"""Match client ordering: zero-padded numeric components, then patch, never date."""
|
||||
version = metadata.get("latest_version") or metadata.get("version")
|
||||
if not version:
|
||||
return None
|
||||
fields = version_fields(version)
|
||||
numbers = tuple(int(part) for part in fields["base"].split("."))
|
||||
inline_patch = fields.get("patch") or "0"
|
||||
patch = int(inline_patch) if len(inline_patch) <= 4 else 0
|
||||
# Legacy manifests may store the effective patch outside the version string.
|
||||
explicit_patch = metadata.get("patch")
|
||||
if isinstance(explicit_patch, str) and re.fullmatch(r"[0-9]+", explicit_patch):
|
||||
explicit_patch = int(explicit_patch)
|
||||
if type(explicit_patch) is int and 0 <= explicit_patch <= 2147483647:
|
||||
patch = explicit_patch
|
||||
return numbers + (0,) * (4 - len(numbers)) + (patch,)
|
||||
|
||||
|
||||
def artifact_downloads(directory):
|
||||
downloads = {}
|
||||
private_key = os.environ.get("CROSSDESK_UPDATE_PRIVATE_KEY", "")
|
||||
public_key = os.environ.get("CROSSDESK_UPDATE_PUBLIC_KEY", "")
|
||||
for path in sorted(Path(directory).iterdir()):
|
||||
if not path.is_file():
|
||||
continue
|
||||
@@ -65,6 +117,19 @@ def artifact_downloads(directory):
|
||||
continue
|
||||
downloads[key] = {"url": DOWNLOAD_BASE + path.name, "filename": path.name,
|
||||
"version": metadata["version"], "releaseDate": metadata["releaseDate"]}
|
||||
with path.open("rb") as artifact:
|
||||
hasher = hashlib.sha256()
|
||||
for chunk in iter(lambda: artifact.read(65536), b""):
|
||||
hasher.update(chunk)
|
||||
digest = hasher.hexdigest()
|
||||
downloads[key].update(size=path.stat().st_size, sha256=digest)
|
||||
if private_key and key.startswith(("windows-", "macos-", "linux-")) and "portable" not in key:
|
||||
payload = json.dumps({
|
||||
"schema": 1, "platform": key, "version": metadata["version"],
|
||||
"url": DOWNLOAD_BASE + path.name, "filename": path.name,
|
||||
"size": path.stat().st_size, "sha256": digest,
|
||||
}, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
downloads[key]["update"] = sign_update(payload, private_key, public_key)
|
||||
break
|
||||
if not downloads:
|
||||
raise ValueError("No supported release artifacts found")
|
||||
@@ -87,10 +152,34 @@ def merge_release(current, tag, name, notes, downloads, published_date=None):
|
||||
if not tag.startswith("v"):
|
||||
raise ValueError(f"Expected a version tag, got {tag!r}")
|
||||
metadata = version_info(tag, published_date)
|
||||
# Clients require the full artifact version to match the advertised
|
||||
# desktop version, including its date and patch spelling.
|
||||
for platform, download in downloads.items():
|
||||
if (platform.startswith(("windows-", "macos-", "linux-"))
|
||||
and download["version"] != metadata["version"]):
|
||||
raise ValueError(
|
||||
f"Desktop release {tag} expects version {metadata['version']}, "
|
||||
f"but {platform} artifact {download['filename']} "
|
||||
f"has version {download['version']}")
|
||||
current_order = release_order(current)
|
||||
if current_order is not None and release_order(metadata) < current_order:
|
||||
current_version = current.get("latest_version") or current.get("version")
|
||||
print(f"Skipping {tag}: older than current desktop release {current_version}")
|
||||
return dict(current)
|
||||
|
||||
accepted_downloads = {}
|
||||
for platform, download in downloads.items():
|
||||
previous_order = release_order(current["downloads"].get(platform, {}))
|
||||
if previous_order is not None and release_order(download) < previous_order:
|
||||
print(f"Skipping {tag} download for {platform}: a newer version is already published")
|
||||
continue
|
||||
accepted_downloads[platform] = download
|
||||
if mobile and not accepted_downloads:
|
||||
return dict(current)
|
||||
|
||||
result = dict(current)
|
||||
result.pop("platforms", None)
|
||||
result["downloads"] = {**current["downloads"], **downloads}
|
||||
result["downloads"] = {**current["downloads"], **accepted_downloads}
|
||||
if not mobile:
|
||||
result.update({**metadata, "releaseName": name, "releaseNotes": notes, "tagName": tag})
|
||||
return result
|
||||
@@ -117,11 +206,16 @@ def main():
|
||||
parser.error("Supply --release-json or --tag, --name and --notes")
|
||||
tag, name, notes = args.tag, args.name, args.notes.read_text()
|
||||
published_date = None
|
||||
current = json.loads(args.current.read_text())
|
||||
current_text = args.current.read_text()
|
||||
current = json.loads(current_text)
|
||||
downloads = artifact_downloads(args.artifacts)
|
||||
result = merge_release(current, tag, name, notes, downloads, published_date)
|
||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(f"Updated {tag}: {', '.join(downloads)}")
|
||||
if result == current:
|
||||
args.output.write_text(current_text)
|
||||
print(f"No changes to version.json for {tag}")
|
||||
else:
|
||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(f"Updated version.json for {tag}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -36,7 +36,6 @@ on:
|
||||
- "apps/android/**"
|
||||
- "apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json"
|
||||
- "apps/ios/licenses/**"
|
||||
- "apps/ios/scripts/generate_third_party_licenses.py"
|
||||
- "deps/submodules/minirtc"
|
||||
- "libs/wire/**"
|
||||
|
||||
@@ -135,14 +134,8 @@ jobs:
|
||||
actions-cache-folder: .xmake-toolchain-cache
|
||||
actions-cache-key: ${{ runner.os }}-${{ runner.arch }}-ubuntu24.04-android-xmake${{ env.XMAKE_MIN_VERSION }}
|
||||
|
||||
- name: Cache Xmake package repository
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/android/.native/global/.xmake/repositories/xmake-repo
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-android-xmake-repo-v1-${{ hashFiles('apps/android/scripts/build_native.py') }}
|
||||
|
||||
# Cache installed packages only. build_native.py checks Xmake >= 3.1.1 and pins
|
||||
# its own package repository; never reuse another platform's build/config.
|
||||
# Repository selection follows desktop Xmake behavior. Keep installed
|
||||
# packages isolated; never reuse another platform's build/config.
|
||||
- name: Cache native dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -155,9 +148,6 @@ jobs:
|
||||
cache-provider: basic
|
||||
cache-read-only: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
- name: Verify shared license catalog
|
||||
run: python3 -B apps/ios/scripts/generate_third_party_licenses.py
|
||||
|
||||
- name: Resolve Android version
|
||||
shell: python
|
||||
env:
|
||||
|
||||
@@ -53,7 +53,6 @@ jobs:
|
||||
EXPECTED_IOS_SDK: "26.5"
|
||||
# Exercise the minimum supported version; local builds may use newer Xmake.
|
||||
XMAKE_MIN_VERSION: "3.1.1"
|
||||
XMAKE_REPO_REVISION: "121109352da272cd1a3d4ed52f2dcda34f839955"
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
@@ -160,17 +159,21 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/ios/.xmake/packages
|
||||
key: "${{ runner.os }}-xmake${{ env.XMAKE_MIN_VERSION }}-deps-ios-arm64-source-v1-xcode26.6-xrepo1211093-min16-${{ github.run_id }}"
|
||||
key: "${{ runner.os }}-xmake${{ env.XMAKE_MIN_VERSION }}-deps-ios-arm64-source-v2-xcode26.6-min16-${{ hashFiles('apps/ios/scripts/build_minirtc_ios.sh', 'xmake.lua', 'deps/submodules/minirtc/xmake.lua', 'deps/submodules/minirtc/thirdparty/**') }}-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake${{ env.XMAKE_MIN_VERSION }}-deps-ios-arm64-source-v1-xcode26.6-xrepo1211093-min16-
|
||||
${{ runner.os }}-xmake${{ env.XMAKE_MIN_VERSION }}-deps-ios-arm64-source-v2-xcode26.6-min16-${{ hashFiles('apps/ios/scripts/build_minirtc_ios.sh', 'xmake.lua', 'deps/submodules/minirtc/xmake.lua', 'deps/submodules/minirtc/thirdparty/**') }}-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Verify iOS license catalog
|
||||
run: python3 -B apps/ios/scripts/generate_third_party_licenses.py
|
||||
- name: Install Python build tools
|
||||
shell: bash
|
||||
run: |
|
||||
# Xmake only accepts the host Python when all three modules exist;
|
||||
# otherwise it falls back to compiling another Python from source.
|
||||
python3 -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
- name: Install xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
@@ -181,19 +184,40 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
xmake --version | tee "${RUNNER_TEMP}/xmake-version.txt"
|
||||
python3 -B apps/ios/scripts/generate_third_party_licenses.py \
|
||||
python3 -B apps/ios/scripts/verify_native_build.py \
|
||||
--xmake-version "${RUNNER_TEMP}/xmake-version.txt"
|
||||
|
||||
- name: Pin xmake package repository
|
||||
# Xcode buffers Run Script output until the phase finishes. Build the
|
||||
# native archives here so dependency downloads and compiler commands are
|
||||
# streamed to Actions; the Xcode phase then reuses the same build products.
|
||||
- name: Build iOS native dependencies
|
||||
timeout-minutes: 60
|
||||
shell: bash
|
||||
env:
|
||||
CONFIGURATION: Release
|
||||
PLATFORM_NAME: iphoneos
|
||||
CURRENT_ARCH: arm64
|
||||
CROSSDESK_NATIVE_VERBOSE: "1"
|
||||
run: |
|
||||
xmake repo -u
|
||||
XMAKE_REPO_DIR="${HOME}/.xmake/repositories/xmake-repo"
|
||||
git -C "${XMAKE_REPO_DIR}" fetch --depth=1 origin "${XMAKE_REPO_REVISION}"
|
||||
git -C "${XMAKE_REPO_DIR}" checkout --detach "${XMAKE_REPO_REVISION}"
|
||||
test "$(git -C "${XMAKE_REPO_DIR}" rev-parse HEAD)" = "${XMAKE_REPO_REVISION}"
|
||||
set -o pipefail
|
||||
(
|
||||
trap 'kill "${SLEEP_PID:-}" 2>/dev/null || true; exit 0' TERM INT
|
||||
while true; do
|
||||
sleep 60 &
|
||||
SLEEP_PID=$!
|
||||
wait "${SLEEP_PID}" || exit 0
|
||||
echo "iOS native build still running at $(date -u +%FT%TZ)"
|
||||
ps -axo pid,ppid,state,etime,pcpu,comm | \
|
||||
awk 'NR == 1 || /xmake|clang|cmake|ninja|meson|python|git|curl|make|zsh/'
|
||||
done
|
||||
) &
|
||||
MONITOR_PID=$!
|
||||
trap 'kill "${MONITOR_PID}" 2>/dev/null || true; wait "${MONITOR_PID}" 2>/dev/null || true' EXIT
|
||||
apps/ios/scripts/build_minirtc_ios.sh 2>&1 | \
|
||||
tee "${RUNNER_TEMP}/ios-native-build.log"
|
||||
|
||||
- name: Build CrossDesk Mobile
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: |
|
||||
DERIVED_DATA="${RUNNER_TEMP}/CrossDeskMobileDerivedData"
|
||||
@@ -209,7 +233,20 @@ jobs:
|
||||
COMPILER_INDEX_STORE_ENABLE=NO \
|
||||
MARKETING_VERSION="${IOS_VERSION_BASE}" \
|
||||
IPHONEOS_DEPLOYMENT_TARGET="${IOS_DEPLOYMENT_TARGET}" \
|
||||
build
|
||||
build 2>&1 | tee "${RUNNER_TEMP}/ios-app-build.log"
|
||||
|
||||
- name: Upload iOS build diagnostics
|
||||
if: ${{ failure() || cancelled() }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ios-build-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
${{ runner.temp }}/ios-native-build.log
|
||||
${{ runner.temp }}/ios-app-build.log
|
||||
~/.xmake/cache/packages/**/logs/*.txt
|
||||
~/.xmake/cache/packages/**/meson-logs/meson-log.txt
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Verify CrossDesk Mobile
|
||||
shell: bash
|
||||
|
||||
+80
-187
@@ -11,6 +11,10 @@ on:
|
||||
- "!android-v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
desktop_version:
|
||||
description: "Desktop version for branch builds, e.g. 1.7.2. Leave blank for 0.0.0; tags use their own version."
|
||||
required: false
|
||||
type: string
|
||||
patch:
|
||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||
required: false
|
||||
@@ -28,11 +32,72 @@ permissions:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
XMAKE_MIN_VERSION: "3.1.1"
|
||||
CROSSDESK_UPDATE_PUBLIC_KEY: ${{ vars.CROSSDESK_UPDATE_PUBLIC_KEY }}
|
||||
CROSSDESK_DESKTOP_VERSION: ${{ inputs.desktop_version }}
|
||||
|
||||
jobs:
|
||||
prepare-version:
|
||||
name: Resolve desktop version
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
# Resolve the date once for every desktop build and the release job.
|
||||
steps:
|
||||
- name: Resolve desktop version
|
||||
id: version
|
||||
shell: bash
|
||||
env:
|
||||
PATCH_NUMBER: ${{ inputs.patch || '0' }}
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
if [[ "${GITHUB_REF}" == refs/heads/* ]]; then
|
||||
VERSION_REF="${CROSSDESK_DESKTOP_VERSION:-0.0.0}"
|
||||
if [[ ! "${VERSION_REF}" =~ ^v?[0-9]+(\.[0-9]+){1,3}$ ]]; then
|
||||
echo "::error::desktop_version must be a numeric version such as 1.7.2"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ -n "${CROSSDESK_DESKTOP_VERSION}" ]]; then
|
||||
echo "::error::desktop_version is only supported for branch builds; tags determine release versions"
|
||||
exit 1
|
||||
fi
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "version=${VERSION_NUM}" >> "$GITHUB_OUTPUT"
|
||||
echo "Desktop version: ${VERSION_NUM}"
|
||||
|
||||
build-linux:
|
||||
name: Build Linux (${{ matrix.arch }})
|
||||
needs: prepare-version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
LEGAL_VERSION: ${{ needs.prepare-version.outputs.version }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -52,59 +117,6 @@ jobs:
|
||||
options: --user root
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
BUILD_DATE_OVERRIDE=""
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set legal Debian version
|
||||
shell: bash
|
||||
run: |
|
||||
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
|
||||
if [[ -z "${BUILD_DATE}" ]]; then
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
|
||||
VERSION_BASE="0.0.0-${VERSION_BASE}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -202,8 +214,10 @@ jobs:
|
||||
# macOS
|
||||
build-macos:
|
||||
name: Build on macOS
|
||||
needs: prepare-version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
EXPECTED_MACOS_SDK: "26.5"
|
||||
strategy:
|
||||
@@ -223,46 +237,6 @@ jobs:
|
||||
package_script: ./apps/desktop/scripts/macos/pkg_arm64.sh
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
echo "VERSION_NUM=${VERSION_NUM}"
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
|
||||
- name: Select Xcode 26.6
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -354,50 +328,12 @@ jobs:
|
||||
# Windows
|
||||
build-windows-x64:
|
||||
name: Build on Windows x64
|
||||
needs: prepare-version
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
VERSION_NUM: ${{ needs.prepare-version.outputs.version }}
|
||||
XMAKE_GLOBALDIR: D:\xmake_global
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ref = $env:GITHUB_REF
|
||||
$version = $ref -replace '^refs/(tags|heads)/', ''
|
||||
$version = $version -replace '^v', ''
|
||||
$version = $version -replace '/', '-'
|
||||
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
|
||||
$PATCH_NUMBER = "${{ github.event.inputs.patch }}"
|
||||
|
||||
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
|
||||
$PATCH_NUMBER = "0"
|
||||
}
|
||||
|
||||
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
$BUILD_DATE = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
$PATCH_NUMBER = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
}
|
||||
|
||||
if ($PATCH_NUMBER -ne "0") {
|
||||
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
|
||||
} else {
|
||||
$VERSION_NUM = "v$version-$BUILD_DATE"
|
||||
}
|
||||
|
||||
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -598,7 +534,7 @@ jobs:
|
||||
release:
|
||||
name: Publish Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build-linux, build-macos, build-ios, build-android, sign-android, build-windows-x64]
|
||||
needs: [prepare-version, build-linux, build-macos, build-ios, build-android, sign-android, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: publish-release-metadata
|
||||
@@ -613,50 +549,6 @@ jobs:
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
VERSION_WITH_V="v${VERSION_NUM}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare release notes
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/release-notes.md" << EOF
|
||||
@@ -686,18 +578,18 @@ jobs:
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
||||
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
||||
cp artifacts/crossdesk-macos-x64-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-macos-x64-${{ needs.prepare-version.outputs.version }}.pkg
|
||||
cp artifacts/crossdesk-macos-arm64-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-macos-arm64-${{ needs.prepare-version.outputs.version }}.pkg
|
||||
for ARCH in amd64 arm64; do
|
||||
for PACKAGE in crossdesk crossdesk-virtual-desktop-xfce; do
|
||||
FILE="${PACKAGE}-linux-${ARCH}-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
|
||||
cp "artifacts/crossdesk-linux-${ARCH}-${{ steps.version.outputs.VERSION_WITH_V }}/${FILE}" "release/${FILE}"
|
||||
FILE="${PACKAGE}-linux-${ARCH}-${{ needs.prepare-version.outputs.version }}.deb"
|
||||
cp "artifacts/crossdesk-linux-${ARCH}-${{ needs.prepare-version.outputs.version }}/${FILE}" "release/${FILE}"
|
||||
done
|
||||
done
|
||||
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
|
||||
cp artifacts/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}/*.apk release/crossdesk-android-arm64-${{ needs.build-android.outputs.version }}.apk
|
||||
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
||||
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||
cp artifacts/crossdesk-win-x64-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-win-x64-${{ needs.prepare-version.outputs.version }}.exe
|
||||
cp artifacts/crossdesk-win-x64-portable-${{ needs.prepare-version.outputs.version }}/* release/crossdesk-win-x64-portable-${{ needs.prepare-version.outputs.version }}.zip
|
||||
|
||||
- name: List release files
|
||||
run: ls -lh release/
|
||||
@@ -705,10 +597,10 @@ jobs:
|
||||
- name: Upload to Versioned GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
tag_name: ${{ needs.prepare-version.outputs.version }}
|
||||
# The pushed tag already determines the release source commit.
|
||||
target_commitish: ${{ github.event.repository.default_branch }}
|
||||
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
name: Release ${{ needs.prepare-version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: release/*
|
||||
@@ -751,7 +643,8 @@ jobs:
|
||||
|
||||
- name: Generate version.json for all platforms
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
RELEASE_TAG: ${{ needs.prepare-version.outputs.version }}
|
||||
CROSSDESK_UPDATE_PRIVATE_KEY: ${{ secrets.CROSSDESK_UPDATE_PRIVATE_KEY }}
|
||||
run: |
|
||||
python3 .github/scripts/update_version_manifest.py \
|
||||
--current current-version.json --output version.json --artifacts release \
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: "Android artifact version, for example v1.6.0-20261002."
|
||||
description: "Android artifact version, for example v1.7.3-20261026 or v1.7.3-1-20261026."
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_tag:
|
||||
description: "Existing Android release to sign without rebuilding, for example android-v1.6.0-20261002."
|
||||
description: "Existing Android release to sign without rebuilding, for example android-v1.7.3-1-20261026."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -61,11 +61,11 @@ jobs:
|
||||
if os.environ["GITHUB_EVENT_NAME"] == "workflow_dispatch" and not version:
|
||||
tag = os.environ["SOURCE_TAG"]
|
||||
if not tag.startswith("android-v"):
|
||||
raise ValueError("Use an existing android-v<app-version>-YYYYMMDD release")
|
||||
raise ValueError("Use an existing android-v<app-version>[-<patch>]-YYYYMMDD release")
|
||||
version = tag.removeprefix("android-")
|
||||
match = re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+-([0-9]{8})", version)
|
||||
match = re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9]+)?-([0-9]{8})", version)
|
||||
if not match:
|
||||
raise ValueError("Invalid Android artifact version")
|
||||
raise ValueError("Invalid Android artifact version; expected v<app-version>[-<patch>]-YYYYMMDD")
|
||||
datetime.datetime.strptime(match[1], "%Y%m%d")
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as stream:
|
||||
stream.write(f"version={version}\n")
|
||||
|
||||
@@ -70,6 +70,9 @@ jobs:
|
||||
|
||||
- name: Merge release version and download URLs
|
||||
shell: bash
|
||||
env:
|
||||
CROSSDESK_UPDATE_PUBLIC_KEY: ${{ vars.CROSSDESK_UPDATE_PUBLIC_KEY }}
|
||||
CROSSDESK_UPDATE_PRIVATE_KEY: ${{ secrets.CROSSDESK_UPDATE_PRIVATE_KEY }}
|
||||
run: |
|
||||
python3 .github/scripts/update_version_manifest.py \
|
||||
--current current-version.json --output version.json --artifacts release \
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
CrossDesk
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
|
||||
+17
-9
@@ -1,18 +1,18 @@
|
||||
# CrossDesk Privacy Policy
|
||||
|
||||
Last updated: October 10, 2026
|
||||
Last updated: October 15, 2026
|
||||
|
||||
[中文](#中文) | [English](#english)
|
||||
|
||||
## 中文
|
||||
|
||||
更新日期:2026 年 10 月 10 日
|
||||
更新日期:2026 年 10 月 15 日
|
||||
|
||||
CrossDesk 重视您的个人信息与隐私保护。本政策说明我们在提供远程桌面服务时如何处理相关信息,以及您可以如何管理授权、保存的内容和数据请求。请在使用前阅读并了解本政策。
|
||||
|
||||
### 一、适用范围
|
||||
|
||||
本政策适用于 CrossDesk 桌面客户端、iOS 与 Android 客户端,以及由 CrossDesk 项目维护者(以下简称“我们”)提供的默认网络服务。
|
||||
本政策适用于 CrossDesk 桌面客户端、iOS、Android 与鸿蒙移动客户端,以及由 CrossDesk 项目维护者(以下简称“我们”)提供的默认网络服务。
|
||||
|
||||
如果您选择自行部署或使用其他运营者提供的服务器,该服务器所处理的信息、保存期限及安全管理由相应运营者负责。请在使用前了解其隐私政策。本政策中有关客户端本地数据处理的说明仍适用。
|
||||
|
||||
@@ -50,11 +50,13 @@ Android 中的设备身份及您选择保存的访问密码使用 Android Keysto
|
||||
|
||||
iOS 通过系统钥匙串和文件保护机制保护相应本地数据,默认信令连接进行 TLS 证书校验。请妥善保管设备和连接凭据,避免向无关人员共享访问密码。
|
||||
|
||||
Android 默认信令连接同样进行 TLS 证书校验。
|
||||
Android 与鸿蒙移动端默认信令连接同样进行 TLS 证书校验。
|
||||
|
||||
鸿蒙移动端使用系统 Asset Store 加密保存设备身份及您选择保存的访问密码,明确禁止凭据跨设备同步。设置和连接记录按服务器隔离。远程预览默认关闭,单独确认后从下一次连接起保存一张 640×360 预览;预览保存在应用私有缓存中,不上传,可随时清除,也可能被系统回收。关闭预览或撤回联网授权会清除预览。接收文件保存在应用私有目录,您可在会话中通过系统文件选择器导出。
|
||||
|
||||
### 五、移动端权限与授权选择
|
||||
|
||||
**隐私授权。** iOS 与 Android 首次使用,或从未提供此选择的旧版升级时,您需阅读本政策、主动勾选并点击同意后,客户端才会登记本机身份、连接信令服务和查询设备在线状态。拒绝后仍可查看设置、离线隐私政策和本地连接记录,远程连接功能暂不可用。未授权时,重新打开或从后台返回应用会再次提示。
|
||||
**隐私授权。** iOS、Android 与鸿蒙移动端 首次使用,或从未提供此选择的旧版升级时,您需阅读本政策、主动勾选并点击同意后,客户端才会登记本机身份、连接信令服务和查询设备在线状态。拒绝后仍可查看设置、离线隐私政策和本地连接记录,远程连接功能暂不可用。未授权时,重新打开或从后台返回应用会再次提示。
|
||||
|
||||
**撤回同意。** 您可以在“设置 → 隐私 → 隐私与授权”中查看隐私政策、授权状态或撤回授权。客户端将停止远程会话、信令连接和版本检查,关闭并清除本地画面预览。再次使用连接功能前需重新同意;重新同意不会自动开启画面预览保存。撤回不影响撤回前已经发生的信息处理,也不等同于删除服务端数据。
|
||||
|
||||
@@ -64,6 +66,8 @@ Android 客户端同样作为控制端接收远程画面与音频,不采集本
|
||||
|
||||
Android 远程会话优先使用系统画中画在切换应用后继续显示画面。画中画不可用时,通过前台服务和连接通知保持您已建立的远程连接;后台暂停本地画面渲染和音频播放,不申请悬浮窗权限。通知权限用于提供返回及断开入口,CPU 唤醒锁用于会话后台期间维持连接,返回前台或会话结束时释放。您可以在应用或连接通知中断开,移除应用任务也会结束会话及保活服务。
|
||||
|
||||
鸿蒙手机和平板只作为控制端,不申请本机录屏、输入注入、摄像头或麦克风权限;文件选择和剪贴板操作由您主动触发。已建立会话在切入后台时优先使用系统画中画,提供静音控制;画中画不可用时申请数据传输后台任务并显示系统通知。若系统不允许或停止后台任务且画中画不可用,会话会断开。后台切换释放远端按键;空闲后台不维持信令连接。完成联网授权后,应用在前台检查鸿蒙本机版本,并在连接后检查远端版本;版本请求不携带设备身份或远端版本。
|
||||
|
||||
应用及通信组件使用本地文件元数据、应用偏好和计时接口实现文件传输、设置保存及网络计时,不使用这些信息制作设备指纹或进行广告追踪。
|
||||
|
||||
### 六、查询与删除请求
|
||||
@@ -86,13 +90,13 @@ Android 远程会话优先使用系统画中画在切换应用后继续显示画
|
||||
|
||||
## English
|
||||
|
||||
Updated: October 2, 2026
|
||||
Updated: October 15, 2026
|
||||
|
||||
CrossDesk respects your privacy. This policy explains how information is processed when we provide remote desktop services and how you can manage consent, locally saved content and data requests. Please read this policy before using the software.
|
||||
|
||||
### 1. Scope
|
||||
|
||||
This policy applies to the CrossDesk desktop, iOS and Android clients and the default network services provided by the CrossDesk project maintainers ("we" or "us").
|
||||
This policy applies to the CrossDesk desktop, iOS, Android and HarmonyOS mobile clients and the default network services provided by the CrossDesk project maintainers ("we" or "us").
|
||||
|
||||
If you deploy your own server or choose a server provided by another operator, that operator is responsible for its data processing, retention and security practices. Please review its privacy policy before use. The descriptions of local client data processing in this policy still apply.
|
||||
|
||||
@@ -130,11 +134,13 @@ On Android, device identities and access passwords you choose to save are encryp
|
||||
|
||||
On iOS, the system Keychain and file protection mechanisms protect the relevant local data, and default signaling connections validate TLS certificates. Keep your devices and connection credentials secure and avoid sharing access passwords with unrelated people.
|
||||
|
||||
Default signaling connections on Android also validate TLS certificates.
|
||||
Default signaling connections on Android and HarmonyOS mobile also validate TLS certificates.
|
||||
|
||||
HarmonyOS mobile uses the system Asset Store to encrypt device identities and saved access passwords, with cross-device credential synchronization disabled. Settings and connection records are scoped to the server. Remote previews are off by default. After separate confirmation, one 640×360 preview is saved per connection starting with the next connection. Previews remain in private app cache, are never uploaded, can be cleared and may be reclaimed by the system. Disabling previews or withdrawing network consent clears them. Received files remain in private app storage and can be exported through the system document picker during the session.
|
||||
|
||||
### 5. Mobile permissions and consent choices
|
||||
|
||||
**Privacy authorization.** On first use of iOS or Android, or when upgrading from a version without this choice, you must read this policy, actively select the checkbox and agree before the client registers a local identity, connects to signaling or queries device presence. If you decline, settings, the offline policy and local connection history remain available, while remote connections are unavailable. Without consent, reopening the app or returning from the background presents the notice again.
|
||||
**Privacy authorization.** On first use of iOS, Android or HarmonyOS mobile, or when upgrading from a version without this choice, you must read this policy, actively select the checkbox and agree before the client registers a local identity, connects to signaling or queries device presence. If you decline, settings, the offline policy and local connection history remain available, while remote connections are unavailable. Without consent, reopening the app or returning from the background presents the notice again.
|
||||
|
||||
**Withdrawal.** You can review the privacy policy and your authorization status or withdraw consent in Settings → Privacy → Privacy and Authorization. The client stops remote sessions, signaling and version checks, disables preview saving and clears local previews. Consent is required again before reconnecting. Agreeing again does not automatically enable preview saving. Withdrawal does not undo processing that already occurred or delete server data.
|
||||
|
||||
@@ -144,6 +150,8 @@ The Android client also receives remote screens and audio as a controller and do
|
||||
|
||||
Android remote sessions prefer system picture-in-picture to continue displaying video when you switch apps. If picture-in-picture is unavailable, a foreground service and connection notification maintain the remote connection you have established. Local video rendering and audio playback pause in the background; no overlay permission is requested. Notification permission provides return and disconnect actions. A CPU wake lock helps maintain the connection while the session is backgrounded and is released on foreground return or session end. You can disconnect in the app or its connection notification; removing the app task also ends the session and its foreground service.
|
||||
|
||||
HarmonyOS phones and tablets act only as controllers and do not request local screen recording, input injection, camera or microphone permissions. File selection and clipboard actions are user initiated. An established session prefers system picture-in-picture with a mute control when backgrounded. If unavailable, the app requests a data-transfer background task with a system notification. The session ends if the system disallows or stops that task while picture-in-picture is unavailable. Backgrounding releases remote keys; idle background sessions do not retain signaling. After network consent, the foreground app checks the local HarmonyOS version and checks the remote version after connection. These requests contain neither device identity nor the remote version.
|
||||
|
||||
The app and its communication components use local file metadata, preferences and timing APIs for file transfer, settings and network timing, not for device fingerprinting or advertising tracking.
|
||||
|
||||
### 6. Access and deletion requests
|
||||
|
||||
@@ -1,25 +1,83 @@
|
||||
<div align="center">
|
||||
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk 标志" />
|
||||
<h1>CrossDesk</h1>
|
||||
<img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk 标志:图标在左,双色文字在右" />
|
||||
<p>轻量、跨平台的远程桌面,支持电脑、浏览器和移动端连接,用桌面墙集中查看与操控多台设备。</p>
|
||||
<p><a href="https://www.crossdesk.cn/">官方网站</a> · <a href="https://github.com/kunkundi/crossdesk/releases">下载客户端</a> · <a href="https://web.crossdesk.cn/">打开 Web 客户端</a> · <a href="README_EN.md">English</a></p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](#download)
|
||||
[](https://github.com/kunkundi/crossdesk/releases)
|
||||
[](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
[](#download)
|
||||
|
||||
[](https://github.com/kunkundi/crossdesk/releases)
|
||||
[](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
|
||||
[](https://afdian.com/a/kunkundi)
|
||||
[](https://ko-fi.com/kunkundi)
|
||||
[](https://github.com/sponsors/kunkundi)
|
||||
|
||||
</div>
|
||||
|
||||
CrossDesk 支持 Windows、macOS、Linux 之间的远程访问,也可从浏览器或原生 iOS / Android 客户端控制电脑。桌面端新增 **桌面墙(DeskHub)**,可在独立窗口中同时查看多台远程桌面,按分组管理设备,并进行批量操作与群控。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),提供实时音视频传输、键鼠控制、文件传输与自托管能力,仍在持续开发中。
|
||||
CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过 **桌面墙(DeskHub)** 集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。
|
||||
|
||||
> 本文按当前仓库代码整理。截图展示当前 Slint 桌面界面,使用演示 ID、密码、在线状态和连接记录;缩略图来自公开页面;下载包的功能与系统要求请同时核对对应 Release 说明。原生 iOS 客户端的构建、签名方式见下文。
|
||||
[主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [IP 直连](#lan-direct) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor)
|
||||
|
||||
[界面预览](#preview) · [下载与安装](#download) · [快速连接](#quick-start) · [桌面墙](#deskhub) · [会话操作](#session) · [设置](#settings) · [iOS](#ios) · [Android](#android) · [自托管](#self-hosting) · [源码构建](docs/BUILD.md) · [常见问题](docs/FAQ.md)
|
||||
<a id="features"></a>
|
||||
|
||||
## 主要功能
|
||||
|
||||
- **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。
|
||||
- **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。
|
||||
- **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。
|
||||
- **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。
|
||||
- **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。
|
||||
- **部署与运维:** P2P 直连、TURN 中继、SRTP 媒体加密、自托管,以及 Windows 登录前远控和 Linux 无头运行。
|
||||
|
||||
<a id="download"></a>
|
||||
|
||||
## 下载与平台支持
|
||||
|
||||
从 [官方网站](https://www.crossdesk.cn/) 或 [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) 下载与你的系统和 CPU 架构匹配的安装包;也可直接打开 [Web 客户端](https://web.crossdesk.cn/)。
|
||||
|
||||
| 平台 | 支持范围与安装要求 |
|
||||
| --- | --- |
|
||||
| Windows | **控制 / 被控**;Windows 10+,x64;`.exe` 安装包,也支持便携构建 |
|
||||
| macOS | **控制 / 被控**;macOS 14.0+;按 Intel / Apple Silicon 选择 x64 / arm64 `.pkg` |
|
||||
| Linux | **控制 / 被控**;Ubuntu 20.04+、glibc 2.31 基线;amd64 / arm64 `.deb` |
|
||||
| iOS / iPadOS | **仅控制**;iOS 16.0+;发布包为未签名应用,需自行签名,或使用 Xcode 构建安装 |
|
||||
| Android | **仅控制**;Android 8.0+,arm64;下载已签名 APK 安装,或自行构建 |
|
||||
| 鸿蒙 PC(开发版) | **控制 / 被控**;API 26,arm64;使用 DevEco 构建并签名 HAP,仍在进行真机验收 |
|
||||
| 鸿蒙手机 / 平板(开发版) | **仅控制**;API 20+,arm64;使用 DevEco 构建并签名 HAP,仍在进行真机验收 |
|
||||
| Web | **仅控制**;支持 WebRTC 的浏览器;无需在控制端安装客户端 |
|
||||
|
||||
系统要求按当前源码与构建配置整理,具体安装包以对应 Release 说明为准。各客户端的功能范围见 [移动端与鸿蒙](#mobile)。
|
||||
|
||||
- **Linux:** 已有桌面选普通版 `crossdesk-linux-…deb`;需要独立 Xfce 桌面的服务器选 `crossdesk-virtual-desktop-xfce-…deb`。两者选择其一,安装与启动方法见 [使用指南](docs/USER_GUIDE.md#installation) 和 [无头运行](docs/HEADLESS.md)。
|
||||
- **macOS:** 首次运行时按提示授予屏幕录制及辅助功能权限;前者用于桌面采集,后者用于远程键鼠输入。[权限说明](docs/USER_GUIDE.md#installation)
|
||||
- **iOS / 鸿蒙:** 签名与构建步骤分别见 [iOS](apps/ios/README.md) 和 [鸿蒙](apps/harmony/README.md),不要将未签名产物当作可直接安装的应用。
|
||||
|
||||
<a id="quick-start"></a>
|
||||
|
||||
## 快速连接
|
||||
|
||||
### 通过设备 ID 连接
|
||||
|
||||
1. **准备被控电脑。** 安装并运行 CrossDesk,切换至 ID 连接模式,等待显示“已连接服务器”,查看“本桌面”中的本机 ID 和密码。
|
||||
2. **发起连接。** 在另一台电脑的 CrossDesk 中输入被控电脑的 9 位 ID,点击“连接”;也可使用原生移动端或 [Web 客户端](https://web.crossdesk.cn/)。
|
||||
3. **验证密码。** 输入被控电脑当前的连接密码,等待远程画面出现。桌面端可勾选“记住密码”,后续从“近期连接”重连。
|
||||
|
||||
使用自建服务器时,两端需要接入同一服务。更多连接、密码及浏览器操作见 [使用指南](docs/USER_GUIDE.md#quick-start);连接失败、黑屏或无法操作键鼠时先查阅 [常见问题](docs/FAQ.md)。
|
||||
|
||||
<a id="lan-direct"></a>
|
||||
|
||||
### 通过局域网 IP 直连
|
||||
|
||||
1. **切换模式。** 两台电脑均运行支持 IP 直连的桌面客户端,点击左上角模式按钮切换到 **IP 直连模式**。若按钮未显示,在 **设置 → 常规 → 网络连接** 中勾选 **启用 IP 直连** 并保存。
|
||||
2. **填写地址。** 在控制端输入被控电脑显示的 `IP:端口`,例如 `192.168.1.20:52888`,点击“连接”。只输入 IP 时使用默认端口 **52888**;请以对方界面显示的实际端口为准。
|
||||
3. **输入密码。** 输入被控电脑在 IP 直连模式下显示的 **6 位英文字母或数字密码**,可勾选“记住密码”。切换模式时,“近期连接”同步切换为对应模式的设备记录。
|
||||
|
||||
两台电脑需要在局域网内可互相访问;网络可以有外网,也可以没有外网,直连会话不依赖云端信令服务。主窗口可直接切换本机 IP、修改各 IP 的端口和密码;关闭设置中的“启用 IP 直连”会停止监听、断开 IP 直连会话并隐藏模式按钮。详见 [IP 直连使用指南](docs/USER_GUIDE.md#lan-direct)。
|
||||
|
||||
<a id="preview"></a>
|
||||
|
||||
@@ -27,301 +85,113 @@ CrossDesk 支持 Windows、macOS、Linux 之间的远程访问,也可从浏览
|
||||
|
||||
### 桌面客户端
|
||||
|
||||
主窗口以“本桌面”和“远程桌面”两张卡片展示本机凭据与连接入口,下方是可按分组筛选的近期连接。左上角打开桌面墙,右上角菜单进入设置、消息与关于。
|
||||
本机凭据、远程连接和近期设备集中在主窗口,左上角提供连接模式切换和桌面墙入口。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-main-zh.png" width="800" alt="新版主窗口:桌面墙入口、本机凭据、远程连接卡片和近期连接分组筛选;数据为演示" />
|
||||
<img src="docs/images/desktop-main-zh.png" width="800" alt="CrossDesk ID 连接模式:模式切换、本机 ID、远程连接入口和近期设备;数据为演示" />
|
||||
</p>
|
||||
|
||||
### 桌面墙(DeskHub)
|
||||
### IP 直连模式
|
||||
|
||||
在独立窗口中同时查看多台设备,支持分组、可调整大小的网格、精简模式、全屏与批量操作。点击画面可放大查看,再按需接管操控。
|
||||
切换本机 IP、修改端口与密码,通过对方地址连接;下方展示独立的局域网近期设备。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-deskhub-zh.png" width="960" alt="桌面墙网格:六台演示设备、分组筛选、添加、选择、精简模式和网络状态入口" />
|
||||
<img src="docs/images/desktop-lan-zh.png" width="800" alt="CrossDesk IP 直连模式:本机 IP 和端口编辑入口、六位密码、远端地址输入和局域网近期设备;数据为演示" />
|
||||
</p>
|
||||
|
||||
### Web 客户端
|
||||
|
||||
在浏览器中输入远程设备 ID 和密码,无需在控制端安装桌面客户端。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/web-client.png" width="800" alt="CrossDesk Web 客户端连接首页(2026-09-25 截图)" />
|
||||
</p>
|
||||
|
||||
图片来源与更新方式见 [截图说明](docs/images/README.md)。
|
||||
|
||||
## 主要功能
|
||||
|
||||
| 能力 | 当前支持 |
|
||||
| --- | --- |
|
||||
| 跨平台控制 | Windows / macOS / Linux 桌面互控;浏览器与原生 iOS / Android 作为控制端 |
|
||||
| 实时画面与声音 | H.264 / AV1、硬件编解码、远端声音播放;会话中可调整画质、30 / 60 fps 与画面偏好 |
|
||||
| 多设备与多显示器 | 近期连接、设备别名与分组筛选、会话标签与远端显示器切换 |
|
||||
| 桌面墙(DeskHub) | 多桌面实时预览、分组与拖动排序、放大接管、群控、批量连接 / 断开 / 静音 / 文件分发;可配置分辨率、帧率与视频总带宽 |
|
||||
| 输入与协作 | 键鼠输入、远端光标同步、组合键、文本剪贴板同步、文件传输 |
|
||||
| 网络与部署 | P2P 直连、TURN 中继与强制中继、SRTP 媒体加密、自托管信令与中继服务 |
|
||||
| 隐私屏 | 被连接时自动开启,或在会话工具栏切换;可用性取决于被控端系统与采集方式 |
|
||||
| Windows 锁屏控制 | 通过 CrossDesk Service 支持锁屏、登录界面及安全桌面的输入转发 |
|
||||
| Linux 无头运行 | 复用已有桌面和应用,或创建独立 Xvfb 虚拟桌面;终端控制台可查看状态、修改密码和设置,见 [运行说明](docs/HEADLESS.md) |
|
||||
|
||||
各客户端的入口和能力有所不同;桌面会话操作见下文,iOS 的具体功能见 [开发说明](apps/ios/README.md)。
|
||||
|
||||
<a id="download"></a>
|
||||
|
||||
## 下载与安装
|
||||
|
||||
前往 [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) 或 [官方网站](https://www.crossdesk.cn/),按系统与 CPU 架构选择安装包。
|
||||
|
||||
| 平台 | 当前源码 / CI 基线 | 安装方式 |
|
||||
| --- | --- | --- |
|
||||
| Windows | Windows 10+,x64 | `.exe` 安装包;便携构建也受支持 |
|
||||
| macOS | macOS 14.0+,Intel / Apple Silicon | 选择 x64 或 arm64 的 `.pkg` |
|
||||
| Linux | Ubuntu 20.04+,amd64 / arm64,glibc 2.31 基线 | 普通版 / Xfce 虚拟桌面版 `.deb` |
|
||||
| iOS / iPadOS | iOS 16.0+,arm64 真机 | 原生客户端;使用 Xcode 构建并签名,CI 产物为未签名应用 |
|
||||
| Android | Android 8.0+,arm64 | 原生控制端;使用 Gradle 构建 APK,见 [开发说明](apps/android/README.md) |
|
||||
| Web | 支持 WebRTC 的浏览器 | 访问 [web.crossdesk.cn](https://web.crossdesk.cn/) |
|
||||
|
||||
Linux 每个架构提供两个可独立安装的包,**选择其中一个**:
|
||||
|
||||
| 安装包 | 适用场景 |
|
||||
| --- | --- |
|
||||
| `crossdesk-linux-<arch>-<version>.deb`(推荐) | 已有桌面的电脑;不会额外安装 Xfce 或 Xvfb |
|
||||
| `crossdesk-virtual-desktop-xfce-linux-<arch>-<version>.deb` | 需要创建独立桌面的服务器;APT 会额外安装 **Xfce、Xvfb 和 D-Bus 会话工具** |
|
||||
|
||||
普通版安装示例(将文件名替换为实际下载名称,arm64 选择对应架构):
|
||||
|
||||
```bash
|
||||
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
|
||||
```
|
||||
|
||||
如需虚拟桌面版,改为安装:
|
||||
|
||||
```bash
|
||||
sudo apt install "./crossdesk-virtual-desktop-xfce-linux-amd64-<version>.deb"
|
||||
```
|
||||
|
||||
两个包包含相同的完整程序,互相替换并保留用户配置;普通版自行补齐依赖后也能创建虚拟桌面。安装虚拟桌面版不会自动启动会话或切换默认桌面,启动方法见 [Linux 无头运行](docs/HEADLESS.md)。应用更新的下载按钮会选择已安装的包类型。已有版本升级后不会自动删除先前安装的 Xfce。
|
||||
|
||||
**macOS 首次运行:** 按应用提示,在“系统设置 → 隐私与安全性”中授予 CrossDesk **屏幕录制**(新系统可能显示为“屏幕与系统音频录制”)及 **辅助功能**权限,再按系统提示重新打开应用。前者用于捕获桌面,后者用于远程键鼠输入。
|
||||
|
||||
<a id="quick-start"></a>
|
||||
|
||||
## 快速连接
|
||||
|
||||
### 从另一台电脑连接
|
||||
|
||||
1. **准备被控电脑。** 安装并运行 CrossDesk,等待底部显示“已连接服务器”。在左侧“本桌面”查看 **本机 ID** 和 **密码**,提供给控制端。
|
||||
2. **输入对端 ID。** 在控制端右侧“远程桌面 → 对端 ID”输入被控电脑的 **9 位 ID**,点击 **连接**。
|
||||
3. **完成密码验证。** 按弹窗输入被控电脑当前的连接密码,确认后等待远程画面出现。需要保存密码时勾选“记住密码”。
|
||||
4. **再次连接。** 成功连接过的设备会出现在“近期连接”中,可通过卡片重连。悬停卡片可编辑别名、修改分组、删除记录或连接 / 断开;标题右侧可按分组筛选设备。
|
||||
|
||||
本机密码旁的眼睛按钮用于显示 / 隐藏密码,刷新按钮用于生成新的随机密码,铅笔按钮用于手动修改。密码统一为 **6 位数字或英文字母**。修改或刷新时需保持连接服务器;等待修改成功、客户端重新连接后,再复制当前密码。已保存旧密码的控制端需重新输入。
|
||||
|
||||
### 从浏览器连接
|
||||
|
||||
1. 保持被控电脑上的 CrossDesk 运行,并确认已连接服务器。
|
||||
2. 打开 [Web 客户端](https://web.crossdesk.cn/),输入 **远程设备 ID** 和 **密码**,点击“连接”。
|
||||
3. 连接后使用页面提供的显示器、鼠标模式和键盘控件操作远端;手机和平板也可通过浏览器访问。
|
||||
|
||||
如果使用自建服务器,需要让控制端与被控端接入同一服务;Web 部署配置见 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
<a id="deskhub"></a>
|
||||
|
||||
## 桌面墙:集中查看与操控多台设备
|
||||
### 桌面墙(DeskHub)
|
||||
|
||||
点击主窗口左上角 **桌面墙**,打开独立窗口;连接窗口仍可用于普通远程连接和全局设置。桌面墙会保存设备成员、分组、排序与窗口布局,重启后需要手动连接设备。
|
||||
|
||||
1. **添加设备。** 点击 **添加 → 通过设备 ID 添加**,填写 ID、密码及可选分组,点击“添加并连接”;只有勾选“记住密码”才保存凭据。也可选择 **从近期连接添加**,多选已有记录并导入;导入后用“选择 → 连接”发起连接。
|
||||
2. **查看与整理。** 从“全部设备”切换到指定分组;拖动网格分隔线统一调整画面大小,拖动设备到另一台设备上交换位置。精简模式隐藏名称和管理工具栏,全屏用于扩大查看区域;翻页保留连接。
|
||||
3. **放大与操控。** 点击设备画面进入只读放大视图,上方缩略图可切换设备,拖动中间分隔条可调整上下区域。点击 **接管操控** 后才发送键鼠;点击“释放操控”或按 **Ctrl / ⌘ + Shift + Esc** 释放。切换设备或窗口失焦也会释放操控。
|
||||
4. **批量操作。** 点击 **选择**,勾选设备或“全选”,执行连接、断开,或从“更多”中静音 / 取消静音、分发文件。“全选”覆盖当前分组的全部分页;切换分组会清空选择。
|
||||
按分组查看多台设备,点击画面放大,再选择接管操控;支持批量连接、静音与文件分发。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-deskhub-focus-zh.png" width="960" alt="桌面墙放大视图:上方设备缩略图、下方主画面,以及只读预览、开启群控和接管操控入口;画面为演示" />
|
||||
<img src="docs/images/desktop-deskhub-zh.png" width="960" alt="DeskHub 桌面墙:六台演示设备的远程画面与管理工具栏" />
|
||||
</p>
|
||||
|
||||
**群控:** 在放大视图点击“开启群控”,将当前设备的键鼠操作同步到 **所有已连接设备**,包含其他分组、分页及未显示在墙内的连接;范围不受批量勾选限制。各设备接收相同坐标与按键,屏幕尺寸不同时不会另做比例适配。释放操控会同时结束群控。
|
||||
|
||||
桌面墙连接默认静音,需要声音时通过“选择 → 更多 → 取消静音”开启。底部 **网络状态** 可查看所有已连接设备的直连 / 中继、加密状态、收发速率、视频延时、丢包率、分辨率与帧率。需要显示器切换、隐私屏等完整单会话工具时,可在主窗口“近期连接”中点击已连接设备,打开独立会话窗口。
|
||||
|
||||
**关闭桌面墙会断开全部远程设备连接,并关闭共用连接的独立会话窗口;最小化不会断开。** 分组、分页和选择范围不会限制关闭时的断开范围。更多操作、预览策略与版本兼容性见 [桌面墙说明](docs/deskhub.md)。
|
||||
群控会向所有已连接设备发送输入;关闭桌面墙会断开全部远程连接。操作范围与详细用法见 [桌面墙使用指南](docs/USER_GUIDE.md#deskhub)。截图使用演示数据,更多界面及来源见 [使用指南](docs/USER_GUIDE.md) 与 [截图说明](docs/images/README.md)。
|
||||
|
||||
<a id="session"></a>
|
||||
|
||||
## 会话中的常用操作
|
||||
|
||||
远程窗口通过会话标签切换设备,控制栏提供以下操作;控制栏收起时先点击展开按钮。将鼠标悬停在图标上可查看提示。
|
||||
|
||||
| 入口 | 用途 |
|
||||
| --- | --- |
|
||||
| 显示器 | 切换远端显示器 |
|
||||
| 键盘 | 发送组合键;Windows 的 `Ctrl+Alt+Del` 依赖远端服务 |
|
||||
| 鼠标 | 控制 / 释放远端鼠标 |
|
||||
| 扬声器 | 播放声音 / 静音 |
|
||||
| 文件夹 | 选择文件发送到远端,并查看传输进度 |
|
||||
| 画面设置 | 在当前会话中调整画面质量、采集帧率与画面偏好 |
|
||||
| 网络状态 | 查看流量、丢包率、帧率、分辨率、直连 / 中继及加密状态 |
|
||||
| 隐私屏 | 开启或关闭被控电脑的隐私屏;悬停查看当前状态与可用操作 |
|
||||
| 全屏 | 进入 / 退出全屏 |
|
||||
| 断开连接 | 结束当前远程会话 |
|
||||
|
||||
桌面端可同步文本剪贴板。接收文件的位置可在“设置 → 常规 → 文件保存路径”调整。关闭主窗口会隐藏窗口并保持程序运行;需要完全退出时使用系统托盘或菜单栏中的退出入口。
|
||||
|
||||
### 在会话中调整画面
|
||||
|
||||
连接成功后,点击工具栏中的 **画面设置**(滑杆图标),直接选择低 / 中 / 高画质、**30 / 60 fps**,以及帧率优先 / 画质优先 / 平衡。选项会发送给当前被控端,无需断开重连;实际画面效果取决于网络、设备与编码器能力。旧版被控端不支持时,菜单会提示并禁用选项。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-stream-video-settings-zh.png" width="800" alt="当前远程会话的画面设置菜单:画面质量、采集帧率与画面偏好;背景为公开页面演示图" />
|
||||
</p>
|
||||
|
||||
<a id="settings"></a>
|
||||
|
||||
## 客户端设置与更新
|
||||
## 会话操作与设置
|
||||
|
||||
点击主窗口右上角 **☰ → 设置**,顶部切换 **常规 / 桌面墙**,修改后点击 **保存**。切换选项卡会保留未保存的修改,点击“取消”放弃修改。“常规”按界面、视频、网络连接等类别组织选项,可向下滚动查看其余设置。编码与连接选项建议在建立连接前调整;会话进行中部分选项不可修改。普通会话的画质、帧率和画面偏好在远程会话工具栏中调整。
|
||||
远程会话支持显示器切换、画质与帧率调整、声音、文本剪贴板同步和文件传输。工具栏还可查看网络状态、控制隐私屏;桌面设置中可配置 IP 直连开关、编码、中继、自托管与桌面墙画面预算。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-settings-zh.png" width="720" alt="新版常规设置:常规与桌面墙选项卡、语言、视频编码、硬件编解码及网络连接设置" />
|
||||
</p>
|
||||
- [会话操作](docs/USER_GUIDE.md#session):工具栏、文本剪贴板、文件保存和隐私屏。
|
||||
- [客户端设置与更新](docs/USER_GUIDE.md#settings):常规设置、桌面墙传输及版本检查。
|
||||
- [桌面墙技术说明](docs/deskhub.md):预览订阅、带宽分配、协议与兼容性。
|
||||
|
||||
| 设置 | 说明 |
|
||||
| --- | --- |
|
||||
| 语言 | 中文、English、Русский |
|
||||
| 采集方式(Windows) | 自动 / DXGI / WGC / GDI;无显示器主机使用“自动”启用自动适配 |
|
||||
| 视频编码格式 | H.264 / AV1;硬件编解码是否可用取决于平台、硬件与构建选项 |
|
||||
| 启用中继服务 | 允许借助 TURN 中继建立连接;P2P 连接失败时先检查此项 |
|
||||
| 强制中继连接 | 启用中继服务后可勾选,让连接通过中继建立;关闭中继服务会同时关闭此项 |
|
||||
| 保存远程画面预览 | 默认开启,保存远程画面预览;关闭后缩略图区域显示被控端平台,并清除已有预览,保留连接记录和密码 |
|
||||
| 被连接时开启隐私屏 | 设置本机被远程连接时是否自动启用隐私屏;会话中可从控制端工具栏切换 |
|
||||
| 自托管配置 | 填写服务器地址和信令服务端口,并勾选启用 |
|
||||
| 开机自启 / 启用守护进程 | 调整启动与进程守护行为,按界面提示重启后生效 |
|
||||
| 文件保存路径 | 选择接收文件的目录 |
|
||||
<a id="mobile"></a>
|
||||
|
||||
### 桌面墙画面传输
|
||||
|
||||
在 **设置 → 桌面墙** 中统一配置墙内设备,保存后立即应用到当前桌面墙会话,并用于后续连接;普通连接不受影响。
|
||||
|
||||
| 设置 | 说明 |
|
||||
| --- | --- |
|
||||
| 分辨率上限 | 自动、320 × 180、640 × 360、1280 × 720、1920 × 1080、2560 × 1440、3840 × 2160 或原始分辨率;自动模式按网格 / 放大视图调整,固定上限保持比例且不放大源画面 |
|
||||
| 帧率 | 跟随设备设置,或指定 15 / 30 / 60 FPS |
|
||||
| 视频总带宽 | 默认 20 Mbps,可选 2 / 5 / 10 / 20 / 50 / 100 Mbps;由支持该能力的桌面墙会话共享,放大画面优先分配,不包含音频、文件传输与普通连接,也不是瞬时流量的硬上限 |
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-settings-deskhub-zh.png" width="720" alt="桌面墙传输设置:自动分辨率、跟随设备帧率和 20 Mbps 视频总带宽" />
|
||||
</p>
|
||||
|
||||
支持预览订阅的设备在不可见时可暂停视频传输,重新可见时恢复;旧版被控端按其能力回退,不保证所有分辨率、帧率或带宽选项都生效。
|
||||
|
||||
### 检查更新
|
||||
|
||||
客户端启动后会在后台检查版本,发现新版本时自动弹出更新提示,可查看更新说明并前往下载页面。关闭提示后,本次运行中的后台检查不会再次弹窗,主窗口菜单入口仍会显示更新提示;重新启动客户端后,如仍有新版本则再次提示。打开 **☰ → 关于** 可查看版本;有新版本时,点击版本信息下方的 **更新日志** 可再次打开更新提示,也可点击 **检查更新** 手动检查。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-about-zh.png" width="720" alt="关于窗口与检查更新按钮,版本号和检查结果为演示数据" />
|
||||
</p>
|
||||
## 移动端与鸿蒙
|
||||
|
||||
<a id="ios"></a>
|
||||
|
||||
## 原生 iOS / iPadOS 客户端
|
||||
### iOS / iPadOS
|
||||
|
||||
仓库的 [`apps/ios`](apps/ios/README.md) 提供原生控制端,与桌面端使用同一套 MiniRTC 协议。它可以控制远端电脑;当前不提供将 iPhone / iPad 作为被控桌面的功能。
|
||||
原生控制端支持触控与虚拟鼠标、多显示器、远端音频、文件收发和文本剪贴板。悬浮菜单的“剪贴板”按钮可主动发送本机文本,单次上限为 128 KiB。iPhone / iPad 当前不能作为被控桌面。
|
||||
|
||||
1. 按 [iOS 开发说明](apps/ios/README.md) 使用 Xcode 构建并签名,安装到 iOS 16+ 的真机。
|
||||
2. 在首页输入对端 ID,点击连接并填写密码;成功连接后可从“最近连接”中重连。
|
||||
3. 在设置中选择鼠标模式:**相对位置**像触控板一样移动光标,**绝对位置**将触摸位置直接映射到远端画面。
|
||||
4. 点击悬浮图标展开菜单,可切换显示器、显示键盘、控制声音或发送文件。收到的文件位于应用的 `Documents/Received`,可通过传输状态旁的分享按钮导出。
|
||||
|
||||
当前支持接收远端文本到 iOS 剪贴板;界面暂未提供主动发送本机剪贴板的按钮。
|
||||
|
||||
当前支持单指点击、双指右键、长按拖动与捏合缩放;缩放后的双指平移用于移动视图。原生端与浏览器端分别维护,使用入口和手势可能不同。
|
||||
[使用说明](docs/USER_GUIDE.md#ios) · [构建与签名](apps/ios/README.md)
|
||||
|
||||
<a id="android"></a>
|
||||
|
||||
## 原生 Android 控制端
|
||||
### Android
|
||||
|
||||
[`apps/android`](apps/android/README.md) 提供独立 Android Studio / Gradle 工程,复用 MiniRTC 和桌面键鼠协议。支持连接电脑、H.264 / AV1 画面、声音、触控/外接键鼠、多屏切换和文本剪贴板。当前面向 Android 8.0+ arm64 设备,使用软件解码;尚未提供手机被控、文件传输和 iOS 全部附加功能。
|
||||
原生控制端支持触控与外接键鼠、多屏切换、文件收发、文本剪贴板和会话画面设置。H.264 / AV1 优先使用硬件解码,不支持或运行失败时回退到软件解码;切换应用时优先通过画中画保持会话,画中画不可用时尝试前台服务保活。当前不提供手机被控功能。悬浮菜单中的“隐私屏”按钮可直接开启或关闭电脑本地遮罩,按钮按电脑确认的状态显示“隐私屏已开启”或“隐私屏已关闭”,开启与关闭使用相同配色,正常切换不显示额外文字,仅在异常时显示提示;开启时与桌面端一致屏蔽电脑本地键鼠。
|
||||
|
||||
按 [Android 开发说明](apps/android/README.md) 构建并安装 APK,在首页填写电脑端的设备 ID 和密码即可连接。长按右键、双指滚动,横屏可扩大画面;切换到后台会结束会话。
|
||||
[使用说明](docs/USER_GUIDE.md#android) · [源码构建](apps/android/README.md)
|
||||
|
||||
<a id="harmonyos"></a>
|
||||
|
||||
### 鸿蒙 HarmonyOS(开发版)
|
||||
|
||||
PC 端可控制和被控制,复用桌面界面与桌面墙;手机和平板使用原生 ArkUI 控制端,支持触控、外接键鼠、多屏、文本剪贴板和文件收发。PC 录屏与输入控制需要系统授权,真机和长期会话验收仍在进行。
|
||||
|
||||
[使用与限制](docs/USER_GUIDE.md#harmonyos) · [构建与签名](apps/harmony/README.md) · [PC 说明](apps/harmony/desktop-port/README.md) · [移动端功能](apps/harmony/MOBILE_PARITY.md)
|
||||
|
||||
<a id="windows-service"></a>
|
||||
|
||||
## Windows 锁屏与登录界面
|
||||
## 无人值守与无头运行
|
||||
|
||||
**CrossDesk Service**(服务名 `CrossDeskService`)提供锁屏、登录、凭据输入及安全桌面的状态上报、`Ctrl+Alt+Del` 发送和键鼠输入转发。
|
||||
|
||||
安装版默认注册按需启动的服务,客户端启动时尝试启动它;未启用无人值守时,本机没有 CrossDesk 客户端进程后服务会自动退出。便携版可在提示窗口或“设置 → 锁屏控制服务”中安装服务,需要管理员权限。
|
||||
|
||||
Windows 的“开机自启”在当前用户登录后启动客户端。安装版使用以最高权限运行的登录计划任务,后续登录无需再次确认 UAC;便携版和 Debug 构建使用当前用户的普通启动项。升级安装版或手动启动新版时,会自动迁移指向该程序的旧启动项。关闭自启会删除相应注册。登录前远控需要另外启用下面的无人值守模式。
|
||||
|
||||
### 开机后尚未登录时远控
|
||||
|
||||
安装版支持独立的无人值守被控端。在完整安装目录中打开**管理员 PowerShell**,一次性启用:
|
||||
|
||||
```powershell
|
||||
.\CrossDesk.exe --unattended-enable
|
||||
# 等待联网注册后,查看独立设备 ID、密码和 online 状态
|
||||
.\CrossDesk.exe --unattended-status
|
||||
```
|
||||
|
||||
使用返回的 `device_id` 和 `password` 从另一台设备连接。首次启用时复制当前客户端的服务器和编码设置,单独生成无人值守凭据;它与普通客户端的设备 ID 不同,重启、注销和重新启用时保留。后续执行启用命令会复用机器配置,不会覆盖它。自建服务器的根证书需要安装到本地计算机的信任存储中。
|
||||
|
||||
启用后,`CrossDeskService` 改为系统启动时自动运行,无需 Windows 自动登录,也不需要保存 Windows 登录密码。服务以 SYSTEM 身份在当前控制台会话中启动无界面的被控进程,复用登录界面抓屏、键鼠及 `Ctrl+Alt+Del` 支持。用户登录、锁屏和关闭普通客户端不会停止它;注销、切换用户或进程崩溃后服务会重新拉起,期间连接可能中断,需要重新连接同一设备 ID。启动时网络尚未就绪会继续重试。
|
||||
|
||||
无人值守模式提供画面、键鼠、多屏切换和安全桌面控制;不提供声音、文件传输、剪贴板或隐私遮罩。便携版和 Debug 构建不能启用此模式。设置和凭据位于 `%ProgramData%\CrossDeskUnattended`,只允许 SYSTEM 和管理员访问;日志位于其 `logs` 子目录。
|
||||
|
||||
```powershell
|
||||
# 停止无人值守被控端,并恢复服务按需启动
|
||||
.\CrossDesk.exe --unattended-disable
|
||||
```
|
||||
|
||||
关闭普通“开机自启”不会关闭无人值守模式。禁用或卸载服务会取消无人值守启动,但保留受保护的机器配置;若需彻底清除凭据,在服务停止后由管理员删除该目录。
|
||||
|
||||
<details>
|
||||
<summary>手动部署与服务管理命令</summary>
|
||||
|
||||
保留完整安装目录;自行部署构建产物时,将 `CrossDesk.exe`、`crossdesk_service.exe`、`crossdesk_session_helper.exe` 及同次构建输出的全部 `.dll` 放在同一目录。在该目录打开管理员 PowerShell,按需执行:
|
||||
|
||||
```powershell
|
||||
.\CrossDesk.exe --service-install
|
||||
.\CrossDesk.exe --service-start
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# 停止或卸载服务
|
||||
.\CrossDesk.exe --service-stop
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
出现“远端 Windows 服务不可用”时,检查被控电脑的安装和服务状态;普通桌面连接仍可使用,受保护界面的控制能力会受限。
|
||||
- **Windows:** CrossDesk Service 支持锁屏、登录和安全桌面控制。安装版可额外启用登录前远控,使用独立设备 ID 和密码;普通“开机自启”仅在用户登录后启动客户端。功能限制及启停命令见 [Windows 无人值守指南](docs/USER_GUIDE.md#windows-service)。
|
||||
- **Linux:** 可复用现有桌面,也可创建独立 Xvfb / Xfce 会话,操作方法见 [Linux 无头运行](docs/HEADLESS.md)。
|
||||
|
||||
<a id="self-hosting"></a>
|
||||
|
||||
## 自托管服务器
|
||||
## 自托管
|
||||
|
||||
可自行部署信令服务与 TURN 中继,并在桌面客户端 **☰ → 设置 → 常规 → 自托管配置** 中填写连接信息。控制端与被控端都需要启用同一套配置。
|
||||
可自行部署信令与 TURN 中继服务,并在客户端“设置 → 常规 → 自托管配置”中启用。控制端与被控端需要接入同一服务。
|
||||
|
||||
- [自托管指南](docs/SELF_HOSTING.md):当前 Compose 部署入口、客户端设置、证书信任与排查。
|
||||
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server):服务端源码与发布配置。
|
||||
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client):浏览器端源码与自托管说明。
|
||||
[部署指南](docs/SELF_HOSTING.md) · [客户端配置](docs/USER_GUIDE.md#self-hosting) · [服务端源码](https://github.com/kunkundi/crossdesk-server) · [Web 客户端部署](https://github.com/kunkundi/crossdesk-web-client)
|
||||
|
||||
## 开发与反馈
|
||||
<a id="development"></a>
|
||||
|
||||
- [源码构建](docs/BUILD.md):Windows、macOS、Linux、可选编译参数与打包。
|
||||
- [iOS 开发说明](apps/ios/README.md):原生端构建和真机验证。
|
||||
- [Android 开发说明](apps/android/README.md):APK 构建、安装与模拟器验证。
|
||||
- [GUI 架构](docs/gui-architecture.md):桌面 UI、平台实现与工程结构。
|
||||
- [常见问题](docs/FAQ.md):连接失败、黑屏、文件位置与编译问题。
|
||||
- [提交问题](https://github.com/kunkundi/crossdesk/issues):请附上两端系统、客户端版本、连接方式和可复现步骤。
|
||||
## 构建与反馈
|
||||
|
||||
- [源码构建](docs/BUILD.md):桌面端依赖、编译参数与打包;移动端请使用上方各平台的构建指南。
|
||||
- [GUI 架构](docs/gui-architecture.md):桌面界面、平台实现与工程结构。
|
||||
- [连接模式架构](docs/connection-architecture.md):ID 连接与 IP 直连的寻址、认证、协商、数据路径及生命周期。
|
||||
- [常见问题](docs/FAQ.md):连接失败、黑屏、系统权限、文件位置与构建问题。
|
||||
- [提交问题或建议](https://github.com/kunkundi/crossdesk/issues):请提供两端系统、客户端版本、连接方式和可复现步骤。
|
||||
|
||||
桌面端的 `CROSSDESK_UPDATE_PUBLIC_KEY` 是可选编译参数,用于验证应用内升级清单的 Ed25519 签名。不配置仍可正常编译、运行和检查版本,但更新只能手动下载;需要应用内自动安装更新时,须配置与发布私钥匹配的公钥。配置方式见 [更新公钥](docs/BUILD.md#update-public-key),密钥生成与发布流程见 [桌面端应用内升级](docs/UPDATER.md)。
|
||||
|
||||
<a id="sponsor"></a>
|
||||
|
||||
## 支持 CrossDesk
|
||||
|
||||
如果 CrossDesk 对你有帮助,欢迎通过以下平台赞助,支持项目的持续开发、跨平台测试与日常维护。感谢你的支持!
|
||||
|
||||
[爱发电](https://afdian.com/a/kunkundi) · [Ko-fi](https://ko-fi.com/kunkundi) · [GitHub Sponsors](https://github.com/sponsors/kunkundi)
|
||||
|
||||
## 致谢与许可
|
||||
|
||||
感谢 [HelloGitHub](https://hellogithub.com/)、[阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 与 [LinuxDo](https://linux.do) 社区的推荐、交流与支持。
|
||||
|
||||
CrossDesk 使用 [GPL-3.0](LICENSE) 许可。隐私说明见 [隐私政策](PRIVACY.md#中文)。
|
||||
Copyright (c) 2026 JUNKUN DI.
|
||||
|
||||
CrossDesk 使用 [GPL-3.0-only](LICENSE) 许可。隐私说明见 [隐私政策](PRIVACY.md#中文)。
|
||||
|
||||
### Code signing policy
|
||||
|
||||
|
||||
+127
-253
@@ -1,25 +1,83 @@
|
||||
<div align="center">
|
||||
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk logo" />
|
||||
<h1>CrossDesk</h1>
|
||||
<img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk logo with the icon on the left and a two-tone wordmark on the right" />
|
||||
<p>A lightweight, cross-platform remote desktop for computers, browsers, and mobile devices, with DeskHub for viewing and controlling multiple desktops.</p>
|
||||
<p><a href="https://www.crossdesk.cn/">Website</a> · <a href="https://github.com/kunkundi/crossdesk/releases">Download</a> · <a href="https://web.crossdesk.cn/">Open Web Client</a> · <a href="README.md">中文</a></p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](#download)
|
||||
[](https://github.com/kunkundi/crossdesk/releases)
|
||||
[](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
[](#download)
|
||||
|
||||
[](https://github.com/kunkundi/crossdesk/releases)
|
||||
[](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
|
||||
[](https://ko-fi.com/kunkundi)
|
||||
[](https://github.com/sponsors/kunkundi)
|
||||
[](https://afdian.com/a/kunkundi)
|
||||
|
||||
</div>
|
||||
|
||||
CrossDesk connects Windows, macOS, and Linux desktops, with browser and native iOS / Android clients for controlling computers. The desktop client now includes **DeskHub**, a separate window for viewing multiple remote desktops, organizing devices into groups, and performing batch actions or synchronized control. Built on [MiniRTC](https://github.com/kunkundi/minirtc), it supports real-time video and audio, keyboard and mouse control, file transfer, and self-hosting. The project is under active development.
|
||||
CrossDesk connects your computers from desktop, browser, and mobile clients, with **DeskHub** for viewing and controlling multiple devices together. It is built on [MiniRTC](https://github.com/kunkundi/minirtc) and supports self-hosting.
|
||||
|
||||
> This guide follows the current repository source. Desktop screenshots render the current Slint UI with sample IDs, passwords, online statuses, and recent devices; thumbnails show public pages. Check each release's notes for the features and requirements of its binaries. Native iOS build and signing instructions are below.
|
||||
[Features](#features) · [Download and platforms](#download) · [Quick start](#quick-start) · [Direct IP](#lan-direct) · [Preview](#preview) · [User guide](docs/USER_GUIDE_EN.md) · [Self-hosting](#self-hosting) · [Build and feedback](#development) · [Sponsor](#sponsor)
|
||||
|
||||
[Preview](#preview) · [Download](#download) · [Quick start](#quick-start) · [DeskHub](#deskhub) · [Session controls](#session) · [Settings](#settings) · [iOS](#ios) · [Self-hosting](#self-hosting) · [Build](docs/BUILD_EN.md) · [FAQ](docs/FAQ.md#english)
|
||||
<a id="features"></a>
|
||||
|
||||
## Features
|
||||
|
||||
- **Cross-platform access:** control Windows, macOS, and Linux desktops from computers, browsers, or native iOS / Android clients. HarmonyOS PC and mobile clients are in development.
|
||||
- **Direct IP over LAN:** connect desktop clients using an IP address, port, and six-character password without a cloud signaling service. Each local IP has its own port; passwords can be remembered and recent devices are separated by mode.
|
||||
- **DeskHub:** live multi-device previews, groups, enlarged control, group input, and batch file distribution.
|
||||
- **Video and audio:** H.264 / AV1, hardware codecs, remote audio, and in-session quality and frame-rate controls. Capabilities depend on the platform and device.
|
||||
- **Everyday controls:** monitor switching, keyboard and mouse input, shortcuts, text clipboard, and file transfer.
|
||||
- **Deployment:** direct P2P connections, TURN relay, SRTP media encryption, self-hosting, Windows access before sign-in, and Linux headless operation.
|
||||
|
||||
<a id="download"></a>
|
||||
|
||||
## Download and platform support
|
||||
|
||||
Get a package matching your OS and CPU architecture from the [website](https://www.crossdesk.cn/) or [GitHub Releases](https://github.com/kunkundi/crossdesk/releases), or open the [Web Client](https://web.crossdesk.cn/) directly.
|
||||
|
||||
| Platform | Support and installation |
|
||||
| --- | --- |
|
||||
| Windows | **Control + host**. Windows 10+, x64; `.exe` installer, with portable builds also supported |
|
||||
| macOS | **Control + host**. macOS 14.0+; x64 / arm64 `.pkg` for Intel / Apple Silicon |
|
||||
| Linux | **Control + host**. Ubuntu 20.04+, glibc 2.31 baseline; amd64 / arm64 `.deb` |
|
||||
| iOS / iPadOS | **Control only**. iOS 16.0+; sign the unsigned release app yourself, or build and install with Xcode |
|
||||
| Android | **Control only**. Android 8.0+, arm64; install the signed APK or build from source |
|
||||
| HarmonyOS PC (development) | **Control + host**. API 26, arm64; build and sign a HAP with DevEco; physical-device validation is ongoing |
|
||||
| HarmonyOS phones / tablets (development) | **Control only**. API 20+, arm64; build and sign a HAP with DevEco; physical-device validation is ongoing |
|
||||
| Web | **Control only**. A WebRTC-capable browser; no client installation on the controlling device |
|
||||
|
||||
Requirements follow the current source and build configuration. Check the release notes for the downloaded package. Client capabilities are summarized under [Mobile and HarmonyOS](#mobile).
|
||||
|
||||
- **Linux:** use `crossdesk-linux-…deb` for an existing desktop, or `crossdesk-virtual-desktop-xfce-…deb` for a server needing a separate Xfce desktop. Choose one. See [installation details](docs/USER_GUIDE_EN.md#installation) and [headless operation](docs/HEADLESS_EN.md).
|
||||
- **macOS:** grant Screen Recording and Accessibility when prompted on first launch. These enable desktop capture and remote keyboard/mouse input. See [permissions](docs/USER_GUIDE_EN.md#installation).
|
||||
- **iOS / HarmonyOS:** follow the [iOS](apps/ios/README.md) or [HarmonyOS](apps/harmony/README.md) (Chinese) signing and build guide. Unsigned artifacts cannot be installed directly.
|
||||
|
||||
<a id="quick-start"></a>
|
||||
|
||||
## Quick start
|
||||
|
||||
### Connect by device ID
|
||||
|
||||
1. **Prepare the host.** Install and run CrossDesk on the computer to control, select ID mode, and wait for the server-connected status, then find its ID and password under **Local Desktop**.
|
||||
2. **Connect.** On another computer, enter the host's 9-digit ID and click **Connect**. Native mobile clients and the [Web Client](https://web.crossdesk.cn/) can also connect.
|
||||
3. **Verify the password.** Enter the host's current connection password and wait for the desktop to appear. On desktop, select **Remember password** to reconnect later from **Recent Connections**.
|
||||
|
||||
For self-hosting, both ends must use the same service. See the [user guide](docs/USER_GUIDE_EN.md#quick-start) for connection, password, and browser controls, or the [FAQ](docs/FAQ.md#english) for failed connections, blank video, and input problems.
|
||||
|
||||
<a id="lan-direct"></a>
|
||||
|
||||
### Connect directly over LAN
|
||||
|
||||
1. **Switch modes.** Run a desktop client with direct IP support on both computers. Click the top-left mode button to select **Direct IP mode**. If the button is hidden, select **Enable direct IP** under **Settings → General → Network** and save.
|
||||
2. **Enter the address.** On the controller, enter the host's displayed `IP:port`, for example `192.168.1.20:52888`, and click **Connect**. Omitting the port uses **52888**; use the actual port shown on the host.
|
||||
3. **Enter the password.** Use the host's **six-character ASCII letter/digit password** shown in Direct IP mode. You can select **Remember password**. Switching modes also switches the recent device list.
|
||||
|
||||
Both computers must be reachable over the LAN. Internet access may be available or absent; direct sessions do not depend on a cloud signaling service. The main window lets you select the local IP, edit its port, and change the password. Disabling **Enable direct IP** stops listeners, disconnects direct IP sessions, and hides the mode button. See the [direct IP guide](docs/USER_GUIDE_EN.md#lan-direct).
|
||||
|
||||
<a id="preview"></a>
|
||||
|
||||
@@ -27,293 +85,113 @@ CrossDesk connects Windows, macOS, and Linux desktops, with browser and native i
|
||||
|
||||
### Desktop client
|
||||
|
||||
The main window presents your credentials and connection controls in Local Desktop and Remote Desktop cards, with recent devices and a group filter below. Open DeskHub at the top left, or use the top-right menu for settings, messages, and About.
|
||||
Local credentials, remote connections, and recent devices share the main window. The top-left controls switch connection modes and open DeskHub.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-main-en.png" width="800" alt="Updated main window with DeskHub access, local credentials, remote connection controls, and recent-device group filtering; data is illustrative" />
|
||||
<img src="docs/images/desktop-main-en.png" width="800" alt="CrossDesk ID mode with the mode switch, local ID, remote connection controls, and recent devices; demonstration data" />
|
||||
</p>
|
||||
|
||||
### DeskHub
|
||||
### Direct IP mode
|
||||
|
||||
View multiple devices in a separate window with groups, a resizable grid, compact and fullscreen views, and batch actions. Click a preview to enlarge it, then take control when needed.
|
||||
Select the local IP, edit its port and password, and connect using the other desktop's address. Recent LAN devices have their own list below.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-deskhub-en.png" width="960" alt="DeskHub grid with six demonstration devices, group filtering, Add, Select, Compact, and Network controls" />
|
||||
<img src="docs/images/desktop-lan-en.png" width="800" alt="CrossDesk Direct IP mode with local IP and port editing, a six-character password, remote address input, and recent LAN devices; demonstration data" />
|
||||
</p>
|
||||
|
||||
### Web client
|
||||
|
||||
Enter a remote device ID and password in your browser without installing the desktop app on the controlling device.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/web-client.png" width="800" alt="CrossDesk Web Client connection page in Chinese, captured on 2026-09-25" />
|
||||
</p>
|
||||
|
||||
See the [screenshot notes](docs/images/README.md) for image sources and refresh instructions.
|
||||
|
||||
## Features
|
||||
|
||||
| Capability | Current support |
|
||||
| --- | --- |
|
||||
| Cross-platform control | Windows / macOS / Linux desktop control; browser and native iOS / Android controllers |
|
||||
| Live video and audio | H.264 / AV1, hardware codecs, and remote audio; adjust quality, 30 / 60 fps, and video preference during a session |
|
||||
| Devices and displays | Recent connections, device aliases and group filtering, session tabs, and remote display switching |
|
||||
| DeskHub | Live multi-desktop previews, groups and drag reordering, enlarged views with control, synchronized input, and batch connection / disconnection / audio / file actions; configurable resolution, frame rate, and shared video budget |
|
||||
| Input and sharing | Keyboard/mouse input, remote cursor synchronization, shortcuts, text clipboard synchronization, and file transfer |
|
||||
| Networking and deployment | Direct P2P connections, TURN relay and forced relay, SRTP media encryption, and self-hosted signaling/relay services |
|
||||
| Privacy screen | Enable automatically when connected to, or toggle from the session toolbar; availability depends on the host OS and capture method |
|
||||
| Windows protected desktops | CrossDesk Service forwards input on lock screens, sign-in screens, and secure desktops |
|
||||
| Linux headless operation | Reuse existing desktops and applications or create separate Xvfb desktops; use the terminal console to check status and change passwords or settings; see [setup](docs/HEADLESS_EN.md) |
|
||||
|
||||
Controls and capabilities vary by client. Desktop controls are described below; see the [iOS guide](apps/ios/README.md) for native mobile features.
|
||||
|
||||
<a id="download"></a>
|
||||
|
||||
## Download and install
|
||||
|
||||
Choose a package matching your OS and CPU architecture from [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) or the [website](https://www.crossdesk.cn/).
|
||||
|
||||
| Platform | Current source / CI baseline | Installation |
|
||||
| --- | --- | --- |
|
||||
| Windows | Windows 10+, x64 | `.exe` installer; portable builds are also supported |
|
||||
| macOS | macOS 14.0+, Intel / Apple Silicon | x64 or arm64 `.pkg` |
|
||||
| Linux | Ubuntu 20.04+, amd64 / arm64, glibc 2.31 baseline | Standard / Xfce virtual desktop `.deb` |
|
||||
| iOS / iPadOS | iOS 16.0+, arm64 physical device | Native controller; build and sign with Xcode, or sign the unsigned CI app |
|
||||
| Android | Android 8.0+, arm64 | Native controller; build an APK with Gradle ([guide](apps/android/README.md)) |
|
||||
| Web | A WebRTC-capable browser | Open [web.crossdesk.cn](https://web.crossdesk.cn/) |
|
||||
|
||||
Linux provides two standalone packages per architecture. **Choose one**:
|
||||
|
||||
| Package | Intended use |
|
||||
| --- | --- |
|
||||
| `crossdesk-linux-<arch>-<version>.deb` (recommended) | Computers with an existing desktop; does not install Xfce or Xvfb |
|
||||
| `crossdesk-virtual-desktop-xfce-linux-<arch>-<version>.deb` | Servers needing an independent desktop; APT additionally installs **Xfce, Xvfb and D-Bus session tools** |
|
||||
|
||||
To install the standard package, replace the example filename with your download (use arm64 for that architecture):
|
||||
|
||||
```bash
|
||||
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
|
||||
```
|
||||
|
||||
For the virtual desktop package, install this file instead:
|
||||
|
||||
```bash
|
||||
sudo apt install "./crossdesk-virtual-desktop-xfce-linux-amd64-<version>.deb"
|
||||
```
|
||||
|
||||
Both contain the same complete application, replace each other and preserve user settings. The standard package can also create virtual desktops after you install the optional dependencies. Installing the virtual desktop package does not start a session or change the default desktop; see [Linux headless operation](docs/HEADLESS_EN.md) to start one. The app's update download button selects the installed package type. Upgrading an existing installation does not automatically remove previously installed Xfce components.
|
||||
|
||||
**First launch on macOS:** follow the app prompts to grant **Screen Recording** (called **Screen & System Audio Recording** on newer systems) and **Accessibility** under **System Settings → Privacy & Security**. Reopen the app if prompted. These permissions allow desktop capture and remote keyboard/mouse input respectively.
|
||||
|
||||
<a id="quick-start"></a>
|
||||
|
||||
## Quick start
|
||||
|
||||
### Connect from another computer
|
||||
|
||||
1. **Prepare the host.** Install and run CrossDesk on the computer to control. Wait for the bottom status bar to show that the server is connected. Share the **Local ID** and **Password** shown under **Local Desktop** with the controller.
|
||||
2. **Enter the remote ID.** On the controlling computer, enter the host's **9-digit ID** in **Remote Desktop → Remote ID**, then click **Connect**.
|
||||
3. **Verify the password.** Enter the host's current connection password when prompted and confirm. Select **Remember password** if you want to save it.
|
||||
4. **Reconnect later.** Previously connected devices appear under **Recent Connections**. Reconnect from a card, or hover to rename it, edit its groups, remove its record, or connect / disconnect. Use the group filter beside the heading to narrow the list.
|
||||
|
||||
The eye button shows or hides the local password, the refresh button generates a new random password, and the pencil button lets you edit it. Passwords use **6 ASCII letters or digits**. Stay connected to the server when editing or refreshing a password. Wait for the change to succeed and the client to reconnect, then copy the current password. Controllers that saved the old password must enter it again.
|
||||
|
||||
### Connect from a browser
|
||||
|
||||
1. Keep CrossDesk running and connected to the server on the host computer.
|
||||
2. Open the [Web Client](https://web.crossdesk.cn/), enter the **Remote Device ID** and **Password**, and click **Connect**.
|
||||
3. Use the page's display, mouse-mode, and keyboard controls during the session. Phone and tablet browsers can also connect.
|
||||
|
||||
For self-hosting, connect both sides to the same service. Browser deployment is documented in [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
|
||||
|
||||
<a id="deskhub"></a>
|
||||
|
||||
## DeskHub: view and control multiple devices
|
||||
### DeskHub
|
||||
|
||||
Click **DeskHub** at the top left of the main window to open a separate window. The connection window remains available for ordinary remote sessions and global settings. DeskHub saves its device membership, groups, order, and window layout; connections must be started manually after relaunching.
|
||||
|
||||
1. **Add devices.** Choose **Add → Add by device ID**, enter an ID, password, and optional group, then click **Add and connect**. Credentials are saved only when you select **Remember password**. Alternatively, choose **Add from recent** and import multiple records; use **Select → Connect** to connect after importing.
|
||||
2. **Arrange the view.** Switch from **All devices** to a group, drag grid dividers to resize all previews, or drag a device onto another to swap positions. **Compact** hides device names and management controls; fullscreen provides more viewing space. Changing pages keeps connections open.
|
||||
3. **Enlarge and control.** Click a preview to open a read-only enlarged view. Switch devices with the thumbnails above it, and drag the divider to resize the two areas. Click **Control** to send keyboard and mouse input. Click **Release** or press **Ctrl / ⌘ + Shift + Esc** to release; switching devices or losing window focus also releases control.
|
||||
4. **Apply batch actions.** Click **Select**, choose devices or **Select all**, and connect, disconnect with **Close**, or use **More** to mute / unmute or send a file. **Select all** includes every page in the current group; switching groups clears the selection.
|
||||
View devices by group, click a preview to enlarge it, and take control when needed. Batch actions include connecting, muting, and sending files.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-deskhub-focus-en.png" width="960" alt="DeskHub enlarged view with thumbnails above the main preview and View only status, Group control, and Control buttons; screens are illustrative" />
|
||||
<img src="docs/images/desktop-deskhub-en.png" width="960" alt="DeskHub with six demonstration remote desktops and its management toolbar" />
|
||||
</p>
|
||||
|
||||
**Group control:** click **Group control** in the enlarged view to mirror input from the current device to **all connected devices**, including other groups, pages, and connections not displayed in the wall. The scope is independent of the batch selection. Devices receive the same coordinates and keys without additional scaling for different screen sizes. Releasing control also ends group control.
|
||||
|
||||
DeskHub connections are muted by default. Enable sound with **Select → More → Unmute**. The bottom **Network** button shows direct / relay mode, encryption, receive/send rates, video latency, packet loss, resolution, and frame rate for all connected devices. For the full single-session toolbar, including display switching and privacy screens, click a connected device under **Recent Connections** in the main window to open its separate session window.
|
||||
|
||||
**Closing DeskHub disconnects all remote devices and closes session windows sharing those connections; minimizing it keeps connections open.** The disconnect scope includes all groups and pages, regardless of selection. See the [DeskHub guide](docs/deskhub.md) (Chinese) for further controls, preview behavior, and compatibility details.
|
||||
Group control sends input to all connected devices; closing DeskHub disconnects all remote connections. See the [DeskHub user guide](docs/USER_GUIDE_EN.md#deskhub) for exact scopes and controls. Screenshots use demonstration data; more views and sources are in the [user guide](docs/USER_GUIDE_EN.md) and [screenshot notes](docs/images/README.md).
|
||||
|
||||
<a id="session"></a>
|
||||
|
||||
## Session controls
|
||||
|
||||
Switch devices using session tabs. Expand the control bar if it is collapsed, and hover over icons to see their tooltips.
|
||||
|
||||
| Control | Action |
|
||||
| --- | --- |
|
||||
| Display | Switch the remote monitor |
|
||||
| Keyboard | Send shortcuts; Windows `Ctrl+Alt+Del` requires the remote service |
|
||||
| Mouse | Enable / release remote mouse control |
|
||||
| Speaker | Play / mute remote audio |
|
||||
| Folder | Select a file to send and view transfer progress |
|
||||
| Video settings | Adjust video quality, capture frame rate, and video preference during the current session |
|
||||
| Network statistics | Inspect traffic, packet loss, frame rate, resolution, direct / relay mode, and encryption status |
|
||||
| Privacy screen | Enable or disable the host's privacy screen; hover to see its status and available actions |
|
||||
| Fullscreen | Enter / exit fullscreen |
|
||||
| Disconnect | End the current remote session |
|
||||
|
||||
Desktop clients synchronize text clipboard contents. Configure the receiving directory under **Settings → General → File Save Path**. Closing the main window hides it and keeps the app running; use the system tray or menu-bar exit command to quit completely.
|
||||
|
||||
### Adjust video during a session
|
||||
|
||||
After connecting, open **Video settings** (the sliders icon) in the toolbar. Choose low / medium / high quality, **30 / 60 fps**, and frame-rate priority / quality priority / balanced adaptation. Changes are sent to the current host without reconnecting; actual results depend on the network, device, and encoder. For older hosts that do not support live video settings, the menu explains the limitation and disables the options.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-stream-video-settings-en.png" width="800" alt="Current session video menu with quality, capture frame rate, and video preference; the background is a public page used for demonstration" />
|
||||
</p>
|
||||
|
||||
<a id="settings"></a>
|
||||
|
||||
## Client settings and updates
|
||||
## Session controls and settings
|
||||
|
||||
Open **☰ → Settings** in the top-right corner, choose **General / DeskHub**, and click **Save** to apply changes. Switching tabs preserves pending changes; **Cancel** discards them. General settings are grouped into interface, video, networking, and further sections available by scrolling. Configure codecs and connections before connecting; some settings are disabled during an active session. Adjust ordinary session quality, frame rate, and video preference from its toolbar.
|
||||
Remote sessions support monitor switching, video quality and frame-rate controls, audio, text clipboard synchronization, and file transfer. The toolbar also provides network statistics and privacy screen controls. Desktop settings configure direct IP availability, codecs, relay, self-hosting, and the DeskHub video budget.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-settings-en.png" width="720" alt="Updated General settings with General and DeskHub tabs, language, video codec, hardware codecs, and networking options" />
|
||||
</p>
|
||||
- [Session controls](docs/USER_GUIDE_EN.md#session): toolbar, text clipboard, received files, and privacy screens.
|
||||
- [Client settings and updates](docs/USER_GUIDE_EN.md#settings): general options, DeskHub video settings, and version checks.
|
||||
- [DeskHub technical guide](docs/deskhub.md) (Chinese): preview subscriptions, bandwidth allocation, protocol, and compatibility.
|
||||
|
||||
| Setting | Purpose |
|
||||
| --- | --- |
|
||||
| Language | 中文, English, Русский |
|
||||
| Capture Method (Windows) | Auto / DXGI / WGC / GDI; choose Auto for automatic headless adaptation |
|
||||
| Video Encode Format | H.264 / AV1; hardware codec availability depends on the platform, device, and build options |
|
||||
| Enable TURN Service | Allow relay-assisted connections; check this setting if P2P fails |
|
||||
| Force Relay | Require relay connections when TURN is enabled; turning TURN off also disables this option |
|
||||
| Save remote screen previews | On by default. When disabled, the thumbnail area shows the remote device’s platform and existing previews are cleared; connection records and passwords are preserved |
|
||||
| Privacy screen on connect | Automatically enable privacy when this computer is controlled remotely; the controller can toggle it from the session toolbar |
|
||||
| Self-Hosted Config | Set the server address and signaling port, then enable the checkbox |
|
||||
| Auto Start / Enable Daemon | Configure startup and process supervision; restart as indicated by the UI |
|
||||
| File Save Path | Choose where received files are saved |
|
||||
<a id="mobile"></a>
|
||||
|
||||
### DeskHub video transmission
|
||||
|
||||
Use **Settings → DeskHub** to configure wall sessions together. Saving applies changes immediately to current DeskHub sessions and future connections; ordinary connections are unaffected.
|
||||
|
||||
| Setting | Purpose |
|
||||
| --- | --- |
|
||||
| Resolution limit | Auto, 320 × 180, 640 × 360, 1280 × 720, 1920 × 1080, 2560 × 1440, 3840 × 2160, or Original resolution; Auto adapts to grid / enlarged views, while fixed limits preserve aspect ratio without upscaling the source |
|
||||
| Frame rate | Follow device settings, or choose 15 / 30 / 60 FPS |
|
||||
| Video budget | Defaults to 20 Mbps; choose 2 / 5 / 10 / 20 / 50 / 100 Mbps, shared by compatible DeskHub sessions with priority for enlarged views. Audio, file transfers, and ordinary connections are excluded; this is not a hard limit on instantaneous traffic |
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-settings-deskhub-en.png" width="720" alt="DeskHub video settings with Auto resolution, Follow device settings frame rate, and a 20 Mbps video budget" />
|
||||
</p>
|
||||
|
||||
Devices supporting preview subscriptions can pause video while their views are hidden and resume when visible. Older hosts fall back according to their capabilities, so not every resolution, frame-rate, or bandwidth setting will apply.
|
||||
|
||||
### Check for updates
|
||||
|
||||
The client checks for updates in the background after launch and automatically opens the update dialog when a new version is found, showing release notes and a link to the download page. After you dismiss the dialog, background checks will not reopen it during the current run, while the main window menu keeps its update indicator. Restarting the client shows the dialog again if an update is still available. Open **☰ → About** to view the version. When an update is available, click **Release notes** below the new version to reopen the update dialog, or click **Check for updates** to check manually.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-about-en.png" width="720" alt="About dialog with the Check for updates button; version and check result are demonstration data" />
|
||||
</p>
|
||||
## Mobile and HarmonyOS
|
||||
|
||||
<a id="ios"></a>
|
||||
|
||||
## Native iOS / iPadOS client
|
||||
### iOS / iPadOS
|
||||
|
||||
[`apps/ios`](apps/ios/README.md) contains a native controller using the same MiniRTC protocol as the desktop client. It controls remote computers; it does not expose an iPhone or iPad as a remotely controlled desktop.
|
||||
The native controller supports touch and a virtual mouse, multiple monitors, remote audio, file transfers, and text clipboard. Tap **剪贴板** (Clipboard) in the floating menu to send local text, up to 128 KiB per transfer. iPhone and iPad cannot currently act as remote hosts. The native interface uses Chinese labels.
|
||||
|
||||
1. Follow the [iOS guide](apps/ios/README.md) to build and sign the app with Xcode, then install it on an iOS 16+ physical device.
|
||||
2. Enter a remote ID on the home screen, connect, and enter its password. Successful connections become available in the recent devices list.
|
||||
3. Choose a mouse mode in Settings: **Relative Position** works like a trackpad; **Absolute Position** maps touches directly to the remote screen.
|
||||
4. Tap the floating icon to expand the menu, then switch displays, open the keyboard, toggle audio, or send files. Received files are stored in `Documents/Received` and can be exported using the share button beside the transfer status.
|
||||
[User guide](docs/USER_GUIDE_EN.md#ios) · [Build and signing](apps/ios/README.md)
|
||||
|
||||
The app currently receives remote text into the iOS clipboard. Its UI does not yet expose a button to send the local clipboard.
|
||||
<a id="android"></a>
|
||||
|
||||
Gestures include one-finger click, two-finger right-click, long-press drag, and pinch-to-zoom. When zoomed in, two-finger panning moves the viewport. The native and browser clients are maintained separately, so their controls and gestures may differ. The current native UI uses Chinese labels.
|
||||
### Android
|
||||
|
||||
The native controller supports touch and external keyboards/mice, monitor switching, file transfers, text clipboard, and video settings. H.264 / AV1 use hardware decoding when available, with software fallback when unsupported or when decoding fails. Active sessions use Picture in Picture when switching apps, with a foreground service as a fallback when Picture in Picture is unavailable. Phone-as-host is not currently supported. The floating menu’s **Privacy screen** button toggles the computer’s local cover directly, labels the host-confirmed on/off state without changing colors, and shows inline feedback only for errors or unavailable status. Enabling privacy also blocks local keyboard/mouse input, matching the desktop controller.
|
||||
|
||||
[User guide](docs/USER_GUIDE_EN.md#android) · [Build from source](apps/android/README.md) (Chinese)
|
||||
|
||||
<a id="harmonyos"></a>
|
||||
|
||||
### HarmonyOS (development)
|
||||
|
||||
The PC client acts as both controller and host, sharing the desktop UI and DeskHub. Phones and tablets use a native ArkUI controller with touch, external input devices, monitors, text clipboard, and file transfers. PC screen capture and input control require system authorization. Physical-device and long-session validation are ongoing.
|
||||
|
||||
[Usage and limits](docs/USER_GUIDE_EN.md#harmonyos) · [Build and signing](apps/harmony/README.md) · [PC guide](apps/harmony/desktop-port/README.md) · [Mobile features](apps/harmony/MOBILE_PARITY.md) (platform guides in Chinese)
|
||||
|
||||
<a id="windows-service"></a>
|
||||
|
||||
## Windows lock screens and sign-in
|
||||
## Unattended and headless access
|
||||
|
||||
**CrossDesk Service** (`CrossDeskService`) reports protected desktop states and forwards `Ctrl+Alt+Del`, keyboard, and mouse input on lock screens, sign-in screens, credential prompts, and secure desktops.
|
||||
|
||||
The installer registers an on-demand service by default. CrossDesk tries to start it on launch. Unless unattended mode is enabled, the service exits when no local CrossDesk client is running. Portable builds offer service installation in the prompt or **Settings → Lock Screen Service**, with administrator privileges.
|
||||
|
||||
On Windows, **Auto Start** launches the client after the current user signs in. Installed release builds use a logon scheduled task with highest privileges, so subsequent logons need no UAC confirmation; portable and Debug builds use a normal per-user startup entry. Upgrading the installed build or launching the new version manually migrates a legacy startup entry pointing to that executable. Turning Auto Start off removes its registration. Remote access before sign-in requires the separate unattended mode below.
|
||||
|
||||
### Remote access before Windows sign-in
|
||||
|
||||
The installed release supports a separate unattended host. Open an **administrator PowerShell** in the complete installation directory and enable it once:
|
||||
|
||||
```powershell
|
||||
.\CrossDesk.exe --unattended-enable
|
||||
# After network registration, show its separate device ID, password and online state
|
||||
.\CrossDesk.exe --unattended-status
|
||||
```
|
||||
|
||||
Connect from another device using the returned `device_id` and `password`. First-time setup copies the current client's server and codec preferences and creates separate credentials. This ID differs from the GUI client's ID and persists across reboots, logoffs and re-enabling. Subsequent enable commands retain the machine configuration. Self-hosted server root certificates must be trusted in the Local Computer certificate store.
|
||||
|
||||
Once enabled, `CrossDeskService` starts automatically at system boot. Windows automatic login and saved Windows login passwords are unnecessary. The service launches a windowless SYSTEM host in the active console session and reuses secure-desktop capture, keyboard/mouse input and `Ctrl+Alt+Del`. Signing in, locking Windows or closing the ordinary client does not stop it. Logoff, user switching or a host crash causes the service to restart the host; the connection may drop, and you can reconnect using the same ID. Network connections retry if networking is unavailable during boot.
|
||||
|
||||
Unattended mode supports screen viewing, keyboard/mouse input, monitor switching and secure desktops. Audio, file transfer, clipboard and privacy screens are unavailable. Portable and Debug builds cannot enable it. Configuration and credentials reside in `%ProgramData%\CrossDeskUnattended`, accessible only to SYSTEM and administrators; logs are in its `logs` subdirectory.
|
||||
|
||||
```powershell
|
||||
# Stop the unattended host and restore on-demand service startup
|
||||
.\CrossDesk.exe --unattended-disable
|
||||
```
|
||||
|
||||
Turning off ordinary Auto Start does not disable unattended mode. Disabling or uninstalling the service disarms unattended startup but retains the protected machine profile. To erase its credentials, stop the service and delete that directory as an administrator.
|
||||
|
||||
<details>
|
||||
<summary>Manual deployment and service commands</summary>
|
||||
|
||||
Keep the complete installation directory. For manual deployment, place `CrossDesk.exe`, `crossdesk_service.exe`, `crossdesk_session_helper.exe`, and all `.dll` files from the same build together. Open an administrator PowerShell in that directory and run the commands you need:
|
||||
|
||||
```powershell
|
||||
.\CrossDesk.exe --service-install
|
||||
.\CrossDesk.exe --service-start
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# Stop or uninstall the service
|
||||
.\CrossDesk.exe --service-stop
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
If **Remote Windows service unavailable** appears, check installation and service status on the host. Ordinary desktop connections remain available, while control of protected screens is limited.
|
||||
- **Windows:** CrossDesk Service supports lock screens, sign-in, and secure desktops. Installed builds can enable access before sign-in with a separate device ID and password; ordinary **Auto Start** runs after a user signs in. See the [Windows unattended guide](docs/USER_GUIDE_EN.md#windows-service) for capabilities, limits, and commands.
|
||||
- **Linux:** reuse an existing desktop or create an independent Xvfb / Xfce session. See [Linux headless operation](docs/HEADLESS_EN.md).
|
||||
|
||||
<a id="self-hosting"></a>
|
||||
|
||||
## Self-hosting
|
||||
|
||||
Host your own signaling and TURN services, then enter their connection details under **☰ → Settings → General → Self-Hosted Config**. Enable the same server configuration on both the controller and host.
|
||||
Host your own signaling and TURN services, then enable them under **Settings → General → Self-Hosted Config**. Both the controller and host must use the same service.
|
||||
|
||||
- [Self-hosting guide](docs/SELF_HOSTING_EN.md): current Compose deployment, client settings, certificate trust, and troubleshooting.
|
||||
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server): server source and release configuration.
|
||||
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client): browser source and deployment instructions.
|
||||
[Deployment guide](docs/SELF_HOSTING_EN.md) · [Client setup](docs/USER_GUIDE_EN.md#self-hosting) · [Server source](https://github.com/kunkundi/crossdesk-server) · [Web client deployment](https://github.com/kunkundi/crossdesk-web-client)
|
||||
|
||||
## Development and feedback
|
||||
<a id="development"></a>
|
||||
|
||||
- [Build from source](docs/BUILD_EN.md): Windows, macOS, Linux, optional flags, and packaging.
|
||||
- [iOS development](apps/ios/README.md): native build and physical-device checks.
|
||||
- [Android development](apps/android/README.md): APK builds, installation, and emulator checks.
|
||||
- [GUI architecture](docs/gui-architecture.md): desktop UI and platform organization.
|
||||
- [FAQ](docs/FAQ.md#english): connection failures, blank video, received files, and build issues.
|
||||
- [Report an issue](https://github.com/kunkundi/crossdesk/issues): include both operating systems, client versions, connection mode, and reproduction steps.
|
||||
## Build and feedback
|
||||
|
||||
- [Build from source](docs/BUILD_EN.md): desktop dependencies, build options, and packaging. Use the platform guides above for mobile builds.
|
||||
- [GUI architecture](docs/gui-architecture.md) (Chinese): desktop UI, platform implementation, and project structure.
|
||||
- [Connection architecture](docs/connection-architecture.md) (Chinese): discovery, authentication, negotiation, data paths, and lifecycle for ID and direct IP modes.
|
||||
- [FAQ](docs/FAQ.md#english): connections, blank video, permissions, received files, and build issues.
|
||||
- [Report an issue or suggestion](https://github.com/kunkundi/crossdesk/issues): include both operating systems, client versions, connection mode, and reproduction steps.
|
||||
|
||||
The optional desktop build parameter `CROSSDESK_UPDATE_PUBLIC_KEY` verifies Ed25519 signatures on in-app update manifests. Without it, the app still builds, runs, and checks for new versions, but updates require manual download. In-app update installation requires a public key matching the release signing private key. See [Update public key](docs/BUILD_EN.md#update-public-key) for configuration and the [desktop updater guide](docs/UPDATER.md) (Chinese) for key generation and publishing.
|
||||
|
||||
<a id="sponsor"></a>
|
||||
|
||||
## Support CrossDesk
|
||||
|
||||
If CrossDesk is useful to you, consider sponsoring its continued development, cross-platform testing, and maintenance through one of the platforms below. Thank you for your support!
|
||||
|
||||
[Ko-fi](https://ko-fi.com/kunkundi) · [GitHub Sponsors](https://github.com/sponsors/kunkundi) · [AFDIAN](https://afdian.com/a/kunkundi)
|
||||
|
||||
## Acknowledgements and license
|
||||
|
||||
Thanks to [HelloGitHub](https://hellogithub.com/), [Ruanyf Weekly](https://github.com/ruanyf/weekly), and the [LinuxDo](https://linux.do) community for featuring CrossDesk and contributing feedback.
|
||||
|
||||
CrossDesk is licensed under [GPL-3.0](LICENSE). See the [Privacy Policy](PRIVACY.md#english).
|
||||
Copyright (c) 2026 JUNKUN DI.
|
||||
|
||||
CrossDesk is licensed under [GPL-3.0-only](LICENSE). See the [Privacy Policy](PRIVACY.md#english).
|
||||
|
||||
### Code signing policy
|
||||
|
||||
@@ -323,7 +201,3 @@ CrossDesk uses SignPath.io to sign official Windows releases built from this rep
|
||||
|
||||
- **Committers and reviewers:** [kunkundi](https://github.com/kunkundi)
|
||||
- **Approvers:** [kunkundi](https://github.com/kunkundi)
|
||||
|
||||
## Native Android controller
|
||||
|
||||
The independent [Android project](apps/android/README.md) builds an APK for Android 8.0+ arm64 devices. It reuses MiniRTC and the desktop input protocol for remote video/audio, touch and external keyboard/mouse input, display switching, and text clipboard transfer. H.264 and AV1 currently use software decoding. Phone-as-host, file transfer, and full iOS feature parity are not included. See the [Android build guide](apps/android/README.md) for setup and validation.
|
||||
|
||||
+11
-7
@@ -7,12 +7,14 @@
|
||||
- 官方或自托管 WSS 信令服务器;授权后进入前台即自动注册或登录本机身份,首页实时显示连接、重连及失败状态。发起桌面连接时复用该身份;切换服务器会重新登录,撤回授权会关闭连接。已有远程会话优先通过系统画中画保持连接,画中画不可用时由带连接通知的前台服务兜底;首页空闲信令在后台关闭。
|
||||
- 优先使用 MediaCodec 硬件解码 H.264 / AV1,硬件缓冲区直接提交 Surface;设备无对应硬件、分辨率超限或运行失败时,按原编码格式回退到 OpenH264 / dav1d。软件输出由独立显示线程将 NV12 转 RGBA;只保留最新待显示帧。MiniRTC 原有的通用压缩帧解码队列保持不变。
|
||||
- 48 kHz 单声道 PCM 播放,支持静音,音频队列有容量限制。
|
||||
- 相对鼠标(触控板)和绝对位置模式;单指点击、绝对模式按住拖动、长按右键,以及外接键盘、鼠标。画面手势与 iOS 一致:双指捏合以触点为中心缩放(1–10 倍),放大后双指拖动画面,原始比例下双指上下滑动控制滚轮。
|
||||
- 相对鼠标(触控板)和绝对位置模式;单指点击、绝对模式按住拖动、长按右键,以及外接键盘、鼠标。画面手势与 iOS 一致:双指可在整个触控区域(包括画面外黑边)捏合,以触点为中心缩放(1–10 倍),放大后双指拖动画面,原始比例下双指上下滑动控制滚轮。相对鼠标移到可见画面的上、下边缘后继续向外移动,会反向上下平移画面,不触发左右平移;未放大时和按住虚拟鼠标键拖动时同样生效,停止移动即停止平移,抬手保留位置。移动到极限时,画面可见部分至少占当前显示区域高度的 20%,与缩放倍数无关;整幅画面高度不足该比例时完整保留,范围计入上下黑边;缩放结束及缩回 1 倍时保留平移位置,仅在超过可见边界时限制位置;切换显示器恢复居中。
|
||||
- 横竖屏等比例显示、远端多显示器切换。与 iOS 一致的画面质量(低/中/高)、采集帧率(30/60 fps)、画面偏好;支持能力检测、电脑端确认、失败/5 秒超时回退及连续操作的应答排序。
|
||||
- “设置 → 屏幕方向 → 连接建立后锁定横屏”默认关闭并保存在本机。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受此选项影响。退出会话恢复首页方向,返回后台会话沿用该会话的方向设置。已移除检测手机姿态后解除初始横屏的逻辑。
|
||||
- 根据常见移动设备解码能力和屏幕尺寸设置起始参数:显示长边不超过 1280 像素时默认低画质(最高 720p)/30 fps,其余默认中画质(最高 1080p)/30 fps。用户保存的画面偏好不变,仍可手动选择高画质或 60 fps。
|
||||
- 网络状态按视频、音频、数据、合计展示收发速率和丢包率,并显示实际提交帧率、分辨率、画面延时、平滑 RTT、P2P/TURN 和实际 SRTP 状态。每秒刷新,过期或无效测量显示“—”。
|
||||
- 可拖动的悬浮控制球、三列控制菜单和浮动 ASCII / 电脑键盘、常用快捷键;通过剪贴板发送中文等文本,再手动执行远端粘贴。
|
||||
- 可拖动的悬浮控制球、三列控制菜单;默认使用系统输入法,保留候选词、纠错和听写,确认文字后直接输入远端。电脑键盘固定底部,分“字母数字 / 符号功能”两页,共用末两行布局,按系统键盘高度适配横竖屏;统一切换、收起按钮和 Android 键盘触感反馈。
|
||||
- 悬浮菜单中的“虚拟鼠标”展开 140 × 48 dp 横向左键/滚轮/右键条,跟随悬浮球移动,可随时收起,不压缩远程画面。按住左键或右键后,同一根手指拖动即可拖拽,滑出按键范围仍继续,松开结束;也支持另一指在画面上移动光标。采用无文字的扁平分体按键,中间滚轮外观为 18 × 30 dp,三键均保留 44 × 44 dp 触控范围,通过留白分隔。无阴影、凹凸或按压位移,按压仅变色。等色横条随手指上下循环移动,松手停止;上下滑动可超出按键范围,在画面放大后仍控制远端滚动。切换模式、显示器、进入后台或取消触摸会释放按键。状态菜单按内容高度完整展开,只有安全区域空间不足时才滚动。
|
||||
- 悬浮菜单提供“隐私屏”直接开关,无需进入详情页;按钮按电脑确认的状态显示“隐私屏已开启”或“隐私屏已关闭”,开启与关闭使用相同配色,正常切换不显示额外文字,仅在异常时显示提示。开启时与桌面端一致屏蔽电脑本地键鼠;声明控制能力后可使用电脑的连接时自动开启设置。状态超过 5 秒未更新时停止显示成功,指令 15 秒未确认时提示超时;后台保活与画中画恢复沿用同一会话状态。
|
||||
- 悬浮控制栏支持选择并发送文件、接收电脑端文件、查看确认进度及保存收到的文件;Ctrl+Alt+Del 使用远端 Windows 系统服务,未启用服务时显示提示。
|
||||
- 设备身份与选择保存的连接密码使用 Android Keystore / AES-GCM 加密;首次联网需同意。远程会话在切换应用时自动使用系统画中画,不可用时自动使用通知保活。
|
||||
- 与 iOS 一致的首页卡片、顶部状态/公告/设置入口、密码弹层及分组设置页;最近连接按服务器隔离,长按可删除记录及密码。
|
||||
@@ -24,7 +26,7 @@
|
||||
- 可选保存远程画面预览:默认关闭,经确认开启后,从下一次连接起保存一张 640×360 预览,用于最近连接卡片。支持清除预览,关闭选项或撤回授权会清除已有图片。
|
||||
- “关于 CrossDesk”对齐 iOS 的版本、软件许可、开源组件及源码与构建说明页面,包含 OpenFEC 告知、组件详情和可离线选择复制的完整许可;版本号读取当前 APK,构建时记录应用与 MiniRTC 的基础版本。
|
||||
|
||||
当前不包含安卓被控端、隐私屏或 iOS 全部附加功能。硬件能力取决于设备和编码格式,软件回退时高分辨率或高帧率可能增加功耗;可通过悬浮菜单的画面设置降低画质或采集帧率。暂只构建 `arm64-v8a`,不支持 x86 模拟器或 32 位设备。
|
||||
当前不包含安卓被控端或 iOS 全部附加功能。硬件能力取决于设备和编码格式,软件回退时高分辨率或高帧率可能增加功耗;可通过悬浮菜单的画面设置降低画质或采集帧率。暂只构建 `arm64-v8a`,不支持 x86 模拟器或 32 位设备。
|
||||
|
||||
## 本机更新提示
|
||||
|
||||
@@ -70,7 +72,7 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
|
||||
首次构建会下载并从源码编译依赖,耗时取决于网络。后续编译复用缓存。Android 直接编译 `deps/submodules/minirtc` 中的源码;构建产物和依赖缓存位于 `.native/`,Xmake 工程配置位于 `native/.xmake/`,与桌面和 iOS 分开保存:
|
||||
|
||||
- Gradle 9.3.1、AGP 9.1.0、NDK r28c 固定在工程中。
|
||||
- Xmake 包仓库固定到 `eda39de3fb99b420c168f1ab9ab2d1791e11b662`。
|
||||
- Xmake 依赖仓库沿用 PC 端策略:使用默认仓库或调用环境指定的仓库,不固定或校验仓库提交。Android 编译产物和安装包缓存仍独立保存。
|
||||
- `scripts/build_native.py` 调用独立的 `native/xmake.lua`,直接包含 MiniRTC 子模块的构建定义。构建过程不复制 MiniRTC 源码,也不应用补丁。
|
||||
- MiniRTC 源码内的 Android 分支启用 OpenH264、SVT-AV1 和 dav1d 软件编解码及依赖配方,兼容 OpenFEC 的 Android 头文件,并关闭 UPnP。SVT-AV1 使用内置 ARM CPU 能力检测;当前应用作为控制端只接收视频,库同时提供 AV1 编码能力。TLS 校验位于 MiniRTC 内部。STUN、TURN UDP/TCP 与直接连接仍由 MiniRTC 提供。
|
||||
- MiniRTC 的 `thirdparty/openssl/xmake.lua` 统一维护 iOS 和 Android 配方;Android 显式使用 NDK 的归档索引工具,避免 macOS `ranlib` 损坏 ELF 静态库。
|
||||
@@ -96,7 +98,7 @@ git push origin android-v1.6.1-20261010
|
||||
|
||||
已有 Release 需要补传服务器或刷新下载信息时,可手动运行 `Update version.json from Release` 并填写已有 `source_tag`,无需重新构建。
|
||||
|
||||
工作流使用 Ubuntu 24.04、JDK 17、Python 3.13、SDK 36、Build Tools 36.0.0、NDK r28c,并使用最低支持的 Xmake 3.1.1 验证构建;本地允许使用更新的 Xmake。Gradle 使用工程内的 Wrapper,原生构建器固定 Xmake 包仓库版本。SDK/NDK 使用独立目录缓存,按宿主系统、架构和工具链版本区分;命中缓存时跳过 SDK 安装,未命中时在安装完成后立即保存,即使后续 APK 构建失败,下次也可复用。Xmake 可执行环境、固定版本的包仓库和 pip 下载也有各自的缓存;Ubuntu 系统工具只安装缺失项。
|
||||
工作流使用 Ubuntu 24.04、JDK 17、Python 3.13、SDK 36、Build Tools 36.0.0、NDK r28c,并使用最低支持的 Xmake 3.1.1 验证构建;本地允许使用更新的 Xmake。Gradle 使用工程内的 Wrapper,原生构建器沿用 PC 端的 Xmake 仓库选择方式。SDK/NDK 使用独立目录缓存,按宿主系统、架构和工具链版本区分;命中缓存时跳过 SDK 安装,未命中时在安装完成后立即保存,即使后续 APK 构建失败,下次也可复用。Xmake 可执行环境和 pip 下载也有各自的缓存;Ubuntu 系统工具只安装缺失项。
|
||||
|
||||
Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。GitHub 托管 runner 每次启动仍是新环境;第一次运行或缓存失效时需要安装。不同分支和标签可读取默认分支的缓存,因此建议先在默认分支运行一次 `Build Android` 预热,再创建发布标签。
|
||||
|
||||
@@ -151,7 +153,9 @@ gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
|
||||
|
||||
身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。
|
||||
|
||||
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。浮动键盘切换“电脑”后的“复制 / 粘贴 / 全选”根据远端平台选择 Ctrl 或 macOS Command。
|
||||
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的 Ctrl / Win / Alt / Shift 可锁定组合键;macOS 的 Command 对应 Win。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
|
||||
|
||||
Windows 文本输入使用 Unicode 键盘事件,无需切换电脑输入法或覆盖剪贴板。本次开发版将完整 UTF-8 文本通过 Windows 服务转发到锁屏、登录与 UAC 安全桌面,保留数字、大小写字母、符号、空格及中文等字符,不将密码转换成数字或 ASCII 按键。该通道需要同时更新 Windows 客户端、CrossDesk Service 和会话辅助程序;仅更新 Android,或仅确认旧服务正在运行,均不会启用修复。旧版可能已声明 `supports_text_input`,但只支持普通桌面文本输入,因此“电脑键盘可输入、搜狗等系统输入法不可输入”仍可能出现。锁屏实际输入效果需在更新后的 Windows 环境验证。
|
||||
|
||||
“发送文件”调用系统文件选择器,后台读取所选文件后使用桌面端相同的 `file` / `file_feedback` 协议传输,支持空文件和中文文件名。每次发送一个文件,以 64 KiB 分块、最多 1 MiB 未确认数据限制发送队列;进度来自接收端确认,30 秒没有有效进展则报告失败。接收时校验大小并追踪完整字节范围,允许跨线程回调乱序,未完成的文件不会提供保存入口。同名文件分开存储,不覆盖已有文件;可通过“保存收到的文件”导出最近收到的文件,连续接收多个文件时应逐个保存。
|
||||
|
||||
@@ -245,4 +249,4 @@ AV1 硬件路径尚无对应实机验证;Retroid 上使用 dav1d 软件解码
|
||||
|
||||
设置 → 关于使用与 iOS 相同的分组及入口:版本、软件许可、开源组件、源码与构建说明。软件许可中可分别阅读 GNU GPL 第 3 版及开源软件权利说明;开源组件页单独展示 OpenFEC 告知,其余组件列出名称和许可证,详情包含版本、源码、适用的构建配方和每一份许可文档。内容按 Android 构建生成,不显示未使用的 iOS 依赖,Android 签名说明替换 Apple 签名说明。
|
||||
|
||||
APK 保留完整 `THIRD_PARTY_NOTICES.txt`,并生成轻量的 `ThirdPartyLicenses.json` 索引、72 份独立许可文档及 `SourceMetadata.json`。打开文档时才读取正文,长文按有限长度分块显示,支持文本选择、复制与网页链接;打包验证核对每份正文的 SHA-256。源码页面显示标签或提交版本及未发布修改提示,源码链接指向基础提交。当前 Android 构建说明、构建脚本、MiniRTC、OpenSSL 及 SVT-AV1 配方随 APK 离线提供,避免开发版链接指向基础提交中尚不存在的 Android 文件。
|
||||
APK 保留完整 `THIRD_PARTY_NOTICES.txt`,并生成轻量的 `ThirdPartyLicenses.json` 索引、72 份独立许可文档及 `SourceMetadata.json`。打开文档时才读取正文,长文按有限长度分块显示,支持文本选择、复制与网页链接。构建继续生成并打包许可资料,CI 和 APK 验证不再检查许可证内容、哈希或组件清单。源码页面显示标签或提交版本及未发布修改提示,源码链接指向基础提交。当前 Android 构建说明、构建脚本、MiniRTC、OpenSSL 及 SVT-AV1 配方随 APK 离线提供,避免开发版链接指向基础提交中尚不存在的 Android 文件。
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<application android:theme="@style/AppTheme" android:label="CrossDesk"
|
||||
<application android:name=".CrossDeskApplication" android:localeConfig="@xml/locales_config"
|
||||
android:theme="@style/AppTheme" android:label="CrossDesk"
|
||||
android:icon="@mipmap/ic_launcher" android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:supportsRtl="true" android:usesCleartextTraffic="false">
|
||||
android:supportsRtl="false" android:usesCleartextTraffic="false">
|
||||
<service android:name=".RemoteSessionService" android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
|
||||
@@ -26,18 +26,18 @@ final class AboutPages {
|
||||
LicenseCatalog loaded;try{loaded=new LicenseCatalog(activity);}catch(Exception error){loaded=null;}catalog=loaded;
|
||||
}
|
||||
void show(){
|
||||
LinearLayout content=activity.formPage("about","关于 CrossDesk",settings);
|
||||
LinearLayout version=activity.section(content,"");value(version,"版本",applicationVersion());
|
||||
LinearLayout content=activity.formPage("about",L10n.tr("关于 CrossDesk"),settings);
|
||||
LinearLayout version=activity.section(content,"");value(version,L10n.tr("版本"),applicationVersion());
|
||||
updateRow=ui.row();updateRow.setPadding(ui.dp(16),0,ui.dp(16),0);updateRow.addView(activity.updateDot(),ui.size(8,8));
|
||||
updateLink=ui.action("",BLUE,()->open("https://crossdesk.cn"));updateLink.setGravity(Gravity.START|Gravity.CENTER_VERTICAL);updateLink.setPadding(ui.dp(8),ui.dp(12),0,ui.dp(12));updateRow.addView(updateLink,new LinearLayout.LayoutParams(0,-2,1));version.addView(updateRow);
|
||||
updateStatus=ui.text("",13,false);updateStatus.setPadding(ui.dp(16),ui.dp(8),ui.dp(16),ui.dp(8));version.addView(updateStatus);
|
||||
checkButton=ui.action("检查更新",BLUE,activity::checkAppUpdates);version.addView(checkButton);renderUpdates();ui.gap(content,28);
|
||||
checkButton=ui.action(L10n.tr("检查更新"),BLUE,activity::checkAppUpdates);version.addView(checkButton);renderUpdates();ui.gap(content,28);
|
||||
if(catalog==null){unavailable(content);return;}
|
||||
LinearLayout links=activity.section(content,"");
|
||||
activity.link(links,"软件许可","",()->component(catalog.find("crossdesk"),this::show));ui.divider(links);
|
||||
activity.link(links,"开源组件","",this::components);ui.divider(links);
|
||||
activity.link(links,"源码与构建说明","",this::source);
|
||||
activity.footer(content,"许可与版权声明可离线查阅。");
|
||||
activity.link(links,L10n.tr("软件许可"),"",()->component(catalog.find("crossdesk"),this::show));ui.divider(links);
|
||||
activity.link(links,L10n.tr("开源组件"),"",this::components);ui.divider(links);
|
||||
activity.link(links,L10n.tr("源码与构建说明"),"",this::source);
|
||||
activity.footer(content,L10n.tr("许可与版权声明可离线查阅。"));
|
||||
}
|
||||
private String applicationVersion(){
|
||||
return activity.applicationVersion();
|
||||
@@ -45,11 +45,11 @@ final class AboutPages {
|
||||
void renderUpdates(){
|
||||
if(updateStatus==null)return;
|
||||
updateRow.setVisibility(updates.updateAvailable()?View.VISIBLE:View.GONE);
|
||||
updateLink.setText("新版本可用:v"+updates.availableVersion);
|
||||
String status=!activity.hasNetworkConsent()?"完成隐私授权后可检查更新":switch(updates.status){
|
||||
case CHECKING -> "正在检查更新…";
|
||||
case UP_TO_DATE -> "当前已是最新版本";
|
||||
case FAILED -> "检查失败,请稍后重试";
|
||||
updateLink.setText(L10n.format("新版本可用:v{0}",updates.availableVersion));
|
||||
String status=!activity.hasNetworkConsent()?L10n.tr("完成隐私授权后可检查更新"):switch(updates.status){
|
||||
case CHECKING -> L10n.tr("正在检查更新…");
|
||||
case UP_TO_DATE -> L10n.tr("当前已是最新版本");
|
||||
case FAILED -> L10n.tr("检查失败,请稍后重试");
|
||||
default -> "";
|
||||
};
|
||||
updateStatus.setText(status);updateStatus.setVisibility(status.isEmpty()?View.GONE:View.VISIBLE);
|
||||
@@ -58,11 +58,11 @@ final class AboutPages {
|
||||
checkButton.setAlpha(checkButton.isEnabled()?1f:.5f);
|
||||
}
|
||||
private void components(){
|
||||
LinearLayout content=activity.formPage("open-source","开源组件",this::show);
|
||||
LinearLayout notice=activity.section(content,"OpenFEC 告知");
|
||||
paragraph(notice,"本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。",SECONDARY);
|
||||
LinearLayout content=activity.formPage("open-source",L10n.tr("开源组件"),this::show);
|
||||
LinearLayout notice=activity.section(content,L10n.tr("OpenFEC 告知"));
|
||||
paragraph(notice,L10n.tr("本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。"),SECONDARY);
|
||||
ui.divider(notice);paragraph(notice,"(c) Copyright 2009 - 2012 INRIA - All rights reserved",INK);ui.divider(notice);
|
||||
activity.link(notice,"OpenFEC 许可与源码","",()->component(catalog.find("openfec"),this::components));ui.gap(content,28);
|
||||
activity.link(notice,L10n.tr("OpenFEC 许可与源码"),"",()->component(catalog.find("openfec"),this::components));ui.gap(content,28);
|
||||
LinearLayout list=activity.section(content,"");boolean first=true;
|
||||
for(LicenseCatalog.Component item:catalog.components){
|
||||
if(item.id.equals("crossdesk")||item.id.equals("openfec"))continue;
|
||||
@@ -73,41 +73,41 @@ final class AboutPages {
|
||||
row.addView(labels,new LinearLayout.LayoutParams(0,-2,1));TextView chevron=ui.text(" ›",23,false);chevron.setTextColor(0xFFB7B7BD);row.addView(chevron);
|
||||
row.setContentDescription(item.name+","+item.license);row.setClickable(true);row.setFocusable(true);row.setOnClickListener(v->component(item,this::components));list.addView(row);
|
||||
}
|
||||
activity.footer(content,"各组件遵循所列开源许可证,其授予的权利不受本应用普通使用条款限制。查看源码需要网络连接。");
|
||||
activity.footer(content,L10n.tr("各组件遵循所列开源许可证,其授予的权利不受本应用普通使用条款限制。查看源码需要网络连接。"));
|
||||
}
|
||||
private void component(LicenseCatalog.Component item,Runnable back){
|
||||
LinearLayout content=activity.formPage("license:"+item.id,item.name,back);LinearLayout info=activity.section(content,"");
|
||||
String version=item.id.equals("crossdesk")?applicationVersion():item.version.length()==40?item.version.substring(0,12):item.version;
|
||||
value(info,"版本",version);ui.divider(info);value(info,"许可",item.license);ui.divider(info);
|
||||
external(info,"查看源码",item.sourceURL);
|
||||
value(info,L10n.tr("版本"),version);ui.divider(info);value(info,L10n.tr("许可"),item.license);ui.divider(info);
|
||||
external(info,L10n.tr("查看源码"),item.sourceURL);
|
||||
if(!item.buildDocuments.isEmpty()||!item.buildSourceURL.isEmpty()){
|
||||
ui.divider(info);
|
||||
if(!item.buildDocuments.isEmpty())activity.link(info,"构建配方与项目补丁","",()->buildSources(item,back));
|
||||
else external(info,"构建配方与项目补丁",item.buildSourceURL);
|
||||
if(!item.buildDocuments.isEmpty())activity.link(info,L10n.tr("构建配方与项目补丁"),"",()->buildSources(item,back));
|
||||
else external(info,L10n.tr("构建配方与项目补丁"),item.buildSourceURL);
|
||||
}
|
||||
ui.gap(content,16);LinearLayout docs=activity.section(content,"许可与版权声明");
|
||||
ui.gap(content,16);LinearLayout docs=activity.section(content,L10n.tr("许可与版权声明"));
|
||||
for(int i=0;i<item.documents.size();i++){
|
||||
if(i>0)ui.divider(docs);LicenseCatalog.Document document=item.documents.get(i);
|
||||
activity.link(docs,item.documentTitle(document),"",()->document(item.documentTitle(document),document,()->component(item,back)));
|
||||
}
|
||||
}
|
||||
private void source(){
|
||||
LinearLayout content=activity.formPage("source","源码与构建说明",this::show);
|
||||
LinearLayout info=activity.section(content,"本版本源码与构建说明");
|
||||
if(catalog.source==null){paragraph(info,"本版本源码信息暂不可用。",SECONDARY);ui.divider(info);external(info,"项目主页","https://github.com/kunkundi/crossdesk");return;}
|
||||
LinearLayout content=activity.formPage("source",L10n.tr("源码与构建说明"),this::show);
|
||||
LinearLayout info=activity.section(content,L10n.tr("本版本源码与构建说明"));
|
||||
if(catalog.source==null){paragraph(info,L10n.tr("本版本源码信息暂不可用。"),SECONDARY);ui.divider(info);external(info,L10n.tr("项目主页"),"https://github.com/kunkundi/crossdesk");return;}
|
||||
String revision=catalog.source.optString("revision","");String tag=catalog.source.isNull("tag")?"":catalog.source.optString("tag","");
|
||||
value(info,"源码版本",tag.isEmpty()?revision.substring(0,Math.min(12,revision.length())):tag);ui.divider(info);
|
||||
if(catalog.source.optBoolean("isModified")){paragraph(info,"开发版本包含尚未发布的修改,以下链接为基础版本源码。",SECONDARY);ui.divider(info);}
|
||||
external(info,"查看源码",catalog.source.optString("sourceURL",catalog.find("crossdesk").sourceURL));ui.divider(info);
|
||||
value(info,L10n.tr("源码版本"),tag.isEmpty()?revision.substring(0,Math.min(12,revision.length())):tag);ui.divider(info);
|
||||
if(catalog.source.optBoolean("isModified")){paragraph(info,L10n.tr("开发版本包含尚未发布的修改,以下链接为基础版本源码。"),SECONDARY);ui.divider(info);}
|
||||
external(info,L10n.tr("查看源码"),catalog.source.optString("sourceURL",catalog.find("crossdesk").sourceURL));ui.divider(info);
|
||||
try{
|
||||
List<LicenseCatalog.Document> documents=LicenseCatalog.documents(catalog.source.optJSONArray("buildDocuments"));
|
||||
if(!documents.isEmpty())actionLink(info,"构建说明",()->document("构建说明",documents.get(0),this::source));
|
||||
else external(info,"构建说明",catalog.source.optString("buildInstructionsURL","https://github.com/kunkundi/crossdesk"));
|
||||
}catch(org.json.JSONException error){paragraph(info,"本版本源码信息暂不可用。",SECONDARY);}
|
||||
if(!documents.isEmpty())actionLink(info,L10n.tr("构建说明"),()->document(L10n.tr("构建说明"),documents.get(0),this::source));
|
||||
else external(info,L10n.tr("构建说明"),catalog.source.optString("buildInstructionsURL","https://github.com/kunkundi/crossdesk"));
|
||||
}catch(org.json.JSONException error){paragraph(info,L10n.tr("本版本源码信息暂不可用。"),SECONDARY);}
|
||||
}
|
||||
private void buildSources(LicenseCatalog.Component item,Runnable back){
|
||||
LinearLayout content=activity.formPage("build:"+item.id,"构建配方与项目补丁",()->component(item,back));
|
||||
if(!item.buildSourceURL.isEmpty()){LinearLayout upstream=activity.section(content,"");external(upstream,"查看源码",item.buildSourceURL);ui.gap(content,28);}
|
||||
LinearLayout content=activity.formPage("build:"+item.id,L10n.tr("构建配方与项目补丁"),()->component(item,back));
|
||||
if(!item.buildSourceURL.isEmpty()){LinearLayout upstream=activity.section(content,"");external(upstream,L10n.tr("查看源码"),item.buildSourceURL);ui.gap(content,28);}
|
||||
LinearLayout docs=activity.section(content,"");
|
||||
for(int i=0;i<item.buildDocuments.size();i++){
|
||||
if(i>0)ui.divider(docs);LicenseCatalog.Document document=item.buildDocuments.get(i);
|
||||
@@ -129,10 +129,10 @@ final class AboutPages {
|
||||
private void actionLink(LinearLayout parent,String title,Runnable action){TextView link=ui.action(title,BLUE,action);link.setTypeface(android.graphics.Typeface.DEFAULT);link.setGravity(Gravity.START|Gravity.CENTER_VERTICAL);link.setPadding(ui.dp(16),ui.dp(12),ui.dp(16),ui.dp(12));parent.addView(link);}
|
||||
private void open(String url){
|
||||
Uri uri=Uri.parse(url);if(!"https".equals(uri.getScheme())&&!"http".equals(uri.getScheme()))return;
|
||||
try{activity.startActivity(new Intent(Intent.ACTION_VIEW,uri).addCategory(Intent.CATEGORY_BROWSABLE));}catch(ActivityNotFoundException error){Toast.makeText(activity,"未找到可打开链接的浏览器",Toast.LENGTH_SHORT).show();}
|
||||
try{activity.startActivity(new Intent(Intent.ACTION_VIEW,uri).addCategory(Intent.CATEGORY_BROWSABLE));}catch(ActivityNotFoundException error){Toast.makeText(activity,L10n.tr("未找到可打开链接的浏览器"),Toast.LENGTH_SHORT).show();}
|
||||
}
|
||||
private void unavailable(LinearLayout content){
|
||||
content.setGravity(Gravity.CENTER);ui.gap(content,32);TextView icon=ui.text("▤",36,false);icon.setTextColor(SECONDARY);content.addView(icon);ui.gap(content,12);
|
||||
content.addView(ui.text("无法读取开源许可",17,true));TextView message=ui.text("应用中的许可文件缺失或损坏,请重新安装应用。",14,false);message.setGravity(Gravity.CENTER);message.setTextColor(SECONDARY);content.addView(message);
|
||||
content.addView(ui.text(L10n.tr("无法读取开源许可"),17,true));TextView message=ui.text(L10n.tr("应用中的许可文件缺失或损坏,请重新安装应用。"),14,false);message.setGravity(Gravity.CENTER);message.setTextColor(SECONDARY);content.addView(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,15 +51,15 @@ final class AnnouncementView extends ScrollView {
|
||||
TextView text=ui.text(value,14,false);text.setTextColor(color);text.setLineSpacing(ui.dp(3),1);return text;
|
||||
}
|
||||
private void renderList(){
|
||||
if(inbox.deleteFailed){content.addView(message("无法删除这条公告,请重试。",RED));ui.gap(content,12);}
|
||||
if(inbox.deleteFailed){content.addView(message(L10n.tr("无法删除这条公告,请重试。"),RED));ui.gap(content,12);}
|
||||
if(!inbox.isConnected()){
|
||||
content.addView(message("连接服务器后可获取最新公告。",SECONDARY));
|
||||
content.addView(ui.action("连接服务器",BLUE,authorize));ui.gap(content,8);
|
||||
content.addView(message(L10n.tr("连接服务器后可获取最新公告。"),SECONDARY));
|
||||
content.addView(ui.action(L10n.tr("连接服务器"),BLUE,authorize));ui.gap(content,8);
|
||||
}else if(inbox.failed){
|
||||
LinearLayout error=ui.row();error.addView(message("公告加载失败,请重试。",SECONDARY),new LinearLayout.LayoutParams(0,-2,1));
|
||||
error.addView(ui.action("重试",BLUE,inbox::refresh),ui.size(64,48));content.addView(error);ui.gap(content,8);
|
||||
LinearLayout error=ui.row();error.addView(message(L10n.tr("公告加载失败,请重试。"),SECONDARY),new LinearLayout.LayoutParams(0,-2,1));
|
||||
error.addView(ui.action(L10n.tr("重试"),BLUE,inbox::refresh),ui.size(64,48));content.addView(error);ui.gap(content,8);
|
||||
}
|
||||
countLabel=ui.text("共 "+inbox.total+" 条 · "+inbox.unread+" 条未读",13,false);
|
||||
countLabel=ui.text(L10n.format("共 {0} 条 · {1} 条未读",inbox.total,inbox.unread),13,false);
|
||||
countLabel.setTextColor(SECONDARY);countLabel.setPadding(ui.dp(16),ui.dp(8),0,ui.dp(8));content.addView(countLabel);
|
||||
LinearLayout card=ui.column();ui.card(card,12);content.addView(card);
|
||||
for(AnnouncementInbox.Item item:inbox.items){
|
||||
@@ -69,11 +69,11 @@ final class AnnouncementView extends ScrollView {
|
||||
if(inbox.loading){
|
||||
LinearLayout loading=ui.row();loading.setGravity(Gravity.CENTER);loading.setPadding(0,ui.dp(18),0,ui.dp(18));
|
||||
ProgressBar spinner=new ProgressBar(getContext());spinner.setIndeterminateTintList(ColorStateList.valueOf(BLUE));loading.addView(spinner,ui.size(20,20));
|
||||
TextView label=message("加载中…",SECONDARY);label.setPadding(ui.dp(10),0,0,0);loading.addView(label);card.addView(loading);
|
||||
TextView label=message(L10n.tr("加载中…"),SECONDARY);label.setPadding(ui.dp(10),0,0,0);loading.addView(label);card.addView(loading);
|
||||
}else if(inbox.items.size()<inbox.total&&!inbox.failed){
|
||||
TextView more=ui.action("加载更多",BLUE,inbox::loadMore);more.setEnabled(inbox.isConnected());card.addView(more,ui.size(-1,48));
|
||||
TextView more=ui.action(L10n.tr("加载更多"),BLUE,inbox::loadMore);more.setEnabled(inbox.isConnected());card.addView(more,ui.size(-1,48));
|
||||
}else if(inbox.loaded&&inbox.total==0&&!inbox.failed&&inbox.isConnected()){
|
||||
TextView empty=message("暂无公告",SECONDARY);empty.setGravity(Gravity.CENTER);card.addView(empty,ui.size(-1,120));
|
||||
TextView empty=message(L10n.tr("暂无公告"),SECONDARY);empty.setGravity(Gravity.CENTER);card.addView(empty,ui.size(-1,120));
|
||||
}
|
||||
}
|
||||
private View itemRow(AnnouncementInbox.Item item){
|
||||
@@ -86,23 +86,23 @@ final class AnnouncementView extends ScrollView {
|
||||
words.addView(title,ui.size(-1,-2));ui.gap(words,7);TextView date=ui.text(date(item),12,false);date.setTextColor(SECONDARY);words.addView(date);
|
||||
top.addView(words,new LinearLayout.LayoutParams(0,-2,1));row.addView(top,new LinearLayout.LayoutParams(0,-2,1));
|
||||
TextView chevron=ui.text("›",23,false);chevron.setTextColor(0xFFB7B7BD);chevron.setPadding(ui.dp(10),0,0,0);row.addView(chevron);
|
||||
row.setContentDescription(item.title+","+date(item)+","+(item.read?"已读":"未读"));row.setFocusable(true);
|
||||
row.setContentDescription(item.title+","+date(item)+","+(item.read?L10n.tr("已读"):L10n.tr("未读")));row.setFocusable(true);
|
||||
row.setOnClickListener(v->open.accept(item));row.setOnLongClickListener(v->{confirmDelete(item);return true;});
|
||||
return new SwipeRow(row,()->confirmDelete(item));
|
||||
}
|
||||
private void confirmDelete(AnnouncementInbox.Item item){
|
||||
if(deleteDialog!=null)deleteDialog.dismiss();
|
||||
deleteDialog=new AlertDialog.Builder(getContext()).setTitle("删除公告").setMessage("删除这条公告?")
|
||||
.setNegativeButton("取消",null).setPositiveButton("删除",(dialog,which)->inbox.dismiss(item)).show();
|
||||
deleteDialog=new AlertDialog.Builder(getContext()).setTitle(L10n.tr("删除公告")).setMessage(L10n.tr("删除这条公告?"))
|
||||
.setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("删除"),(dialog,which)->inbox.dismiss(item)).show();
|
||||
}
|
||||
private void renderDetail(){
|
||||
AnnouncementInbox.Item item=inbox.find(id,revision);
|
||||
if(item==null){content.addView(message("这条公告已更新或删除,请返回公告列表查看。",SECONDARY));return;}
|
||||
if(item==null){content.addView(message(L10n.tr("这条公告已更新或删除,请返回公告列表查看。"),SECONDARY));return;}
|
||||
content.addView(ui.text(item.title,22,true));ui.gap(content,16);
|
||||
TextView date=ui.text(date(item)+" · "+(item.read?"已读":"未读"),12,false);date.setTextColor(SECONDARY);content.addView(date);ui.gap(content,16);
|
||||
TextView date=ui.text(date(item)+" · "+(item.read?L10n.tr("已读"):L10n.tr("未读")),12,false);date.setTextColor(SECONDARY);content.addView(date);ui.gap(content,16);
|
||||
if(inbox.readSaveFailed){
|
||||
content.addView(message("无法保存本地阅读状态,请重试。",RED));
|
||||
content.addView(ui.action("重试保存阅读状态",BLUE,()->inbox.markRead(item)));ui.gap(content,16);
|
||||
content.addView(message(L10n.tr("无法保存本地阅读状态,请重试。"),RED));
|
||||
content.addView(ui.action(L10n.tr("重试保存阅读状态"),BLUE,()->inbox.markRead(item)));ui.gap(content,16);
|
||||
}
|
||||
TextView body=ui.text("",17,false);body.setText(AnnouncementText.format(item.body));body.setTextIsSelectable(true);
|
||||
body.setMovementMethod(LinkMovementMethod.getInstance());body.setLinksClickable(true);body.setLinkTextColor(BLUE);body.setLineSpacing(ui.dp(4),1);
|
||||
@@ -127,7 +127,7 @@ final class AnnouncementView extends ScrollView {
|
||||
trackPull(event);
|
||||
if(!pulling)return super.onTouchEvent(event);
|
||||
boolean reached=event.getY()-downY>=ui.dp(72);
|
||||
if(event.getActionMasked()==MotionEvent.ACTION_MOVE&&countLabel!=null)countLabel.setText(reached?"松开刷新":"下拉刷新");
|
||||
if(event.getActionMasked()==MotionEvent.ACTION_MOVE&&countLabel!=null)countLabel.setText(reached?L10n.tr("松开刷新"):L10n.tr("下拉刷新"));
|
||||
if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL){
|
||||
pulling=false;if(reached&&event.getActionMasked()==MotionEvent.ACTION_UP)inbox.refresh();else render();
|
||||
}
|
||||
@@ -142,7 +142,7 @@ final class AnnouncementView extends ScrollView {
|
||||
private boolean dragging;
|
||||
SwipeRow(View front,Runnable delete){
|
||||
super(ui.context);this.front=front;setClipChildren(true);
|
||||
remove=ui.action("删除",Color.WHITE,delete);remove.setBackgroundColor(RED);
|
||||
remove=ui.action(L10n.tr("删除"),Color.WHITE,delete);remove.setBackgroundColor(RED);
|
||||
remove.setVisibility(INVISIBLE);
|
||||
remove.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);remove.setFocusable(false);
|
||||
addView(remove,new FrameLayout.LayoutParams(ui.dp(76),-1,Gravity.END));addView(front,new FrameLayout.LayoutParams(-1,-2));
|
||||
|
||||
@@ -19,6 +19,7 @@ final class BackgroundSession implements NativeSession.Listener {
|
||||
RemoteVideoSettings videoSettings;
|
||||
int signalState, selectedDisplay, width, height;
|
||||
boolean muted, remoteServiceAvailable, background, ended;
|
||||
boolean autoLandscape;
|
||||
File receivedFile;
|
||||
|
||||
BackgroundSession(Context context, NativeSession session, String host, int port, String remote) {
|
||||
@@ -45,7 +46,7 @@ final class BackgroundSession implements NativeSession.Listener {
|
||||
if (listener != null) listener.ended(reason);
|
||||
}
|
||||
@Override public void ended(String reason) { finish(reason); }
|
||||
@Override public void passwordRejected() { finish("远端访问密码已失效,请重新连接"); }
|
||||
@Override public void passwordRejected() { finish(L10n.tr("远端访问密码已失效,请重新连接")); }
|
||||
@Override public void connected() { /* Already connected before the service takes ownership. */ }
|
||||
@Override public void status(String message) { if (view != null) view.status(message); }
|
||||
@Override public void signaling(int state, String id) {
|
||||
@@ -74,11 +75,12 @@ final class BackgroundSession implements NativeSession.Listener {
|
||||
@Override public void statistics(RemoteNetworkStatistics.Snapshot value) { if (view != null) view.statistics(value); }
|
||||
@Override public void fileTransfer(String name, double progress, boolean sending, File received) {
|
||||
if (received != null) receivedFile = received;
|
||||
fileStatus = progress < 0 ? name + " 传输失败" : (sending ? "发送 " : "接收 ") + name
|
||||
+ (progress >= 1 ? " · 已完成" : " · " + (int)(progress * 100) + "%");
|
||||
fileStatus = progress < 0 ? L10n.format("{0} 传输失败", name) : (sending ? L10n.tr("发送 ") : L10n.tr("接收 ")) + name
|
||||
+ (progress >= 1 ? L10n.tr(" · 已完成") : " · " + (int)(progress * 100) + "%");
|
||||
if (view != null) view.fileTransfer(name, progress, sending, received);
|
||||
}
|
||||
@Override public void controlFeedback(String text) { if (view != null) view.controlFeedback(text); }
|
||||
@Override public void privacyChanged() { if (view != null) view.privacyChanged(); }
|
||||
@Override public void presence(JSONObject message) { if (view != null) view.presence(message); }
|
||||
@Override public void announcement(JSONObject message) { if (view != null) view.announcement(message); }
|
||||
@Override public void announcementFailed(String id) { if (view != null) view.announcementFailed(id); }
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
/** Initialize localization before any activity, background service or model. */
|
||||
public final class CrossDeskApplication extends Application {
|
||||
@Override public void onCreate() {
|
||||
super.onCreate();
|
||||
L10n.initialize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Native resource lookup; never translates remote content, file names or protocol data. */
|
||||
final class L10n {
|
||||
private static volatile Context application;
|
||||
static void initialize(Context context) { application = context.getApplicationContext(); }
|
||||
static String tr(String key) {
|
||||
Context context = application;
|
||||
Integer id = LocalizationKeys.IDS.get(key);
|
||||
return context == null || id == null ? key : context.getString(id);
|
||||
}
|
||||
static String format(String key, Object... args) {
|
||||
java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("\\{(\\d+)\\}").matcher(tr(key));
|
||||
StringBuffer result = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
int index = Integer.parseInt(matcher.group(1));
|
||||
String value = index < args.length ? String.valueOf(args[index]) : matcher.group();
|
||||
matcher.appendReplacement(result, java.util.regex.Matcher.quoteReplacement(value));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
static boolean isChinese() {
|
||||
Context context = application;
|
||||
Locale locale = context == null ? Locale.getDefault()
|
||||
: context.getResources().getConfiguration().getLocales().get(0);
|
||||
return locale.getLanguage().equals("zh");
|
||||
}
|
||||
private L10n() {}
|
||||
}
|
||||
@@ -28,9 +28,9 @@ final class LicenseCatalog {
|
||||
if(documents.isEmpty())throw new JSONException("Missing documents");
|
||||
}
|
||||
String documentTitle(Document document){
|
||||
if(document.title.equals("Source copyright and license notices"))return "版权与许可声明";
|
||||
if(document.title.equals("apps/android/licenses/OPEN_SOURCE_NOTICE.txt"))return "开源软件权利说明";
|
||||
if(id.equals("crossdesk")&&document.title.equals("LICENSE"))return "GNU GPL 第 3 版";
|
||||
if(document.title.equals("Source copyright and license notices"))return L10n.tr("版权与许可声明");
|
||||
if(document.title.equals("apps/android/licenses/OPEN_SOURCE_NOTICE.txt"))return L10n.tr("开源软件权利说明");
|
||||
if(id.equals("crossdesk")&&document.title.equals("LICENSE"))return L10n.tr("GNU GPL 第 3 版");
|
||||
return document.title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Generated resource keys; text and locale selection live in Android resources. */
|
||||
final class LocalizationKeys {
|
||||
static final Map<String, Integer> IDS = new HashMap<>();
|
||||
static {
|
||||
IDS.put(" · 已完成", R.string.text_58b6df1059088699);
|
||||
IDS.put(" 传输失败", R.string.text_53af552fb3af7f6e);
|
||||
IDS.put(" 台设备", R.string.text_23ebae95d26dfe5e);
|
||||
IDS.put(" 条 · ", R.string.text_a7f4dfd234a43b16);
|
||||
IDS.put(" 条未读", R.string.text_2c894057fc526576);
|
||||
IDS.put("CrossDesk 远程连接", R.string.text_d0ac753c6681e578);
|
||||
IDS.put("GNU GPL 第 3 版", R.string.text_0991c6fc1de4ffd6);
|
||||
IDS.put("OpenFEC 告知", R.string.text_5ae7dc4079223967);
|
||||
IDS.put("OpenFEC 许可与源码", R.string.text_69ad29820a259526);
|
||||
IDS.put("P2P 直连", R.string.text_63ae4a0c7a448d21);
|
||||
IDS.put("SRTP 已启用", R.string.text_70cae3c6aad8e152);
|
||||
IDS.put("TURN 中继", R.string.text_ef813fec7d6d8a3b);
|
||||
IDS.put("{0} 传输失败", R.string.text_ebede975edd52aee);
|
||||
IDS.put("{0} 台设备", R.string.text_b3ffb6e982ecf5df);
|
||||
IDS.put("{0} 条未读", R.string.text_fc66a78b9e27fecf);
|
||||
IDS.put("{0},接收 {1},发送 {2},丢包率 {3}", R.string.text_dcb59cdcf66150a1);
|
||||
IDS.put("● 已连接", R.string.text_01965b3249458d01);
|
||||
IDS.put("下拉刷新", R.string.text_eb4236fe6a926a5a);
|
||||
IDS.put("丢包率", R.string.text_37772187ab854191);
|
||||
IDS.put("中", R.string.text_a567bdaa11367f26);
|
||||
IDS.put("优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。", R.string.text_e6ff95a9fe6acc7f);
|
||||
IDS.put("低", R.string.text_aa9e366f68d3d097);
|
||||
IDS.put("使用自定义服务器", R.string.text_f03d03eb35e3330c);
|
||||
IDS.put("保存密码", R.string.text_2e9f4819e064ce12);
|
||||
IDS.put("保存收到的文件", R.string.text_10019cef593819ed);
|
||||
IDS.put("保存远程画面预览", R.string.text_3bcdcd330bb3044c);
|
||||
IDS.put("保存远程画面预览?", R.string.text_0f197074373e38fd);
|
||||
IDS.put("信令连接中断,请重新连接", R.string.text_71ca53f95a8484da);
|
||||
IDS.put("公告加载失败,请重试。", R.string.text_789f22a8b1b34797);
|
||||
IDS.put("公告详情", R.string.text_00554b17c432937e);
|
||||
IDS.put("共 ", R.string.text_2428420e407fe2e8);
|
||||
IDS.put("共 {0} 条 · {1} 条未读", R.string.text_d977c7568cc5201e);
|
||||
IDS.put("关于", R.string.text_52d25a9e30ba94f1);
|
||||
IDS.put("关于 CrossDesk", R.string.text_459c1e980c1c8ce2);
|
||||
IDS.put("关于,有新版本", R.string.text_390a95b65eb33052);
|
||||
IDS.put("关闭", R.string.text_3fd47edce45b3603);
|
||||
IDS.put("关闭控制栏", R.string.text_56a18c93e12fdc4e);
|
||||
IDS.put("关闭隐私屏", R.string.text_4eb0565cc4658d7f);
|
||||
IDS.put("兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。", R.string.text_7d466b69be42fc43);
|
||||
IDS.put("分辨率", R.string.text_b92421822c213c2b);
|
||||
IDS.put("删除", R.string.text_2f9daa828907b93f);
|
||||
IDS.put("删除公告", R.string.text_0e46cdb3442edfba);
|
||||
IDS.put("删除记录", R.string.text_f2cf9101bb2d8816);
|
||||
IDS.put("删除这条公告?", R.string.text_d087671ca7df7f0f);
|
||||
IDS.put("刷新公告", R.string.text_33e7584cd5c252d7);
|
||||
IDS.put("剪贴板", R.string.text_8c10e2c1728ddf8e);
|
||||
IDS.put("加载中…", R.string.text_4927a53bcc886afb);
|
||||
IDS.put("加载更多", R.string.text_af90a08fec8ee28d);
|
||||
IDS.put("升级提示", R.string.text_a739f24a9fd8a5d9);
|
||||
IDS.put("原生连接初始化失败", R.string.text_861521c9e107c648);
|
||||
IDS.put("发送", R.string.text_edecf0ae6e5144f9);
|
||||
IDS.put("发送 ", R.string.text_43b7b60867ca51e7);
|
||||
IDS.put("发送后,在远程键盘中点击“粘贴”。接收的远端文本可手动复制到本机。", R.string.text_da7df5a5543c4ba1);
|
||||
IDS.put("发送文件", R.string.text_e7281fbca7af369c);
|
||||
IDS.put("发送文本", R.string.text_ce6c02d19022e016);
|
||||
IDS.put("取消", R.string.text_2cd0f3be8738a86c);
|
||||
IDS.put("取消连接", R.string.text_3687f953cfe85fb8);
|
||||
IDS.put("各组件遵循所列开源许可证,其授予的权利不受本应用普通使用条款限制。查看源码需要网络连接。", R.string.text_72814b15a6fa152c);
|
||||
IDS.put("合计", R.string.text_e009c148d1890dac);
|
||||
IDS.put("同意并继续", R.string.text_81c92013219dd115);
|
||||
IDS.put("后台保持连接,点按返回远程桌面", R.string.text_409696774c9998ce);
|
||||
IDS.put("后台保活未能启动,请保持应用在前台", R.string.text_dcb8d4e2e6973912);
|
||||
IDS.put("后台保活未能启动,请保持应用在前台后重新连接", R.string.text_d96a21463557171d);
|
||||
IDS.put("后台连接已结束,请重新连接", R.string.text_952f7300de0a7bd9);
|
||||
IDS.put("后台连接服务已停止,请重新连接", R.string.text_da904f715e1cff87);
|
||||
IDS.put("后续连接仍会保存新的预览图。", R.string.text_0812359e18082071);
|
||||
IDS.put("在线", R.string.text_b9086662b1df1f20);
|
||||
IDS.put("填写主机名或 IPv4 地址,不含协议和路径", R.string.text_39f2f444a042643e);
|
||||
IDS.put("声音", R.string.text_473400815c0f687d);
|
||||
IDS.put("复制远端文本", R.string.text_ab5ca1fab212f4ca);
|
||||
IDS.put("始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。", R.string.text_c92269b2039e7aa3);
|
||||
IDS.put("媒体加密", R.string.text_7ff73de85ec16d15);
|
||||
IDS.put("字母数字", R.string.text_22dc6217af77bc37);
|
||||
IDS.put("安全存储或证书初始化失败,请重试或清除应用数据", R.string.text_9ca1cdaf831182e3);
|
||||
IDS.put("完成", R.string.text_c0b3fbff51ccc40b);
|
||||
IDS.put("完成隐私授权后可检查更新", R.string.text_678328f6f02881c7);
|
||||
IDS.put("完成隐私授权后登记本机身份", R.string.text_c4361f9abdba4882);
|
||||
IDS.put("密码保存失败,本次连接仍可使用", R.string.text_c29b888e357d727d);
|
||||
IDS.put("密码将加密保存在本机 Android Keystore 中", R.string.text_3f31300a23afb390);
|
||||
IDS.put("对端 ID", R.string.text_8d3b052c4c67acda);
|
||||
IDS.put("对端 ID ", R.string.text_0512084e130984bc);
|
||||
IDS.put("对端 ID {0}", R.string.text_c5147d65960196b0);
|
||||
IDS.put("将删除本机保存的所有远程预览图,保留连接记录和密码。", R.string.text_882b705d31ac7e2a);
|
||||
IDS.put("屏幕方向", R.string.text_60cc99cc0013972e);
|
||||
IDS.put("展开远程控制菜单", R.string.text_9e7294d264afecb7);
|
||||
IDS.put("已从通知断开远程连接", R.string.text_9343612ee473edd6);
|
||||
IDS.put("已保存密码", R.string.text_9d32476fe83aef60);
|
||||
IDS.put("已保存密码无法读取,请重新输入", R.string.text_1cbd391383283924);
|
||||
IDS.put("已保存的密码未能保留,请重新输入一次", R.string.text_6792b9f341380c68);
|
||||
IDS.put("已发送 Ctrl+Alt+Del", R.string.text_74d3f0927d465047);
|
||||
IDS.put("已启用后台保活,切换应用可继续保持连接", R.string.text_c28d86acbedba3b6);
|
||||
IDS.put("已启用后台保活,可从连接通知返回或断开", R.string.text_506409ec08cb2a79);
|
||||
IDS.put("已授权", R.string.text_521e65ffc7d0ff2d);
|
||||
IDS.put("已读", R.string.text_efdd1a47e4beb2f9);
|
||||
IDS.put("已连接 ", R.string.text_1f672b9b18d18e87);
|
||||
IDS.put("已连接服务器", R.string.text_9c05d9d7fa1b0abd);
|
||||
IDS.put("帧率", R.string.text_f9404bf335bc14cf);
|
||||
IDS.put("帧率优先", R.string.text_9ae0c69e26db1d6a);
|
||||
IDS.put("平衡", R.string.text_df544e46aa4d5c17);
|
||||
IDS.put("应用中的许可文件缺失或损坏,请重新安装应用。", R.string.text_3362e8fa7f65cc2f);
|
||||
IDS.put("开发版本包含尚未发布的修改,以下链接为基础版本源码。", R.string.text_3f2904435942cc0f);
|
||||
IDS.put("开启保存", R.string.text_4338afaad52ea445);
|
||||
IDS.put("开启隐私屏", R.string.text_c8bb8255390df3b6);
|
||||
IDS.put("开源组件", R.string.text_c6ecaca362ae1c80);
|
||||
IDS.put("开源软件权利说明", R.string.text_6099db946bdb5ee4);
|
||||
IDS.put("当前已是最新版本", R.string.text_6e86570183c3cdd0);
|
||||
IDS.put("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送", R.string.text_24a52788cf474bf6);
|
||||
IDS.put("成功连接后,这里会显示最近的远程设备。", R.string.text_33f25b500697a7d9);
|
||||
IDS.put("我已阅读并同意《隐私政策》", R.string.text_70341256d7317e7d);
|
||||
IDS.put("所选文件(无法读取或空间不足)", R.string.text_85c4c8fda0461f35);
|
||||
IDS.put("找不到该设备 ID", R.string.text_9b58c9ff5f6a856f);
|
||||
IDS.put("指令发送失败,请重试", R.string.text_97b7b18df07b9787);
|
||||
IDS.put("指令已发送,等待电脑确认…", R.string.text_cb782027ca5986c7);
|
||||
IDS.put("授权状态", R.string.text_3ca40a586103f6b7);
|
||||
IDS.put("接收", R.string.text_6e684586884ebee6);
|
||||
IDS.put("接收 ", R.string.text_84ae92e54e3078aa);
|
||||
IDS.put("控制端解码器", R.string.text_d16488b987723161);
|
||||
IDS.put("撤回", R.string.text_6b58198955259066);
|
||||
IDS.put("撤回后将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。", R.string.text_c52e42ee856825db);
|
||||
IDS.put("撤回联网授权", R.string.text_61f4788032ec596b);
|
||||
IDS.put("撤回联网授权?", R.string.text_e736e419008ac10a);
|
||||
IDS.put("收起远程控制菜单", R.string.text_73431dcf0afb7a65);
|
||||
IDS.put("收起键盘", R.string.text_d826c4ece4dd4031);
|
||||
IDS.put("收起鼠标", R.string.text_6e9a457c0c3c34ed);
|
||||
IDS.put("数据", R.string.text_5440f7424f2865bf);
|
||||
IDS.put("文件保存失败,请重试", R.string.text_94244f41d0a41378);
|
||||
IDS.put("文件已保存", R.string.text_2553a785fef2a405);
|
||||
IDS.put("文件选择已超时,会话已结束", R.string.text_d573b3f9a810ea04);
|
||||
IDS.put("文本超过 128 KB", R.string.text_985a5c380be49f4f);
|
||||
IDS.put("断开", R.string.text_f33ac04eece6a6a6);
|
||||
IDS.put("断开后将返回首页", R.string.text_09b1395f50589327);
|
||||
IDS.put("断开远程连接?", R.string.text_a73c0680db0d2e06);
|
||||
IDS.put("断开连接", R.string.text_a33f3804768e68ee);
|
||||
IDS.put("新版本", R.string.text_47ceaf502c5a05d6);
|
||||
IDS.put("新版本可用:v", R.string.text_78823e6cf1a8afe5);
|
||||
IDS.put("新版本可用:v{0}", R.string.text_b7e58b67fa8f7e78);
|
||||
IDS.put("无未读公告", R.string.text_ef23787ec32c5588);
|
||||
IDS.put("无法保存本地阅读状态,请重试。", R.string.text_dca2c418ac967366);
|
||||
IDS.put("无法删除这条公告,请重试。", R.string.text_5c92da76360056cc);
|
||||
IDS.put("无法发起远程连接", R.string.text_615709dd3129313c);
|
||||
IDS.put("无法读取开源许可", R.string.text_a5c4f4ad79a640ae);
|
||||
IDS.put("显示器", R.string.text_1a13ea5910a0c5e6);
|
||||
IDS.put("显示密码", R.string.text_ffe21a671190323e);
|
||||
IDS.put("暂不同意", R.string.text_a94460e8a04f04c5);
|
||||
IDS.put("暂无公告", R.string.text_6d9a7758610a2432);
|
||||
IDS.put("最近连接", R.string.text_0b290c6600d92f43);
|
||||
IDS.put("服务器", R.string.text_2388cbc6a486af9f);
|
||||
IDS.put("服务器 TLS 证书校验失败", R.string.text_dfe408e686175095);
|
||||
IDS.put("服务器地址", R.string.text_d3716cc5a2f5a810);
|
||||
IDS.put("服务器登录失败,请检查服务器地址和网络", R.string.text_2a9608d4465d0f70);
|
||||
IDS.put("服务器端口", R.string.text_d270868960e68629);
|
||||
IDS.put("服务器连接超时,请检查服务器地址和网络", R.string.text_7a4e4a6a1b479a52);
|
||||
IDS.put("未启用", R.string.text_f95ea7f4c063eea7);
|
||||
IDS.put("未找到可打开链接的浏览器", R.string.text_c8f7ed6f2186fb98);
|
||||
IDS.put("未授权", R.string.text_94bc3d40defef3e9);
|
||||
IDS.put("未读", R.string.text_4d0680f9efaef147);
|
||||
IDS.put("未连接服务器 · 点击重试", R.string.text_d1fcb66a323a16f5);
|
||||
IDS.put("本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。", R.string.text_7eea7966c0b746c5);
|
||||
IDS.put("本机 ID ", R.string.text_c611b4e50b0dbc08);
|
||||
IDS.put("本机 ID {0}", R.string.text_97d10220f08dd7fc);
|
||||
IDS.put("本版本源码与构建说明", R.string.text_91f15d505770d7a0);
|
||||
IDS.put("本版本源码信息暂不可用。", R.string.text_5b2c8ca2c5a87241);
|
||||
IDS.put("松开刷新", R.string.text_f0e274aa130e08a2);
|
||||
IDS.put("构建说明", R.string.text_47077144715b7074);
|
||||
IDS.put("构建配方与项目补丁", R.string.text_837ab2d0b2f4cb98);
|
||||
IDS.put("查看源码", R.string.text_921c3b62f1f7d6f4);
|
||||
IDS.put("检查失败,请稍后重试", R.string.text_fdd1059f10c4b979);
|
||||
IDS.put("检查更新", R.string.text_7f68ebad19ba6bcd);
|
||||
IDS.put("正在关闭隐私屏…", R.string.text_4f4d1e704e38c161);
|
||||
IDS.put("正在建立安全连接…", R.string.text_2a05f6fbd3f4436e);
|
||||
IDS.put("正在开启隐私屏…", R.string.text_0da575e3c92de73a);
|
||||
IDS.put("正在检查更新…", R.string.text_91834aebecabcbc3);
|
||||
IDS.put("正在清除…", R.string.text_0571a5f5f8ec4c83);
|
||||
IDS.put("正在获取本机 ID…", R.string.text_0e6336c8fe918730);
|
||||
IDS.put("正在读取所选文件…", R.string.text_faebd20c2801fd0f);
|
||||
IDS.put("正在连接", R.string.text_694186911ae3e4da);
|
||||
IDS.put("正在连接信令服务器…", R.string.text_c3ccc7f7d37533e3);
|
||||
IDS.put("正在连接服务器…", R.string.text_4024d3c0f467a4f6);
|
||||
IDS.put("正在重连服务器…", R.string.text_46fe1ef52ec3a10f);
|
||||
IDS.put("此设备没有可用的文件选择器", R.string.text_4e4fce649fc00238);
|
||||
IDS.put("清除", R.string.text_bce2377283c2455a);
|
||||
IDS.put("清除预览图", R.string.text_ee0e76b8507212de);
|
||||
IDS.put("清除预览图?", R.string.text_ecc809e44637de18);
|
||||
IDS.put("源码与构建说明", R.string.text_2782e4485ad23d34);
|
||||
IDS.put("源码版本", R.string.text_1f566fe8f8b718f3);
|
||||
IDS.put("版本", R.string.text_5f76b2bf82dd2c5d);
|
||||
IDS.put("版权与许可声明", R.string.text_16410ee9663fad10);
|
||||
IDS.put("电脑键盘", R.string.text_11de77e1e41e680f);
|
||||
IDS.put("画质优先", R.string.text_20f57900234debd7);
|
||||
IDS.put("画面偏好", R.string.text_43b04aba53a54cf5);
|
||||
IDS.put("画面延时", R.string.text_82a5600c470179fa);
|
||||
IDS.put("画面设置", R.string.text_33b458faaf967e15);
|
||||
IDS.put("画面质量", R.string.text_afb5acdf8f07fadb);
|
||||
IDS.put("画面采集帧率", R.string.text_12b41b7a3fbcf8ca);
|
||||
IDS.put("相对位置", R.string.text_66062971234f422d);
|
||||
IDS.put("相对鼠标", R.string.text_c421dc051bea3f11);
|
||||
IDS.put("知道了", R.string.text_de32e20193ad1042);
|
||||
IDS.put("确定", R.string.text_fac2a67ad87807c4);
|
||||
IDS.put("确认", R.string.text_36f33adaf0942634);
|
||||
IDS.put("确认连接", R.string.text_a5b7fba4dbd8f622);
|
||||
IDS.put("离线", R.string.text_be1b4f3c6c1cda1e);
|
||||
IDS.put("空格", R.string.text_bfa4a6e0fe42ac28);
|
||||
IDS.put("端口范围为 1–65535", R.string.text_471a579e8d87faac);
|
||||
IDS.put("符号功能", R.string.text_c7d0c77862e32b52);
|
||||
IDS.put("等待电脑返回隐私屏状态;旧版电脑端可能不支持", R.string.text_a562f07bd38c1a49);
|
||||
IDS.put("等待远端屏幕信息", R.string.text_5ee880e2524a983a);
|
||||
IDS.put("等待远端画面…", R.string.text_674c723a85ee7f57);
|
||||
IDS.put("等待隐私授权", R.string.text_119f0483da4756c4);
|
||||
IDS.put("系统键盘", R.string.text_7916071c1a8ee458);
|
||||
IDS.put("绝对位置", R.string.text_02e9ba6fda719e2e);
|
||||
IDS.put("绝对鼠标", R.string.text_8527c8c6d1ccc1ca);
|
||||
IDS.put("网络状态", R.string.text_872428a7210d5e0c);
|
||||
IDS.put("自定义", R.string.text_4eafa9e925b30bcd);
|
||||
IDS.put("虚拟鼠标", R.string.text_dffd0c12b8f27247);
|
||||
IDS.put("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。", R.string.text_62c199f37bf95869);
|
||||
IDS.put("被控端版本过低,请升级到最新版本。", R.string.text_a569ce729c1d6e19);
|
||||
IDS.put("被控端编码器", R.string.text_d814476826827b78);
|
||||
IDS.put("要发送到远端的文本", R.string.text_488402883e27f6f0);
|
||||
IDS.put("视频", R.string.text_c20f7618d330a854);
|
||||
IDS.put("许可", R.string.text_310c625425f014d8);
|
||||
IDS.put("许可与版权声明", R.string.text_5c34f7d0e2124dca);
|
||||
IDS.put("许可与版权声明可离线查阅。", R.string.text_e24134b643a17975);
|
||||
IDS.put("设备身份保存失败,请检查安全存储", R.string.text_5e7628f043ce71d0);
|
||||
IDS.put("设备身份及选择保存的访问密码使用 Android Keystore 加密。连接记录仅保存在本机,不纳入设备备份。剪贴板内容在结束会话时清除。离开应用后通过画中画或连接通知保持会话,可在应用或通知中断开。", R.string.text_5d35ee0d7945fcbc);
|
||||
IDS.put("设置", R.string.text_df3d58c7d84b85f2);
|
||||
IDS.put("设置,有新版本", R.string.text_4f8771952222a011);
|
||||
IDS.put("访问密码", R.string.text_f4f712d5be91721a);
|
||||
IDS.put("访问密码错误,请重新输入", R.string.text_dd6c465847af1b60);
|
||||
IDS.put("证书校验失败 · 点击重试", R.string.text_defba1d2989bfab4);
|
||||
IDS.put("请先完成联网授权", R.string.text_44f3d4d52347095a);
|
||||
IDS.put("请在远端启用 CrossDesk 系统服务后重试", R.string.text_2a07ca002bc5093d);
|
||||
IDS.put("请求超时,尚未确认电脑的隐私屏状态", R.string.text_990fd23c62de5ccf);
|
||||
IDS.put("请等待当前文件发送完成", R.string.text_e5c8fa6dcf7b2a8b);
|
||||
IDS.put("请输入 1–65535 范围内的端口", R.string.text_9305d9d72b06110d);
|
||||
IDS.put("请输入 9 位设备 ID", R.string.text_3c7223dfb8dde5cf);
|
||||
IDS.put("请输入有效访问密码", R.string.text_285a0714894d2899);
|
||||
IDS.put("调整失败,请重试。", R.string.text_c808bb77ffd2b4cd);
|
||||
IDS.put("软件许可", R.string.text_a762e7e650e285fa);
|
||||
IDS.put("返回", R.string.text_572cf45ba43634b3);
|
||||
IDS.put("返回控制栏", R.string.text_7565f94031582c7c);
|
||||
IDS.put("还没有连接记录", R.string.text_55113b6925c362fd);
|
||||
IDS.put("这条公告已更新或删除,请返回公告列表查看。", R.string.text_3aa527b2106cc987);
|
||||
IDS.put("远程任务已关闭", R.string.text_e357aa9a0fb28c16);
|
||||
IDS.put("远程桌面", R.string.text_c255841f68096d3b);
|
||||
IDS.put("远程桌面触控区域", R.string.text_133f4c7dabd88b97);
|
||||
IDS.put("远程桌面:轻触点击,拖动移动,长按右键,双指捏合缩放,放大后双指拖动画面,原始比例下双指上下滑动滚动", R.string.text_5931aeb3778f28f1);
|
||||
IDS.put("远程电脑", R.string.text_3535087994d050de);
|
||||
IDS.put("远程窗口已关闭,或后台保活未能启动,会话已结束", R.string.text_ed2e03072ce131de);
|
||||
IDS.put("远程连接", R.string.text_678b783fb578172b);
|
||||
IDS.put("远端密码错误", R.string.text_deccbd3eb9ba45f2);
|
||||
IDS.put("远端已关闭连接", R.string.text_a2ce499295e358ae);
|
||||
IDS.put("远端当前不可用", R.string.text_176e117df3ace734);
|
||||
IDS.put("远端访问密码已失效,请重新连接", R.string.text_d2c0694243f93249);
|
||||
IDS.put("连接", R.string.text_a5574109f0208e89);
|
||||
IDS.put("连接 ", R.string.text_1d54bfbae5f8ac08);
|
||||
IDS.put("连接失败", R.string.text_89c4766afc85125f);
|
||||
IDS.put("连接失败 · 点击重试", R.string.text_04bf1a61ba1369f6);
|
||||
IDS.put("连接已断开", R.string.text_2ab7d89557e779ab);
|
||||
IDS.put("连接已结束", R.string.text_c1d20bf4c3e471c1);
|
||||
IDS.put("连接延时(RTT)", R.string.text_e57d2cbc0e9908e6);
|
||||
IDS.put("连接建立后可调整,需被控端支持。", R.string.text_db4d4f0fab78498a);
|
||||
IDS.put("连接建立后锁定横屏", R.string.text_2123a9ed718f2c40);
|
||||
IDS.put("连接提示", R.string.text_f741e0536210eafa);
|
||||
IDS.put("连接方式", R.string.text_485a26050ce57431);
|
||||
IDS.put("连接服务器", R.string.text_5a048351a77fe88d);
|
||||
IDS.put("连接服务器后可获取最新公告。", R.string.text_5854c4add7ab0f7a);
|
||||
IDS.put("连接超时,请检查设备 ID、服务器及远端在线状态", R.string.text_32cd27b5a72515f3);
|
||||
IDS.put("连接远程桌面", R.string.text_6edc4b54cc1a9838);
|
||||
IDS.put("退格", R.string.text_d5407b4f3c666681);
|
||||
IDS.put("通知公告", R.string.text_e02926784fcedb91);
|
||||
IDS.put("部分预览图未能清除,请重试。", R.string.text_465a092680d132b3);
|
||||
IDS.put("重试", R.string.text_b8784c8dd5636ff2);
|
||||
IDS.put("重试保存阅读状态", R.string.text_a504fb6f162bdb20);
|
||||
IDS.put("锁定", R.string.text_ff9872a023e3a8aa);
|
||||
IDS.put("键盘", R.string.text_864200bbb241e067);
|
||||
IDS.put("阅读并授权", R.string.text_1f0f014d4177a0cc);
|
||||
IDS.put("隐私", R.string.text_86651d17a401c55b);
|
||||
IDS.put("隐私与授权", R.string.text_26c6e011e343708c);
|
||||
IDS.put("隐私屏", R.string.text_6cad42fd3da85585);
|
||||
IDS.put("隐私屏已关闭", R.string.text_e157fafb76aad954);
|
||||
IDS.put("隐私屏已开启", R.string.text_bbef0eac0c04f214);
|
||||
IDS.put("隐私屏已被解锁", R.string.text_0c7535f7c5e43b6f);
|
||||
IDS.put("隐私屏异常", R.string.text_b62ac3dfb606c00b);
|
||||
IDS.put("隐私屏暂不可用", R.string.text_598a2747dbd21dd5);
|
||||
IDS.put("隐私屏状态已过期,等待电脑确认", R.string.text_4d59be3c75189032);
|
||||
IDS.put("隐私屏状态待确认", R.string.text_1674532c70ed1db2);
|
||||
IDS.put("隐私政策", R.string.text_e464b41fd5c45ba5);
|
||||
IDS.put("隐私政策暂时无法读取,请拒绝联网并重新安装应用。", R.string.text_554e957e8e3c00af);
|
||||
IDS.put("静音", R.string.text_7db2e189ecb01065);
|
||||
IDS.put("音频", R.string.text_296c632ec857a0ba);
|
||||
IDS.put("项目主页", R.string.text_0cadb49096a382e0);
|
||||
IDS.put("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。", R.string.text_3beca60bd0c654f7);
|
||||
IDS.put("高", R.string.text_b1c27820fec23edb);
|
||||
IDS.put("默认", R.string.text_844b8cc8dff7c1d8);
|
||||
IDS.put("默认关闭。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受影响。", R.string.text_4db8737054f3b0cc);
|
||||
IDS.put("鼠标右键,按住并拖动,松开结束拖拽", R.string.text_1184a97edd8e1646);
|
||||
IDS.put("鼠标左键,按住并拖动,松开结束拖拽", R.string.text_926eaa3af0b5e48e);
|
||||
IDS.put("鼠标控制", R.string.text_269f39a07b4a4e01);
|
||||
IDS.put("鼠标滚轮,上下滑动,可滑出按键范围", R.string.text_cba62aeeb4a63a23);
|
||||
IDS.put(",丢包率 ", R.string.text_9aa238fc6121dede);
|
||||
IDS.put(",发送 ", R.string.text_0b5d16c21095fe10);
|
||||
IDS.put(",已保存密码", R.string.text_ba9a792d063a8c86);
|
||||
IDS.put(",接收 ", R.string.text_f4c1f9d7896760a7);
|
||||
IDS.put(",点击重试", R.string.text_42237b5ec70c26f3);
|
||||
}
|
||||
private LocalizationKeys() {}
|
||||
}
|
||||
@@ -72,12 +72,14 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private int videoWidth,videoHeight;
|
||||
private final RemotePictureInPicture pictureInPicture=new RemotePictureInPicture();
|
||||
private RemoteVideoView video;
|
||||
private RemoteOrientation remoteOrientation;
|
||||
private SessionControls controls;
|
||||
private Dialog passwordDialog, progressDialog, privacyDialog;
|
||||
private AlertDialog disconnectDialog;
|
||||
private AlertDialog connectionDialog;
|
||||
private String pendingConnectionMessage="";
|
||||
private AlertDialog remoteUpdateDialog;
|
||||
private AlertDialog privacyUnlockDialog;
|
||||
private RemoteVersionCheck remoteVersionCheck;
|
||||
private String host, connectedRemote="", hostPlatform="", remoteClipboard="", pendingPassword="", page="home";
|
||||
private int port,selectedDisplay;
|
||||
@@ -87,7 +89,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private static final int PICK_FILE=41,SAVE_FILE=42;
|
||||
private NativeSession documentSession;
|
||||
private java.io.File receivedFile,documentToSave;
|
||||
private final Runnable documentTimeout=()->{if(documentSession!=null&&!foreground)end("文件选择已超时,会话已结束");};
|
||||
private final Runnable documentTimeout=()->{if(documentSession!=null&&!foreground)end(L10n.tr("文件选择已超时,会话已结束"));};
|
||||
private RemoteVideoSettings videoSettings=new RemoteVideoSettings(1);
|
||||
private final android.os.Handler mainHandler=new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
private Runnable videoSettingsTimeout;
|
||||
@@ -95,7 +97,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private Runnable backAction;
|
||||
|
||||
@Override public void onCreate(Bundle state){
|
||||
super.onCreate(state);ui=new MobileUi(this);history=new RecentConnections(this);
|
||||
super.onCreate(state);ui=new MobileUi(this);history=new RecentConnections(this);remoteOrientation=new RemoteOrientation(this);
|
||||
pageHost=new PageHost(this);setContentView(pageHost);
|
||||
preferences=getSharedPreferences("settings",MODE_PRIVATE);host=preferences.getString("host","api.crossdesk.cn");port=preferences.getInt("port",9099);
|
||||
recentPresence.sender=ids->{if(signaling!=null&&!signaling.isClosed())signaling.presenceRequest(ids);};
|
||||
@@ -124,6 +126,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
else motion=navigatingBack||nextPage.equals("home")?PageHost.Motion.POP:PageHost.Motion.PUSH;
|
||||
}
|
||||
if(root!=null)hideKeyboard();page=nextPage;
|
||||
// Remote controls own the IME inset. Android 11–14 otherwise also resize
|
||||
// the content, unlike Android 15's enforced edge-to-edge layout.
|
||||
if(android.os.Build.VERSION.SDK_INT>=30&&android.os.Build.VERSION.SDK_INT<35)getWindow().setDecorFitsSystemWindows(!dark);
|
||||
signalBadge=null;identitySummary=null;announcementBadge=null;announcementView=null;announcementRefresh=null;
|
||||
settingsUpdateDot=null;aboutUpdateDot=null;settingsUpdateButton=null;aboutUpdateRow=null;aboutUpdateLabel=null;
|
||||
previewSwitch=null;clearPreviewsButton=null;previewCleanupError=null;previewCards.clear();recentGrid=null;
|
||||
@@ -132,7 +137,11 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
root=ui.pageColumn(dark?0:nextPage.equals("home")?1200:720);
|
||||
safeArea.addView(root,new FrameLayout.LayoutParams(-1,-1,Gravity.TOP|Gravity.CENTER_HORIZONTAL));
|
||||
safeArea.setOnApplyWindowInsetsListener((view,insets)->{
|
||||
view.setPadding(insets.getSystemWindowInsetLeft(),insets.getSystemWindowInsetTop(),insets.getSystemWindowInsetRight(),insets.getSystemWindowInsetBottom());return insets;
|
||||
if(dark&&android.os.Build.VERSION.SDK_INT>=30){
|
||||
android.graphics.Insets bars=insets.getInsets(WindowInsets.Type.systemBars()|WindowInsets.Type.displayCutout());
|
||||
view.setPadding(bars.left,bars.top,bars.right,bars.bottom);
|
||||
if(controls!=null)controls.keyboardInsets(Math.max(0,insets.getInsets(WindowInsets.Type.ime()).bottom-bars.bottom),insets.isVisible(WindowInsets.Type.ime()),bars.bottom);
|
||||
}else view.setPadding(insets.getSystemWindowInsetLeft(),insets.getSystemWindowInsetTop(),insets.getSystemWindowInsetRight(),insets.getSystemWindowInsetBottom());return insets;
|
||||
});
|
||||
root.setFocusableInTouchMode(true);pageHost.show(canvas,motion);
|
||||
applySystemBars(dark);
|
||||
@@ -149,7 +158,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
}
|
||||
private LinearLayout navigation(String title,Runnable back){
|
||||
LinearLayout bar=row();bar.setPadding(dp(12),0,dp(12),0);root.addView(bar,ui.size(-1,56));
|
||||
bar.addView(ui.iconButton("back","返回",this::navigateBack),ui.size(44,44));
|
||||
bar.addView(ui.iconButton("back",L10n.tr("返回"),this::navigateBack),ui.size(44,44));
|
||||
TextView label=text(title,17,true);label.setGravity(Gravity.CENTER);label.setSingleLine();label.setEllipsize(TextUtils.TruncateAt.END);bar.addView(label,new LinearLayout.LayoutParams(0,-1,1));
|
||||
bar.addView(new View(this),ui.size(44,44));backAction=back;return bar;
|
||||
}
|
||||
@@ -171,14 +180,18 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
TextView chevron=text(" ›",23,false);chevron.setTextColor(0xFFB7B7BD);item.addView(chevron);item.setContentDescription(title+(detail.isEmpty()?"":","+detail));item.setClickable(true);item.setFocusable(true);item.setOnClickListener(v->action.run());parent.addView(item);
|
||||
return item;
|
||||
}
|
||||
@android.annotation.SuppressLint("SourceLockedOrientationActivity") // Phones keep the portrait home; tablets and multi-window follow user rotation settings.
|
||||
private void updateHomeOrientation(){
|
||||
private boolean isTablet(){
|
||||
boolean tablet=getResources().getConfiguration().smallestScreenWidthDp>=600;
|
||||
if(android.os.Build.VERSION.SDK_INT>=30){
|
||||
android.graphics.Rect bounds=getWindowManager().getMaximumWindowMetrics().getBounds();
|
||||
tablet|=Math.min(bounds.width(),bounds.height())>=dp(600);
|
||||
}
|
||||
int orientation=tablet||isInMultiWindowMode()?ActivityInfo.SCREEN_ORIENTATION_FULL_USER:ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
return tablet;
|
||||
}
|
||||
@android.annotation.SuppressLint("SourceLockedOrientationActivity") // Phones keep the portrait home; tablets and multi-window follow user rotation settings.
|
||||
private void updateHomeOrientation(){
|
||||
remoteOrientation.end();
|
||||
int orientation=isTablet()||isInMultiWindowMode()?ActivityInfo.SCREEN_ORIENTATION_FULL_USER:ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
if(getRequestedOrientation()!=orientation)setRequestedOrientation(orientation);
|
||||
}
|
||||
private void home(String message){
|
||||
@@ -187,34 +200,34 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
signalBadge=text("",12,true);signalBadge.setGravity(Gravity.CENTER);signalBadge.setPadding(dp(11),0,dp(11),0);signalBadge.setSingleLine(true);
|
||||
signalBadge.setOnClickListener(v->{
|
||||
if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);
|
||||
else if(signalState==1)toast("已连接 "+serverKey());
|
||||
else if(signalState==1)toast(L10n.tr("已连接 ")+serverKey());
|
||||
else {stopSignaling();ensureSignaling();}
|
||||
});renderSignaling();toolbar.addView(signalBadge,ui.size(-2,34));toolbar.addView(new View(this),new LinearLayout.LayoutParams(0,1,1));
|
||||
FrameLayout announcementButton=(FrameLayout)ui.iconButton("envelope","通知公告",this::announcements);announcementButton.setClipChildren(false);toolbar.setClipChildren(false);toolbar.setClipToPadding(false);
|
||||
FrameLayout announcementButton=(FrameLayout)ui.iconButton("envelope",L10n.tr("通知公告"),this::announcements);announcementButton.setClipChildren(false);toolbar.setClipChildren(false);toolbar.setClipToPadding(false);
|
||||
announcementBadge=text("",10,true);announcementBadge.setTextColor(Color.WHITE);announcementBadge.setGravity(Gravity.CENTER);announcementBadge.setMinWidth(dp(16));announcementBadge.setPadding(dp(4),0,dp(4),0);announcementBadge.setBackground(ui.background(RED,12));announcementBadge.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
FrameLayout.LayoutParams badgePosition=new FrameLayout.LayoutParams(-2,dp(16),Gravity.TOP|Gravity.END);badgePosition.topMargin=-dp(3);badgePosition.setMarginEnd(-dp(5));announcementButton.addView(announcementBadge,badgePosition);renderAnnouncements();
|
||||
toolbar.addView(announcementButton,ui.size(40,40));View spacer=new View(this);toolbar.addView(spacer,ui.size(12,1));
|
||||
FrameLayout settingsButton=(FrameLayout)ui.iconButton("settings","设置",this::settings);settingsButton.setClipChildren(false);settingsUpdateButton=settingsButton;
|
||||
FrameLayout settingsButton=(FrameLayout)ui.iconButton("settings",L10n.tr("设置"),this::settings);settingsButton.setClipChildren(false);settingsUpdateButton=settingsButton;
|
||||
TextView updateBadge=text("!",10,true);updateBadge.setTextColor(Color.WHITE);updateBadge.setGravity(Gravity.CENTER);updateBadge.setIncludeFontPadding(false);updateBadge.setBackground(ui.background(RED,8));updateBadge.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);settingsUpdateDot=updateBadge;
|
||||
FrameLayout.LayoutParams updatePosition=new FrameLayout.LayoutParams(dp(16),dp(16),Gravity.TOP|Gravity.END);updatePosition.topMargin=-dp(3);updatePosition.setMarginEnd(-dp(5));settingsButton.addView(settingsUpdateDot,updatePosition);
|
||||
toolbar.addView(settingsButton,ui.size(40,40));renderAppUpdates();
|
||||
HomeLayout layout=new HomeLayout(ui);root.addView(layout,new LinearLayout.LayoutParams(-1,0,1));LinearLayout content=layout.panels;
|
||||
LinearLayout connection=column();connection.setPadding(dp(18),dp(18),dp(18),dp(18));content.addView(ui.shadowCard(connection,20));
|
||||
connection.addView(text("远程桌面",20,true));ui.gap(connection,16);
|
||||
connection.addView(text(L10n.tr("远程桌面"),20,true));ui.gap(connection,16);
|
||||
LinearLayout entry=row();connection.addView(entry,ui.size(-1,54));
|
||||
remote=field("对端 ID",InputType.TYPE_CLASS_NUMBER);remote.setTextSize(20);remote.setTypeface(Typeface.create("sans-serif-medium",Typeface.NORMAL));
|
||||
remote=field(L10n.tr("对端 ID"),InputType.TYPE_CLASS_NUMBER);remote.setTextSize(20);remote.setTypeface(Typeface.create("sans-serif-medium",Typeface.NORMAL));
|
||||
remote.setOnFocusChangeListener((view,focused)->{GradientDrawable shape=ui.background(BACKGROUND,12);shape.setStroke(dp(focused?2:1),focused?BLUE:LINE);remote.setBackground(shape);});
|
||||
remote.setFilters(new InputFilter[]{new InputFilter.LengthFilter(11)});remote.setText(formatId(preferences.getString("lastRemote","")));
|
||||
entry.addView(remote,new LinearLayout.LayoutParams(0,-1,1));
|
||||
TextView connect=ui.primary("连接 →",()->promptConnection(id()));connect.setContentDescription("连接");LinearLayout.LayoutParams buttonParams=ui.size(96,54);buttonParams.leftMargin=dp(10);entry.addView(connect,buttonParams);
|
||||
TextView connect=ui.primary(L10n.tr("连接"),()->promptConnection(id()));connect.setContentDescription(L10n.tr("连接"));LinearLayout.LayoutParams buttonParams=ui.size(96,54);buttonParams.leftMargin=dp(10);entry.addView(connect,buttonParams);
|
||||
Runnable update=()->{connect.setEnabled(!id().isEmpty());connect.setAlpha(id().isEmpty()?.45f:1);};update.run();
|
||||
remote.addTextChangedListener(new TextWatcher(){boolean editing;public void beforeTextChanged(CharSequence s,int start,int count,int after){}public void onTextChanged(CharSequence s,int start,int before,int count){update.run();}public void afterTextChanged(Editable value){if(editing)return;String formatted=formatId(value.toString());if(!formatted.equals(value.toString())){editing=true;int cursor=remote.getSelectionStart(),digits=value.subSequence(0,Math.max(0,cursor)).toString().replaceAll("[^0-9]","").length();remote.setText(formatted);remote.setSelection(Math.min(formatted.length(),digits+Math.max(0,digits-1)/3));editing=false;}}});
|
||||
LinearLayout recent=column();recent.setPadding(dp(16),dp(16),dp(16),dp(16));ui.card(recent,18);LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(-1,0,1);p.topMargin=dp(20);content.addView(recent,p);
|
||||
JSONArray records=history.list(serverKey());recentPresence.watch(records,android.os.SystemClock.elapsedRealtime());LinearLayout heading=row();heading.addView(text("最近连接",20,true),new LinearLayout.LayoutParams(0,-2,1));
|
||||
if(records.length()>0){TextView count=text(records.length()+" 台设备",12,false);count.setTextColor(SECONDARY);heading.addView(count);}recent.addView(heading);
|
||||
JSONArray records=history.list(serverKey());recentPresence.watch(records,android.os.SystemClock.elapsedRealtime());LinearLayout heading=row();heading.addView(text(L10n.tr("最近连接"),20,true),new LinearLayout.LayoutParams(0,-2,1));
|
||||
if(records.length()>0){TextView count=text(L10n.format("{0} 台设备",records.length()),12,false);count.setTextColor(SECONDARY);heading.addView(count);}recent.addView(heading);
|
||||
if(records.length()==0){
|
||||
LinearLayout empty=column();empty.setGravity(Gravity.CENTER);recent.addView(empty,new LinearLayout.LayoutParams(-1,0,1));empty.addView(ui.icon("history",0xFFB6B6BB),ui.size(34,34));ui.gap(empty,12);
|
||||
TextView emptyTitle=text("还没有连接记录",17,true);emptyTitle.setGravity(Gravity.CENTER);empty.addView(emptyTitle);ui.gap(empty,8);TextView hint=text("成功连接后,这里会显示最近的远程设备。",13,false);hint.setTextColor(SECONDARY);hint.setGravity(Gravity.CENTER);empty.addView(hint);
|
||||
TextView emptyTitle=text(L10n.tr("还没有连接记录"),17,true);emptyTitle.setGravity(Gravity.CENTER);empty.addView(emptyTitle);ui.gap(empty,8);TextView hint=text(L10n.tr("成功连接后,这里会显示最近的远程设备。"),13,false);hint.setTextColor(SECONDARY);hint.setGravity(Gravity.CENTER);empty.addView(hint);
|
||||
}else{
|
||||
ui.gap(recent,14);ScrollView scroll=new ScrollView(this);recent.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));recentGrid=column();scroll.addView(recentGrid);
|
||||
recentGrid.addOnLayoutChangeListener((view,left,top,right,bottom,oldLeft,oldTop,oldRight,oldBottom)->{
|
||||
@@ -230,7 +243,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private void showConnectionMessage(){
|
||||
if(pendingConnectionMessage.isEmpty()||!foreground||isFinishing()||isDestroyed())return;
|
||||
if(connectionDialog!=null)connectionDialog.dismiss();
|
||||
connectionDialog=new AlertDialog.Builder(this).setTitle("连接提示").setMessage(pendingConnectionMessage).setPositiveButton("确定",null).create();
|
||||
connectionDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("连接提示")).setMessage(pendingConnectionMessage).setPositiveButton(L10n.tr("确定"),null).create();
|
||||
connectionDialog.setOnDismissListener(dialog->connectionDialog=null);
|
||||
pendingConnectionMessage="";connectionDialog.show();
|
||||
}
|
||||
@@ -246,27 +259,27 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
if(item==null)return new View(this);String id=item.optString("id"),platform=item.optString("platform");
|
||||
LinearLayout card=column();ui.card(card,14);FrameLayout preview=new FrameLayout(this){@Override protected void onMeasure(int w,int h){super.onMeasure(w,MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(w)*9/16,MeasureSpec.EXACTLY));}};
|
||||
int color=platform.equals("windows")?0xFF316AB7:platform.equals("macos")?0xFF8057B9:platform.equals("linux")?0xFFAE642B:0xFF677587;preview.setBackgroundColor(color);
|
||||
TextView label=text(platform.equals("windows")?"Windows":platform.equals("macos")?"macOS":platform.equals("linux")?"Linux":"远程电脑",19,true);label.setGravity(Gravity.CENTER);label.setTextColor(Color.WHITE);preview.addView(label,new FrameLayout.LayoutParams(-1,-1));card.addView(preview,ui.size(-1,80));
|
||||
TextView label=text(platform.equals("windows")?"Windows":platform.equals("macos")?"macOS":platform.equals("linux")?"Linux":L10n.tr("远程电脑"),19,true);label.setGravity(Gravity.CENTER);label.setTextColor(Color.WHITE);preview.addView(label,new FrameLayout.LayoutParams(-1,-1));card.addView(preview,ui.size(-1,80));
|
||||
ImageView image=new ImageView(this);image.setScaleType(ImageView.ScaleType.CENTER_CROP);image.setVisibility(View.GONE);image.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);preview.addView(image,new FrameLayout.LayoutParams(-1,-1));
|
||||
String server=serverKey();Runnable load=()->{if(!image.isAttachedToWindow())return;Object binding=new Object();image.setTag(binding);image.setImageDrawable(null);image.setVisibility(View.GONE);label.setVisibility(View.VISIBLE);
|
||||
previews.load(server,id,bitmap->{if(image.getTag()!=binding||!image.isAttachedToWindow()){if(bitmap!=null)bitmap.recycle();return;}if(bitmap!=null){image.setImageBitmap(bitmap);image.setVisibility(View.VISIBLE);label.setVisibility(View.GONE);}});};previewCards.add(load);image.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener(){public void onViewAttachedToWindow(View view){load.run();}public void onViewDetachedFromWindow(View view){image.setTag(null);}});if(image.isAttachedToWindow())load.run();
|
||||
LinearLayout caption=column();caption.setPadding(dp(9),dp(9),dp(9),dp(9));TextView name=text(item.optString("name",id),12,true);name.setSingleLine();name.setEllipsize(TextUtils.TruncateAt.END);caption.addView(name);ui.gap(caption,4);
|
||||
LinearLayout details=row();TextView detail=text("ID "+id,10,false);detail.setTypeface(Typeface.create("sans-serif",Typeface.NORMAL));detail.setTextColor(SECONDARY);detail.setSingleLine();detail.setEllipsize(TextUtils.TruncateAt.END);details.addView(detail,new LinearLayout.LayoutParams(0,-2,1));
|
||||
if(item.optBoolean("remember")){View key=ui.icon("key",SECONDARY);key.setContentDescription("已保存密码");LinearLayout.LayoutParams keySize=ui.size(10,10);keySize.leftMargin=dp(4);details.addView(key,keySize);}
|
||||
boolean online=recentPresence.isOnline(id,android.os.SystemClock.elapsedRealtime());String availability=online?"在线":"离线";TextView presence=text(availability,10,true);presence.setSingleLine();presence.setTextColor(online?GREEN:SECONDARY);LinearLayout.LayoutParams presenceSize=ui.size(-2,-2);presenceSize.leftMargin=dp(5);details.addView(presence,presenceSize);caption.addView(details);card.addView(caption);
|
||||
card.setContentDescription("连接 "+item.optString("name",id)+",ID "+id+(item.optBoolean("remember")?",已保存密码":"")+","+availability);card.setClickable(true);card.setFocusable(true);card.setOnClickListener(v->{remote.setText(formatId(id));promptConnection(id);});
|
||||
card.setOnLongClickListener(v->{new AlertDialog.Builder(this).setTitle(item.optString("name",id)).setItems(new String[]{"连接","删除记录"},(d,w)->{if(w==0)promptConnection(id);else{history.delete(serverKey(),id);home("");}}).show();return true;});return card;
|
||||
if(item.optBoolean("remember")){View key=ui.icon("key",SECONDARY);key.setContentDescription(L10n.tr("已保存密码"));LinearLayout.LayoutParams keySize=ui.size(10,10);keySize.leftMargin=dp(4);details.addView(key,keySize);}
|
||||
boolean online=recentPresence.isOnline(id,android.os.SystemClock.elapsedRealtime());String availability=online?L10n.tr("在线"):L10n.tr("离线");TextView presence=text(availability,10,true);presence.setSingleLine();presence.setTextColor(online?GREEN:SECONDARY);LinearLayout.LayoutParams presenceSize=ui.size(-2,-2);presenceSize.leftMargin=dp(5);details.addView(presence,presenceSize);caption.addView(details);card.addView(caption);
|
||||
card.setContentDescription(L10n.tr("连接 ")+item.optString("name",id)+",ID "+id+(item.optBoolean("remember")?L10n.tr(",已保存密码"):"")+","+availability);card.setClickable(true);card.setFocusable(true);card.setOnClickListener(v->{remote.setText(formatId(id));promptConnection(id);});
|
||||
card.setOnLongClickListener(v->{new AlertDialog.Builder(this).setTitle(item.optString("name",id)).setItems(new String[]{L10n.tr("连接"),L10n.tr("删除记录")},(d,w)->{if(w==0)promptConnection(id);else{history.delete(serverKey(),id);home("");}}).show();return true;});return card;
|
||||
}
|
||||
private void promptConnection(String id){
|
||||
hideKeyboard();if(!id.matches("[0-9]{9}")){remote.setError("请输入 9 位设备 ID");remote.requestFocus();return;}
|
||||
hideKeyboard();if(!id.matches("[0-9]{9}")){remote.setError(L10n.tr("请输入 9 位设备 ID"));remote.requestFocus();return;}
|
||||
if(!preferences.getBoolean("networkConsent",false)){consent(()->connectOrPrompt(id));return;}connectOrPrompt(id);
|
||||
}
|
||||
private void connectOrPrompt(String id){
|
||||
String stored;
|
||||
try{stored=history.password(serverKey(),id);}catch(Exception error){passwordSheet(id,"已保存密码无法读取,请重新输入",true);return;}
|
||||
try{stored=history.password(serverKey(),id);}catch(Exception error){passwordSheet(id,L10n.tr("已保存密码无法读取,请重新输入"),true);return;}
|
||||
if(!stored.isEmpty()){rememberPassword=true;begin(id,stored);return;}
|
||||
boolean remember=history.remembersPassword(serverKey(),id);
|
||||
passwordSheet(id,remember?"已保存的密码未能保留,请重新输入一次":"",remember);
|
||||
passwordSheet(id,remember?L10n.tr("已保存的密码未能保留,请重新输入一次"):"",remember);
|
||||
}
|
||||
private void consent(Runnable accepted){
|
||||
if(privacyDialog!=null&&privacyDialog.isShowing())return;
|
||||
@@ -278,15 +291,15 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private void passwordSheet(String id,String error,boolean rememberDefault){
|
||||
Dialog dialog=new Dialog(this);passwordDialog=dialog;LinearLayout sheet=column();sheet.setPadding(dp(22),dp(10),dp(22),dp(22));sheet.setBackground(ui.background(BACKGROUND,24));
|
||||
View handle=new View(this);handle.setBackground(ui.background(0xFFC6C6CB,3));LinearLayout.LayoutParams hp=ui.size(36,5);hp.gravity=Gravity.CENTER;sheet.addView(handle,hp);ui.gap(sheet,20);
|
||||
LinearLayout header=row(),titles=column();titles.addView(text("连接远程桌面",22,true));ui.gap(titles,5);TextView subtitle=text("对端 ID "+id,15,false);subtitle.setTextColor(SECONDARY);titles.addView(subtitle);header.addView(titles,new LinearLayout.LayoutParams(0,-2,1));header.addView(ui.iconButton("close","关闭",dialog::dismiss),ui.size(36,36));sheet.addView(header);ui.gap(sheet,18);
|
||||
LinearLayout passwordRow=row();ui.card(passwordRow,10);EditText password=field("访问密码",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);password.setBackgroundColor(Color.TRANSPARENT);passwordRow.addView(password,new LinearLayout.LayoutParams(0,dp(50),1));
|
||||
View eye=ui.iconButton("eye","显示密码",()->{boolean hidden=password.getTransformationMethod()!=null;password.setTransformationMethod(hidden?null:PasswordTransformationMethod.getInstance());password.setSelection(password.length());});passwordRow.addView(eye,ui.size(44,44));sheet.addView(passwordRow);ui.gap(sheet,18);
|
||||
LinearLayout save=row(),saveText=column();saveText.addView(text("保存密码",15,true));ui.gap(saveText,3);TextView security=text("密码将加密保存在本机 Android Keystore 中",11,false);security.setTextColor(SECONDARY);saveText.addView(security);save.addView(saveText,new LinearLayout.LayoutParams(0,-2,1));Switch remember=new Switch(this);remember.setContentDescription("保存密码");save.addView(remember);sheet.addView(save);
|
||||
LinearLayout header=row(),titles=column();titles.addView(text(L10n.tr("连接远程桌面"),22,true));ui.gap(titles,5);TextView subtitle=text(L10n.format("对端 ID {0}",id),15,false);subtitle.setTextColor(SECONDARY);titles.addView(subtitle);header.addView(titles,new LinearLayout.LayoutParams(0,-2,1));header.addView(ui.iconButton("close",L10n.tr("关闭"),dialog::dismiss),ui.size(36,36));sheet.addView(header);ui.gap(sheet,18);
|
||||
LinearLayout passwordRow=row();ui.card(passwordRow,10);EditText password=field(L10n.tr("访问密码"),InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);password.setBackgroundColor(Color.TRANSPARENT);passwordRow.addView(password,new LinearLayout.LayoutParams(0,dp(50),1));
|
||||
View eye=ui.iconButton("eye",L10n.tr("显示密码"),()->{boolean hidden=password.getTransformationMethod()!=null;password.setTransformationMethod(hidden?null:PasswordTransformationMethod.getInstance());password.setSelection(password.length());});passwordRow.addView(eye,ui.size(44,44));sheet.addView(passwordRow);ui.gap(sheet,18);
|
||||
LinearLayout save=row(),saveText=column();saveText.addView(text(L10n.tr("保存密码"),15,true));ui.gap(saveText,3);TextView security=text(L10n.tr("密码将加密保存在本机 Android Keystore 中"),11,false);security.setTextColor(SECONDARY);saveText.addView(security);save.addView(saveText,new LinearLayout.LayoutParams(0,-2,1));Switch remember=new Switch(this);remember.setContentDescription(L10n.tr("保存密码"));save.addView(remember);sheet.addView(save);
|
||||
remember.setChecked(rememberDefault);
|
||||
ui.gap(sheet,18);TextView connect=ui.primary("连接 →",()->{
|
||||
String secret=password.getText().toString();if(secret.isEmpty()||secret.length()>128||secret.contains("@")){password.setError("请输入有效访问密码");return;}
|
||||
ui.gap(sheet,18);TextView connect=ui.primary(L10n.tr("连接"),()->{
|
||||
String secret=password.getText().toString();if(secret.isEmpty()||secret.length()>128||secret.contains("@")){password.setError(L10n.tr("请输入有效访问密码"));return;}
|
||||
rememberPassword=remember.isChecked();password.setText("");dialog.dismiss();begin(id,secret);
|
||||
});connect.setContentDescription("确认连接");sheet.addView(connect,ui.size(-1,48));
|
||||
});connect.setContentDescription(L10n.tr("确认连接"));sheet.addView(connect,ui.size(-1,48));
|
||||
ScrollView scroll=new ScrollView(this);scroll.addView(sheet);
|
||||
dialog.setContentView(scroll);Window window=dialog.getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);window.setDimAmount(.22f);}dialog.show();sizePasswordDialog();password.requestFocus();if(!error.isEmpty())password.setError(error);
|
||||
dialog.setOnDismissListener(d->{password.setText("");passwordDialog=null;});
|
||||
@@ -306,33 +319,40 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
previewCapture=previews.begin(serverKey(),id);previewCopying=false;previewAttempts=0;
|
||||
connectedRemote=id;pendingPassword=rememberPassword?secret:"";ready=false;muted=false;selectedDisplay=0;hostPlatform="";remoteClipboard="";displays=new JSONArray();preferences.edit().putString("lastRemote",id).apply();
|
||||
ensureSignaling();
|
||||
if(signaling==null){pendingPassword="";toast("请先完成联网授权");return;}
|
||||
if(signaling==null){pendingPassword="";toast(L10n.tr("请先完成联网授权"));return;}
|
||||
session=signaling;showProgress();session.connect(id,secret);
|
||||
}
|
||||
private void showProgress(){
|
||||
Dialog dialog=new Dialog(this);progressDialog=dialog;LinearLayout card=column();ui.card(card,24);card.setGravity(Gravity.CENTER_HORIZONTAL);LinearLayout body=column();body.setGravity(Gravity.CENTER);body.setPadding(dp(26),dp(24),dp(26),dp(20));
|
||||
ProgressBar spinner=new ProgressBar(this);spinner.setIndeterminateTintList(ColorStateList.valueOf(BLUE));body.addView(spinner,ui.size(34,34));ui.gap(body,14);TextView title=text("正在连接",20,true);title.setGravity(Gravity.CENTER);body.addView(title);ui.gap(body,6);status=text("正在连接信令服务器…",14,false);status.setGravity(Gravity.CENTER);status.setTextColor(SECONDARY);status.setMinHeight(dp(36));body.addView(status);card.addView(body,ui.size(-1,-2));ui.divider(card);card.addView(ui.action("取消连接",RED,()->end("")),ui.size(-1,46));
|
||||
ProgressBar spinner=new ProgressBar(this);spinner.setIndeterminateTintList(ColorStateList.valueOf(BLUE));body.addView(spinner,ui.size(34,34));ui.gap(body,14);TextView title=text(L10n.tr("正在连接"),20,true);title.setGravity(Gravity.CENTER);body.addView(title);ui.gap(body,6);status=text(L10n.tr("正在连接信令服务器…"),14,false);status.setGravity(Gravity.CENTER);status.setTextColor(SECONDARY);status.setMinHeight(dp(36));body.addView(status);card.addView(body,ui.size(-1,-2));ui.divider(card);card.addView(ui.action(L10n.tr("取消连接"),RED,()->end("")),ui.size(-1,46));
|
||||
dialog.setContentView(card);dialog.setCanceledOnTouchOutside(false);dialog.setOnCancelListener(d->end(""));Window w=dialog.getWindow();if(w!=null){w.setBackgroundDrawableResource(android.R.color.transparent);w.setDimAmount(.08f);}dialog.show();if(w!=null)w.setLayout(dp(292),-2);
|
||||
}
|
||||
private void sessionScreen(){
|
||||
installRoot("session",true);setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);video=new RemoteVideoView(this,session);video.setRelative(preferences.getBoolean("relativeMouse",true));canvas.addView(video,0,new FrameLayout.LayoutParams(-1,-1));
|
||||
private void sessionScreen(boolean autoLandscape){
|
||||
installRoot("session",true);remoteOrientation.begin(autoLandscape,!isTablet()&&!isInMultiWindowMode());video=new RemoteVideoView(this,session);video.setRelative(preferences.getBoolean("relativeMouse",true));canvas.addView(video,0,new FrameLayout.LayoutParams(-1,-1));
|
||||
controls=new SessionControls(this,ui,new SessionControls.Actions(){
|
||||
public void disconnect(){confirmDisconnect();}public void display(){selectDisplay();}public void clipboard(){MainActivity.this.clipboard();}
|
||||
public void sendFile(){chooseFile();}public void saveFile(){saveReceivedFile();}public void secureAttention(){MainActivity.this.secureAttention();}
|
||||
public void togglePrivacy(){if(ready&&session!=null)session.togglePrivacy();}
|
||||
public void key(int code){tapKey(code);}
|
||||
public boolean textEdit(int count,String value){return sendTextEdit(count,value);}
|
||||
public void chord(int code,int[] modifiers){if(session==null)return;for(int modifier:modifiers)session.key(modifier,true);tapKey(code);for(int i=modifiers.length-1;i>=0;i--)session.key(modifiers[i],false);}public void shortcut(int code){MainActivity.this.shortcut(code);}public void text(String value){sendText(value);}
|
||||
public void mute(){muted=!muted;if(session!=null)session.control(2,muted?0:1);controls.setMuted(muted);}
|
||||
public void mouse(){boolean relative=!preferences.getBoolean("relativeMouse",true);preferences.edit().putBoolean("relativeMouse",relative).apply();video.setRelative(relative);controls.setRelative(relative);}
|
||||
public void videoSettings(int field,int value){RemoteVideoSettings.Values old=MainActivity.this.videoSettings.selection;updateVideoSettings(new RemoteVideoSettings.Values(field==0?value:old.quality,field==1?value:old.frameRate,field==2?value:old.preference));}
|
||||
});controls.setRelative(preferences.getBoolean("relativeMouse",true));controls.setMuted(muted);controls.videoSettings(videoSettings);root.addView(controls,new LinearLayout.LayoutParams(-1,-1));getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
});controls.setHostPlatform(hostPlatform);controls.setRelative(preferences.getBoolean("relativeMouse",true));controls.setMuted(muted);controls.videoSettings(videoSettings);root.addView(controls,new LinearLayout.LayoutParams(-1,-1));getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
controls.attachMouseBar(canvas,new VirtualMouseBar(ui,video.mouseInput));
|
||||
privacyChanged();
|
||||
video.remoteFocusChanged=()->{if(controls!=null)controls.resetKeyboardContext();};
|
||||
controls.requestApplyInsets();
|
||||
video.addOnLayoutChangeListener((view,left,top,right,bottom,oldLeft,oldTop,oldRight,oldBottom)->{
|
||||
if(!pictureInPictureUi&&!isInPictureInPictureMode())updatePictureInPicture();
|
||||
});
|
||||
pictureInPicture.connected();updatePictureInPicture();
|
||||
}
|
||||
private void confirmDisconnect(){disconnectDialog=new AlertDialog.Builder(this).setTitle("断开远程连接?").setMessage("断开后将返回首页").setNegativeButton("取消",null).setPositiveButton("断开连接",(d,w)->end("")).show();}
|
||||
private void confirmDisconnect(){disconnectDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("断开远程连接?")).setMessage(L10n.tr("断开后将返回首页")).setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("断开连接"),(d,w)->end("")).show();}
|
||||
private void end(String reason){
|
||||
dismissPrivacyUnlockDialog();
|
||||
remoteOrientation.end();
|
||||
if(backgroundSession!=null){BackgroundSession retained=backgroundSession;backgroundSession=null;retained.detach(this);retained.finish(reason);}
|
||||
boolean closeWindow=isInPictureInPictureMode();
|
||||
pictureInPicture.disconnect();updatePictureInPicture();
|
||||
@@ -347,45 +367,48 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
appUpdates.checkNow();
|
||||
}
|
||||
private void settings(){
|
||||
LinearLayout content=formPage("settings","设置",()->home(""));
|
||||
LinearLayout mouse=section(content,"鼠标控制",MobileUi.SETTINGS_CARD_RADIUS);mouse.setPadding(dp(12),dp(12),dp(12),dp(12));boolean relative=preferences.getBoolean("relativeMouse",true);
|
||||
mouse.addView(segments(new String[]{"相对位置","绝对位置"},relative?0:1,true,index->preferences.edit().putBoolean("relativeMouse",index==0).apply()));
|
||||
LinearLayout picture=section(content,"画面偏好",MobileUi.SETTINGS_CARD_RADIUS);picture.setPadding(dp(12),dp(12),dp(12),dp(12));int preference=Math.max(0,Math.min(2,preferences.getInt("videoPreference",1)));
|
||||
picture.addView(segments(RemoteVideoSettings.PREFERENCES,preference,true,index->preferences.edit().putInt("videoPreference",index).apply()));ui.gap(content,28);
|
||||
LinearLayout content=formPage("settings",L10n.tr("设置"),()->home(""));
|
||||
LinearLayout mouse=section(content,L10n.tr("鼠标控制"),MobileUi.SETTINGS_CARD_RADIUS);mouse.setPadding(dp(12),dp(12),dp(12),dp(12));boolean relative=preferences.getBoolean("relativeMouse",true);
|
||||
mouse.addView(segments(new String[]{L10n.tr("相对位置"),L10n.tr("绝对位置")},relative?0:1,true,index->preferences.edit().putBoolean("relativeMouse",index==0).apply()));
|
||||
LinearLayout picture=section(content,L10n.tr("画面偏好"),MobileUi.SETTINGS_CARD_RADIUS);picture.setPadding(dp(12),dp(12),dp(12),dp(12));int preference=Math.max(0,Math.min(2,preferences.getInt("videoPreference",1)));
|
||||
picture.addView(segments(RemoteVideoSettings.preferences(),preference,true,index->preferences.edit().putInt("videoPreference",index).apply()));ui.gap(content,28);
|
||||
LinearLayout direction=section(content,L10n.tr("屏幕方向"),MobileUi.SETTINGS_CARD_RADIUS);LinearLayout directionRow=row();directionRow.setPadding(dp(16),dp(10),dp(16),dp(10));directionRow.addView(text(L10n.tr("连接建立后锁定横屏"),16,false),new LinearLayout.LayoutParams(0,-2,1));
|
||||
Switch autoLandscape=new Switch(this);autoLandscape.setContentDescription(L10n.tr("连接建立后锁定横屏"));autoLandscape.setChecked(preferences.getBoolean("autoLandscape",false));autoLandscape.setOnCheckedChangeListener((button,checked)->preferences.edit().putBoolean("autoLandscape",checked).apply());directionRow.addView(autoLandscape);direction.addView(directionRow);
|
||||
footer(content,L10n.tr("默认关闭。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受影响。"));
|
||||
LinearLayout server=section(content,"",MobileUi.SETTINGS_CARD_RADIUS);LinearLayout fields=column();fields.setVisibility(View.GONE);boolean custom=!host.equals("api.crossdesk.cn")||port!=9099;
|
||||
link(server,"服务器",custom?"自定义":"默认",()->fields.setVisibility(fields.getVisibility()==View.VISIBLE?View.GONE:View.VISIBLE));server.addView(fields);
|
||||
LinearLayout toggleRow=row();toggleRow.setPadding(dp(16),dp(8),dp(16),dp(8));toggleRow.addView(text("使用自定义服务器",16,false),new LinearLayout.LayoutParams(0,-2,1));Switch toggle=new Switch(this);toggle.setContentDescription("使用自定义服务器");toggle.setChecked(custom);toggleRow.addView(toggle);fields.addView(toggleRow);
|
||||
LinearLayout inputs=column();inputs.setPadding(dp(12),0,dp(12),dp(12));EditText address=field("服务器地址",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_URI),number=field("服务器端口",InputType.TYPE_CLASS_NUMBER);address.setText(custom?host:"");number.setText(custom?String.valueOf(port):"");inputs.addView(address,ui.size(-1,48));ui.gap(inputs,8);inputs.addView(number,ui.size(-1,48));ui.gap(inputs,8);
|
||||
inputs.addView(ui.action("完成",BLUE,()->{if(saveServer(address,number)){hideKeyboard();settings();}}));fields.addView(inputs);inputs.setVisibility(custom?View.VISIBLE:View.GONE);
|
||||
link(server,L10n.tr("服务器"),custom?L10n.tr("自定义"):L10n.tr("默认"),()->fields.setVisibility(fields.getVisibility()==View.VISIBLE?View.GONE:View.VISIBLE));server.addView(fields);
|
||||
LinearLayout toggleRow=row();toggleRow.setPadding(dp(16),dp(8),dp(16),dp(8));toggleRow.addView(text(L10n.tr("使用自定义服务器"),16,false),new LinearLayout.LayoutParams(0,-2,1));Switch toggle=new Switch(this);toggle.setContentDescription(L10n.tr("使用自定义服务器"));toggle.setChecked(custom);toggleRow.addView(toggle);fields.addView(toggleRow);
|
||||
LinearLayout inputs=column();inputs.setPadding(dp(12),0,dp(12),dp(12));EditText address=field(L10n.tr("服务器地址"),InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_URI),number=field(L10n.tr("服务器端口"),InputType.TYPE_CLASS_NUMBER);address.setText(custom?host:"");number.setText(custom?String.valueOf(port):"");inputs.addView(address,ui.size(-1,48));ui.gap(inputs,8);inputs.addView(number,ui.size(-1,48));ui.gap(inputs,8);
|
||||
inputs.addView(ui.action(L10n.tr("完成"),BLUE,()->{if(saveServer(address,number)){hideKeyboard();settings();}}));fields.addView(inputs);inputs.setVisibility(custom?View.VISIBLE:View.GONE);
|
||||
toggle.setOnCheckedChangeListener((button,checked)->{inputs.setVisibility(checked?View.VISIBLE:View.GONE);if(!checked){host="api.crossdesk.cn";port=9099;preferences.edit().putString("host",host).putInt("port",port).apply();ensureSignaling();} });
|
||||
backAction=()->{if(toggle.isChecked()&&!saveServer(address,number))return;hideKeyboard();home("");};
|
||||
identitySummary=text("",12,false);identitySummary.setTextColor(SECONDARY);identitySummary.setGravity(Gravity.CENTER);identitySummary.setPadding(dp(16),dp(12),dp(16),dp(16));identitySummary.setTextIsSelectable(true);content.addView(identitySummary);renderSignaling();
|
||||
LinearLayout privacy=section(content,"隐私",MobileUi.SETTINGS_CARD_RADIUS);link(privacy,"隐私与授权",preferences.getBoolean("networkConsent",false)?"已授权":"未授权",this::privacyPage);ui.divider(privacy);
|
||||
LinearLayout preview=row();preview.setPadding(dp(16),dp(10),dp(16),dp(10));preview.addView(text("保存远程画面预览",16,false),new LinearLayout.LayoutParams(0,-2,1));previewSwitch=new Switch(this);previewSwitch.setContentDescription("保存远程画面预览");preview.addView(previewSwitch);privacy.addView(preview);
|
||||
LinearLayout privacy=section(content,L10n.tr("隐私"),MobileUi.SETTINGS_CARD_RADIUS);link(privacy,L10n.tr("隐私与授权"),preferences.getBoolean("networkConsent",false)?L10n.tr("已授权"):L10n.tr("未授权"),this::privacyPage);ui.divider(privacy);
|
||||
LinearLayout preview=row();preview.setPadding(dp(16),dp(10),dp(16),dp(10));preview.addView(text(L10n.tr("保存远程画面预览"),16,false),new LinearLayout.LayoutParams(0,-2,1));previewSwitch=new Switch(this);previewSwitch.setContentDescription(L10n.tr("保存远程画面预览"));preview.addView(previewSwitch);privacy.addView(preview);
|
||||
previewSwitch.setOnCheckedChangeListener((button,checked)->{if(updatingPreviewSwitch)return;if(checked){renderPreviews();confirmPreviewSaving();}else previews.setEnabled(false);});
|
||||
ui.divider(privacy);clearPreviewsButton=ui.action("清除预览图",RED,this::confirmClearPreviews);clearPreviewsButton.setGravity(Gravity.START|Gravity.CENTER_VERTICAL);clearPreviewsButton.setTypeface(Typeface.DEFAULT);clearPreviewsButton.setPadding(dp(16),0,dp(16),0);privacy.addView(clearPreviewsButton,ui.size(-1,48));
|
||||
ui.divider(privacy);clearPreviewsButton=ui.action(L10n.tr("清除预览图"),RED,this::confirmClearPreviews);clearPreviewsButton.setGravity(Gravity.START|Gravity.CENTER_VERTICAL);clearPreviewsButton.setTypeface(Typeface.DEFAULT);clearPreviewsButton.setPadding(dp(16),0,dp(16),0);privacy.addView(clearPreviewsButton,ui.size(-1,48));
|
||||
previewCleanupError=text("",12,false);previewCleanupError.setTextColor(RED);previewCleanupError.setPadding(dp(16),dp(8),dp(16),dp(12));privacy.addView(previewCleanupError);renderPreviews();
|
||||
ui.gap(content,28);LinearLayout about=section(content,"",MobileUi.SETTINGS_CARD_RADIUS);LinearLayout aboutLink=link(about,"关于","新版本",this::about);aboutUpdateRow=aboutLink;
|
||||
ui.gap(content,28);LinearLayout about=section(content,"",MobileUi.SETTINGS_CARD_RADIUS);LinearLayout aboutLink=link(about,L10n.tr("关于"),L10n.tr("新版本"),this::about);aboutUpdateRow=aboutLink;
|
||||
aboutUpdateLabel=(TextView)aboutLink.getChildAt(1);aboutUpdateLabel.setPadding(0,0,dp(8),0);
|
||||
aboutUpdateDot=updateDot();LinearLayout.LayoutParams dotPosition=new LinearLayout.LayoutParams(dp(8),dp(8));dotPosition.setMarginEnd(dp(8));aboutLink.addView(aboutUpdateDot,2,dotPosition);renderAppUpdates();
|
||||
}
|
||||
private void confirmPreviewSaving(){
|
||||
previewDialog=new AlertDialog.Builder(this).setTitle("保存远程画面预览?")
|
||||
.setMessage("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。")
|
||||
.setNegativeButton("取消",null).setPositiveButton("开启保存",(d,w)->previews.setEnabled(true)).show();
|
||||
previewDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("保存远程画面预览?"))
|
||||
.setMessage(L10n.tr("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。"))
|
||||
.setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("开启保存"),(d,w)->previews.setEnabled(true)).show();
|
||||
previewDialog.setOnDismissListener(d->{previewDialog=null;renderPreviews();});
|
||||
}
|
||||
private void confirmClearPreviews(){
|
||||
previewDialog=new AlertDialog.Builder(this).setTitle("清除预览图?")
|
||||
.setMessage("将删除本机保存的所有远程预览图,保留连接记录和密码。"+(previews.enabled()?"后续连接仍会保存新的预览图。":""))
|
||||
.setNegativeButton("取消",null).setPositiveButton("清除",(d,w)->previews.clear()).show();
|
||||
previewDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("清除预览图?"))
|
||||
.setMessage(L10n.tr("将删除本机保存的所有远程预览图,保留连接记录和密码。")+(previews.enabled()?L10n.tr("后续连接仍会保存新的预览图。"):""))
|
||||
.setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("清除"),(d,w)->previews.clear()).show();
|
||||
previewDialog.setOnDismissListener(d->previewDialog=null);
|
||||
}
|
||||
private void renderPreviews(){
|
||||
if(previews==null)return;
|
||||
if(!previews.enabled()||previews.cleaning)previewCapture=null;
|
||||
if(previewSwitch!=null){updatingPreviewSwitch=true;previewSwitch.setChecked(previews.enabled());previewSwitch.setEnabled(preferences.getBoolean("networkConsent",false)&&!previews.cleaning);updatingPreviewSwitch=false;}
|
||||
if(clearPreviewsButton!=null){clearPreviewsButton.setText(previews.cleaning?"正在清除…":"清除预览图");clearPreviewsButton.setEnabled(!previews.cleaning);}
|
||||
if(clearPreviewsButton!=null){clearPreviewsButton.setText(previews.cleaning?L10n.tr("正在清除…"):L10n.tr("清除预览图"));clearPreviewsButton.setEnabled(!previews.cleaning);}
|
||||
if(previewCleanupError!=null){previewCleanupError.setText(previews.cleanupError);previewCleanupError.setVisibility(previews.cleanupError.isEmpty()?View.GONE:View.VISIBLE);}
|
||||
for(Runnable card:previewCards)card.run();
|
||||
}
|
||||
@@ -404,17 +427,17 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
return new SegmentedControl(ui,titles,selected,enabled,changed);
|
||||
}
|
||||
private boolean saveServer(EditText address,EditText number){
|
||||
String name=address.getText().toString().trim();int value;try{value=Integer.parseInt(number.getText().toString());}catch(NumberFormatException e){number.setError("请输入 1–65535 范围内的端口");return false;}
|
||||
if(name.length()>253||!name.matches("[A-Za-z0-9.-]+")||name.startsWith(".")||name.endsWith(".")){address.setError("填写主机名或 IPv4 地址,不含协议和路径");return false;}if(value<1||value>65535){number.setError("端口范围为 1–65535");return false;}host=name;port=value;preferences.edit().putString("host",host).putInt("port",port).apply();ensureSignaling();return true;
|
||||
String name=address.getText().toString().trim();int value;try{value=Integer.parseInt(number.getText().toString());}catch(NumberFormatException e){number.setError(L10n.tr("请输入 1–65535 范围内的端口"));return false;}
|
||||
if(name.length()>253||!name.matches("[A-Za-z0-9.-]+")||name.startsWith(".")||name.endsWith(".")){address.setError(L10n.tr("填写主机名或 IPv4 地址,不含协议和路径"));return false;}if(value<1||value>65535){number.setError(L10n.tr("端口范围为 1–65535"));return false;}host=name;port=value;preferences.edit().putString("host",host).putInt("port",port).apply();ensureSignaling();return true;
|
||||
}
|
||||
private void announcements(){
|
||||
installRoot("announcements",false);LinearLayout bar=navigation("通知公告",()->home(""));
|
||||
bar.removeViewAt(bar.getChildCount()-1);announcementRefresh=ui.iconButton("refresh","刷新公告",announcementInbox::refresh);bar.addView(announcementRefresh,ui.size(44,44));
|
||||
installRoot("announcements",false);LinearLayout bar=navigation(L10n.tr("通知公告"),()->home(""));
|
||||
bar.removeViewAt(bar.getChildCount()-1);announcementRefresh=ui.iconButton("refresh",L10n.tr("刷新公告"),announcementInbox::refresh);bar.addView(announcementRefresh,ui.size(44,44));
|
||||
announcementView=new AnnouncementView(ui,announcementInbox,null,this::announcementDetail,this::connectAnnouncements);
|
||||
root.addView(announcementView,new LinearLayout.LayoutParams(-1,0,1));announcementInbox.refresh();renderAnnouncements();
|
||||
}
|
||||
private void announcementDetail(AnnouncementInbox.Item item){
|
||||
installRoot("announcement-detail",false);navigation("公告详情",this::announcements);
|
||||
installRoot("announcement-detail",false);navigation(L10n.tr("公告详情"),this::announcements);
|
||||
announcementView=new AnnouncementView(ui,announcementInbox,item,this::announcementDetail,this::connectAnnouncements);
|
||||
root.addView(announcementView,new LinearLayout.LayoutParams(-1,0,1));announcementInbox.markRead(item);
|
||||
}
|
||||
@@ -424,7 +447,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
}
|
||||
private void renderAnnouncements(){
|
||||
if(announcementBadge!=null){int unread=announcementInbox.unread;announcementBadge.setText(unread>99?"99+":String.valueOf(unread));announcementBadge.setVisibility(unread>0?View.VISIBLE:View.GONE);
|
||||
if(android.os.Build.VERSION.SDK_INT>=30)((View)announcementBadge.getParent()).setStateDescription(unread>0?unread+" 条未读":"无未读公告");}
|
||||
if(android.os.Build.VERSION.SDK_INT>=30)((View)announcementBadge.getParent()).setStateDescription(unread>0?L10n.format("{0} 条未读",unread):L10n.tr("无未读公告"));}
|
||||
if(announcementRefresh!=null){boolean enabled=announcementInbox.isConnected()&&!announcementInbox.loading;announcementRefresh.setEnabled(enabled);announcementRefresh.setAlpha(enabled?1:.4f);}
|
||||
if(announcementView!=null)announcementView.render();
|
||||
}
|
||||
@@ -434,12 +457,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
}
|
||||
@Override public void announcementFailed(String requestId){announcementInbox.failed(requestId);}
|
||||
private void privacyPage(){
|
||||
LinearLayout content=formPage("privacy","隐私与授权",this::settings);LinearLayout card=section(content,"");boolean consent=preferences.getBoolean("networkConsent",false);link(card,"授权状态",consent?"已授权":"未授权",()->{});ui.divider(card);link(card,"隐私政策","",this::privacyPolicyPage);
|
||||
footer(content,"设备身份及选择保存的访问密码使用 Android Keystore 加密。连接记录仅保存在本机,不纳入设备备份。剪贴板内容在结束会话时清除。离开应用后通过画中画或连接通知保持会话,可在应用或通知中断开。");
|
||||
if(consent)content.addView(ui.action("撤回联网授权",RED,()->new AlertDialog.Builder(this).setTitle("撤回联网授权?").setMessage("撤回后将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。").setNegativeButton("取消",null).setPositiveButton("撤回",(d,w)->{preferences.edit().putBoolean("networkConsent",false).apply();appUpdates.setEnabled(false);previews.setEnabled(false);stopSignaling();announcementInbox.reset();privacyPage();}).show()));else content.addView(ui.primary("阅读并授权",()->consent(this::privacyPage)));
|
||||
LinearLayout content=formPage("privacy",L10n.tr("隐私与授权"),this::settings);LinearLayout card=section(content,"");boolean consent=preferences.getBoolean("networkConsent",false);link(card,L10n.tr("授权状态"),consent?L10n.tr("已授权"):L10n.tr("未授权"),()->{});ui.divider(card);link(card,L10n.tr("隐私政策"),"",this::privacyPolicyPage);
|
||||
footer(content,L10n.tr("设备身份及选择保存的访问密码使用 Android Keystore 加密。连接记录仅保存在本机,不纳入设备备份。剪贴板内容在结束会话时清除。离开应用后通过画中画或连接通知保持会话,可在应用或通知中断开。"));
|
||||
if(consent)content.addView(ui.action(L10n.tr("撤回联网授权"),RED,()->new AlertDialog.Builder(this).setTitle(L10n.tr("撤回联网授权?")).setMessage(L10n.tr("撤回后将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。")).setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("撤回"),(d,w)->{preferences.edit().putBoolean("networkConsent",false).apply();appUpdates.setEnabled(false);previews.setEnabled(false);stopSignaling();announcementInbox.reset();privacyPage();}).show()));else content.addView(ui.primary(L10n.tr("阅读并授权"),()->consent(this::privacyPage)));
|
||||
}
|
||||
private void privacyPolicyPage(){
|
||||
installRoot("privacy-policy",false);navigation("隐私政策",this::privacyPage);
|
||||
installRoot("privacy-policy",false);navigation(L10n.tr("隐私政策"),this::privacyPage);
|
||||
root.addView(new PrivacyPolicyDocument(this).view(ui),new LinearLayout.LayoutParams(-1,0,1));
|
||||
}
|
||||
String applicationVersion(){
|
||||
@@ -456,22 +479,27 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
if(settingsUpdateDot!=null)settingsUpdateDot.setVisibility(available?View.VISIBLE:View.GONE);
|
||||
if(aboutUpdateDot!=null)aboutUpdateDot.setVisibility(available?View.VISIBLE:View.GONE);
|
||||
if(aboutUpdateLabel!=null)aboutUpdateLabel.setVisibility(available?View.VISIBLE:View.GONE);
|
||||
if(settingsUpdateButton!=null)settingsUpdateButton.setContentDescription(available?"设置,有新版本":"设置");
|
||||
if(aboutUpdateRow!=null)aboutUpdateRow.setContentDescription(available?"关于,有新版本":"关于");
|
||||
if(settingsUpdateButton!=null)settingsUpdateButton.setContentDescription(available?L10n.tr("设置,有新版本"):L10n.tr("设置"));
|
||||
if(aboutUpdateRow!=null)aboutUpdateRow.setContentDescription(available?L10n.tr("关于,有新版本"):L10n.tr("关于"));
|
||||
if(aboutPages!=null&&page.equals("about"))aboutPages.renderUpdates();
|
||||
}
|
||||
private void about(){
|
||||
if(aboutPages==null)aboutPages=new AboutPages(this,ui,this::settings,appUpdates);aboutPages.show();
|
||||
}
|
||||
private void sendText(String value){if(session==null)return;if(value.length()>2048){toast("单次最多 2048 个字符");return;}for(char ch:value.toCharArray())if(KeyMap.ascii(ch)==0){toast("中文等文本请通过剪贴板发送,再点击粘贴");return;}for(char ch:value.toCharArray()){int code=KeyMap.ascii(ch);boolean shift=(code&0x100)!=0;if(shift)session.key(0x10,true);tapKey(code&0xFF);if(shift)session.key(0x10,false);}}
|
||||
private void sendText(String value){sendTextEdit(0,value);}
|
||||
private boolean sendTextEdit(int count,String value){
|
||||
if(session==null)return false;
|
||||
if(session.textEdit(count,value))return true;
|
||||
toast(L10n.tr("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送"));return false;
|
||||
}
|
||||
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,false);}}
|
||||
private void shortcut(int code){if(session!=null){int modifier=hostPlatform.equals("macos")?0x5B:0x11;session.key(modifier,true);tapKey(code);session.key(modifier,false);}}
|
||||
private void selectDisplay(){
|
||||
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast("等待远端屏幕信息");return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?"显示器":display.optString("name","显示器"));}new AlertDialog.Builder(this).setTitle("显示器").setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.resetVideoStatistics();}}).show();
|
||||
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast(L10n.tr("等待远端屏幕信息"));return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?L10n.tr("显示器"):display.optString("name",L10n.tr("显示器")));}new AlertDialog.Builder(this).setTitle(L10n.tr("显示器")).setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.resetVideoStatistics();}}).show();
|
||||
}
|
||||
private void secureAttention(){
|
||||
if(!ready||session==null)return;
|
||||
if(!remoteServiceAvailable){controlFeedback(hostPlatform.equals("windows")?"请在远端启用 CrossDesk 系统服务后重试":"Ctrl+Alt+Del 需要远端 Windows 系统服务支持");return;}
|
||||
if(!ready||session==null||!hostPlatform.equals("windows"))return;
|
||||
if(!remoteServiceAvailable){controlFeedback(L10n.tr("请在远端启用 CrossDesk 系统服务后重试"));return;}
|
||||
session.secureAttention();
|
||||
}
|
||||
private void chooseFile(){
|
||||
@@ -488,7 +516,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
documentSession=session;releaseKeys();
|
||||
pictureInPicture.choosingDocument(true);updatePictureInPicture();
|
||||
try{startActivityForResult(intent,request);mainHandler.postDelayed(documentTimeout,5*60*1000);}
|
||||
catch(android.content.ActivityNotFoundException error){documentSession=null;documentToSave=null;pictureInPicture.choosingDocument(false);updatePictureInPicture();controlFeedback("此设备没有可用的文件选择器");}
|
||||
catch(android.content.ActivityNotFoundException error){documentSession=null;documentToSave=null;pictureInPicture.choosingDocument(false);updatePictureInPicture();controlFeedback(L10n.tr("此设备没有可用的文件选择器"));}
|
||||
}
|
||||
@Override protected void onActivityResult(int request,int result,Intent data){
|
||||
super.onActivityResult(request,result,data);
|
||||
@@ -502,7 +530,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
@Override public void fileTransfer(String name,double progress,boolean sending,java.io.File received){
|
||||
if(!ready)return;
|
||||
if(received!=null)receivedFile=received;
|
||||
String fileStatus=progress<0?name+" 传输失败":(sending?"发送 ":"接收 ")+name+(progress>=1?" · 已完成":" · "+(int)(progress*100)+"%");
|
||||
String fileStatus=progress<0?L10n.format("{0} 传输失败",name):(sending?L10n.tr("发送 "):L10n.tr("接收 "))+name+(progress>=1?L10n.tr(" · 已完成"):" · "+(int)(progress*100)+"%");
|
||||
if(controls!=null)controls.fileStatus(fileStatus,receivedFile!=null);
|
||||
if(progress<0||progress>=1)toast(fileStatus);
|
||||
}
|
||||
@@ -511,14 +539,14 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
}
|
||||
private void clipboard(){
|
||||
if(!ready)return;
|
||||
EditText input=field("要发送到远端的文本",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_MULTI_LINE);input.setSingleLine(false);input.setMinLines(3);
|
||||
new AlertDialog.Builder(this).setTitle("剪贴板").setView(input)
|
||||
.setMessage("发送后,在远程键盘中点击“粘贴”。接收的远端文本可手动复制到本机。")
|
||||
.setNegativeButton("关闭",null).setNeutralButton("复制远端文本",(d,w)->{
|
||||
EditText input=field(L10n.tr("要发送到远端的文本"),InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_MULTI_LINE);input.setSingleLine(false);input.setMinLines(3);
|
||||
new AlertDialog.Builder(this).setTitle(L10n.tr("剪贴板")).setView(input)
|
||||
.setMessage(L10n.tr("发送后,在远程键盘中点击“粘贴”。接收的远端文本可手动复制到本机。"))
|
||||
.setNegativeButton(L10n.tr("关闭"),null).setNeutralButton(L10n.tr("复制远端文本"),(d,w)->{
|
||||
if(!remoteClipboard.isEmpty())((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("CrossDesk",remoteClipboard));
|
||||
}).setPositiveButton("发送文本",(d,w)->{
|
||||
}).setPositiveButton(L10n.tr("发送文本"),(d,w)->{
|
||||
String value=input.getText().toString();
|
||||
if(value.getBytes(java.nio.charset.StandardCharsets.UTF_8).length>128*1024){Toast.makeText(this,"文本超过 128 KB",Toast.LENGTH_SHORT).show();return;}
|
||||
if(value.getBytes(java.nio.charset.StandardCharsets.UTF_8).length>128*1024){Toast.makeText(this,L10n.tr("文本超过 128 KB"),Toast.LENGTH_SHORT).show();return;}
|
||||
if(session!=null)session.clipboard(value);
|
||||
}).show();
|
||||
}
|
||||
@@ -553,17 +581,17 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
boolean consent=preferences.getBoolean("networkConsent",false);
|
||||
boolean connected=consent&&signalError.isEmpty()&&signalState==1;
|
||||
String label;int tint,background;
|
||||
if(!consent){label="等待隐私授权";tint=0xFFB76A00;background=0xFFFFEBD2;}
|
||||
else if(!signalError.isEmpty()){label=signalError.contains("TLS")?"证书校验失败 · 点击重试":"连接失败 · 点击重试";tint=RED;background=0xFFFFE8E6;}
|
||||
else if(connected){label="已连接服务器";tint=GREEN;background=0xFFE5F4E8;}
|
||||
else{label=signalState==0?"正在连接服务器…":signalState==4?"正在重连服务器…":"未连接服务器 · 点击重试";tint=0xFFB76A00;background=0xFFFFEBD2;}
|
||||
if(!consent){label=L10n.tr("等待隐私授权");tint=0xFFB76A00;background=0xFFFFEBD2;}
|
||||
else if(!signalError.isEmpty()){label=signalError.contains("TLS")?L10n.tr("证书校验失败 · 点击重试"):L10n.tr("连接失败 · 点击重试");tint=RED;background=0xFFFFE8E6;}
|
||||
else if(connected){label=L10n.tr("已连接服务器");tint=GREEN;background=0xFFE5F4E8;}
|
||||
else{label=signalState==0?L10n.tr("正在连接服务器…"):signalState==4?L10n.tr("正在重连服务器…"):L10n.tr("未连接服务器 · 点击重试");tint=0xFFB76A00;background=0xFFFFEBD2;}
|
||||
if(signalBadge!=null){
|
||||
android.graphics.drawable.Drawable icon=connected?getDrawable(R.drawable.ic_check_circle):null;
|
||||
if(icon!=null){int size=dp(14);icon.setBounds(0,0,size,size);}
|
||||
signalBadge.setCompoundDrawablesRelative(icon,null,null,null);signalBadge.setCompoundDrawablePadding(dp(6));
|
||||
signalBadge.setText(label);signalBadge.setTextColor(tint);signalBadge.setBackground(ui.background(background,20));signalBadge.setContentDescription(signalError.isEmpty()?label:signalError+",点击重试");
|
||||
signalBadge.setText(label);signalBadge.setTextColor(tint);signalBadge.setBackground(ui.background(background,20));signalBadge.setContentDescription(signalError.isEmpty()?label:signalError+L10n.tr(",点击重试"));
|
||||
}
|
||||
if(identitySummary!=null)identitySummary.setText(!consent?"完成隐私授权后登记本机身份":!localIdentity.isEmpty()?"本机 ID "+localIdentity:!signalError.isEmpty()?signalError:"正在获取本机 ID…");
|
||||
if(identitySummary!=null)identitySummary.setText(!consent?L10n.tr("完成隐私授权后登记本机身份"):!localIdentity.isEmpty()?L10n.format("本机 ID {0}",localIdentity):!signalError.isEmpty()?signalError:L10n.tr("正在获取本机 ID…"));
|
||||
}
|
||||
@Override public void signaling(int state,String deviceId){
|
||||
signalState=state;localIdentity=deviceId;signalError="";renderSignaling();
|
||||
@@ -578,7 +606,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
// Transport readiness can be reported again after ICE changes its selected path.
|
||||
// The first success consumes pendingPassword; a duplicate must not save it again.
|
||||
if(session==null||ready)return;ready=true;if(progressDialog!=null){progressDialog.dismiss();progressDialog=null;}
|
||||
try{history.save(serverKey(),connectedRemote,connectedRemote,hostPlatform,rememberPassword,pendingPassword);}catch(Exception error){toast("密码保存失败,本次连接仍可使用");}pendingPassword="";sessionScreen();
|
||||
try{history.save(serverKey(),connectedRemote,connectedRemote,hostPlatform,rememberPassword,pendingPassword);}catch(Exception error){toast(L10n.tr("密码保存失败,本次连接仍可使用"));}pendingPassword="";sessionScreen(preferences.getBoolean("autoLandscape",false));
|
||||
if(!canUsePictureInPicture())prepareBackgroundSession(true);
|
||||
if(videoSettings.supported&&videoSettings.requestId==0)updateVideoSettings(videoSettings.selection);
|
||||
if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.connected();
|
||||
@@ -592,12 +620,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
String id=connectedRemote;boolean remember=rememberPassword;
|
||||
end("");
|
||||
// Let the user replace an expired credential; never retry it automatically.
|
||||
if(foreground)passwordSheet(id,"访问密码错误,请重新输入",remember);
|
||||
if(foreground)passwordSheet(id,L10n.tr("访问密码错误,请重新输入"),remember);
|
||||
}
|
||||
private void cancelVideoSettingsTimeout(){if(videoSettingsTimeout!=null)mainHandler.removeCallbacks(videoSettingsTimeout);videoSettingsTimeout=null;}
|
||||
private void showRemoteUpdate(){
|
||||
if(!ready||session==null||!foreground||pictureInPictureUi||isInPictureInPictureMode()||isFinishing()||!preferences.getBoolean("networkConsent",false))return;
|
||||
remoteUpdateDialog=new AlertDialog.Builder(this).setTitle("升级提示").setMessage(RemoteVersionCheck.NOTICE).setPositiveButton("知道了",null).create();
|
||||
remoteUpdateDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("升级提示")).setMessage(L10n.tr(RemoteVersionCheck.NOTICE)).setPositiveButton(L10n.tr("知道了"),null).create();
|
||||
remoteUpdateDialog.setOnDismissListener(dialog->remoteUpdateDialog=null);
|
||||
remoteUpdateDialog.setCanceledOnTouchOutside(false);
|
||||
remoteUpdateDialog.show();
|
||||
@@ -624,6 +652,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
JSONObject info=message.optJSONObject("host_info");
|
||||
if(info!=null){
|
||||
hostPlatform=info.optString("platform","");JSONArray list=info.optJSONArray("displays");if(list!=null)displays=list;history.metadata(serverKey(),connectedRemote,info.optString("host_name",connectedRemote),hostPlatform);
|
||||
if(controls!=null)controls.setHostPlatform(hostPlatform);
|
||||
boolean wasSupported=videoSettings.supported;videoSettings.setSupported(info.optBoolean("supports_video_settings",false));
|
||||
if(!videoSettings.supported)cancelVideoSettingsTimeout();
|
||||
if(controls!=null)controls.videoSettings(videoSettings);
|
||||
@@ -631,21 +660,63 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.hostInfo(info);
|
||||
}
|
||||
}
|
||||
private void dismissPrivacyUnlockDialog() {
|
||||
if (privacyUnlockDialog != null) { privacyUnlockDialog.dismiss(); privacyUnlockDialog = null; }
|
||||
}
|
||||
@Override public void privacyChanged() {
|
||||
if (controls != null && session != null) controls.privacyScreen(session.privacyScreen);
|
||||
if (!ready || session == null || !session.privacyScreen.unlockNotice) {
|
||||
dismissPrivacyUnlockDialog(); return;
|
||||
}
|
||||
if (!foreground || pictureInPictureUi || isInPictureInPictureMode() || isFinishing()) return;
|
||||
if (privacyUnlockDialog == null) {
|
||||
releaseKeys();
|
||||
if (controls != null) { controls.closeKeyboard(); controls.cancelMouseInput(); }
|
||||
if (video != null) video.releaseMouse();
|
||||
privacyUnlockDialog = new AlertDialog.Builder(this).setTitle(L10n.tr("隐私屏已被解锁"))
|
||||
.setMessage(L10n.tr("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。"))
|
||||
.setNegativeButton(L10n.tr("确认"), (dialog, which) -> {
|
||||
if (session != null) session.privacyScreen.unlockNotice = false;
|
||||
dismissPrivacyUnlockDialog();
|
||||
}).setPositiveButton(L10n.tr("锁定"), null).create();
|
||||
privacyUnlockDialog.setCancelable(false);
|
||||
privacyUnlockDialog.show();
|
||||
privacyUnlockDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(view -> {
|
||||
if (session != null) session.lockPrivacy();
|
||||
});
|
||||
}
|
||||
String feedback = session.privacyScreen.feedback(android.os.SystemClock.elapsedRealtime());
|
||||
privacyUnlockDialog.setMessage(L10n.tr("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。")
|
||||
+ (feedback.isEmpty() ? "" : "\n\n" + feedback));
|
||||
privacyUnlockDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(
|
||||
session.privacyScreen.canLock(android.os.SystemClock.elapsedRealtime()));
|
||||
}
|
||||
@Override public void videoSize(int width,int height){videoWidth=width;videoHeight=height;if(video!=null)video.videoSize(width,height);if(controls!=null)controls.videoSize(width,height);pictureInPicture.videoSize(width,height);updatePictureInPicture();}
|
||||
@Override public void clipboard(String value){remoteClipboard=value;}
|
||||
@Override public void statistics(RemoteNetworkStatistics.Snapshot value){if(controls!=null)controls.statistics(value);if(value.fps>0)capturePreviewIfNeeded();}
|
||||
@Override public boolean dispatchKeyEvent(KeyEvent event){
|
||||
if(ready&&session!=null&&!pictureInPictureUi&&hasWindowFocus()&&!(getCurrentFocus() instanceof EditText)&&event.getKeyCode()!=KeyEvent.KEYCODE_BACK){int code=KeyMap.windowsCode(event.getKeyCode());if(code!=0){boolean down=event.getAction()==KeyEvent.ACTION_DOWN;if(down)pressed.add(code);else pressed.remove(code);session.key(code,down);return true;}}return super.dispatchKeyEvent(event);
|
||||
if (privacyUnlockDialog != null) return super.dispatchKeyEvent(event);
|
||||
int code=KeyMap.windowsCode(event.getKeyCode());View focus=getCurrentFocus();
|
||||
boolean remoteEditor=focus instanceof RemoteKeyboardInput;
|
||||
boolean desktopKey=event.isCtrlPressed()||event.isAltPressed()||event.isMetaPressed()||pressed.contains(code)
|
||||
||code==17||code==18||code==91||code==27||code==9||code>=33&&code<=40||code==45||code==46||code>=112&&code<=123;
|
||||
if(ready&&session!=null&&!pictureInPictureUi&&hasWindowFocus()&&(!(focus instanceof EditText)||remoteEditor&&(!((RemoteKeyboardInput)focus).isAcceptingInput()||desktopKey))&&event.getKeyCode()!=KeyEvent.KEYCODE_BACK&&code!=0){
|
||||
boolean down=event.getAction()==KeyEvent.ACTION_DOWN;
|
||||
if(down){pressed.add(code);if(remoteEditor)controls.resetKeyboardContext();}else pressed.remove(code);
|
||||
session.key(code,down);return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
private void releaseKeys(){if(session!=null)for(int code:pressed)session.key(code,false);pressed.clear();}
|
||||
private void navigateBack(){if(backAction==null)return;navigatingBack=true;try{backAction.run();}finally{navigatingBack=false;}}
|
||||
private void back(){pageHost.finish();if(session!=null){if(ready)confirmDisconnect();else end("");}else if(backAction!=null)navigateBack();else finish();}
|
||||
private void back(){if(controls!=null&&controls.closeKeyboard())return;pageHost.finish();if(session!=null){if(ready)confirmDisconnect();else end("");}else if(backAction!=null)navigateBack();else finish();}
|
||||
@Override public void onWindowFocusChanged(boolean focus){super.onWindowFocusChanged(focus);if(!focus)releaseKeys();else applySystemBars(ready);}
|
||||
@android.annotation.SuppressLint("GestureBackNavigation") @Override public void onBackPressed(){back();}
|
||||
@Override public void onConfigurationChanged(Configuration config){
|
||||
super.onConfigurationChanged(config);
|
||||
releaseKeys();
|
||||
if(!page.equals("session"))updateHomeOrientation();
|
||||
else if(!isInPictureInPictureMode())remoteOrientation.update(!isTablet()&&!isInMultiWindowMode());
|
||||
sizePasswordDialog();
|
||||
if(privacyDialog instanceof PrivacyConsentDialog)((PrivacyConsentDialog)privacyDialog).updateSize();
|
||||
if(controls!=null&&!pictureInPictureUi)controls.post(controls::constrainPanels);
|
||||
@@ -664,6 +735,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
retained.muted=muted;retained.selectedDisplay=selectedDisplay;retained.remoteServiceAvailable=remoteServiceAvailable;
|
||||
retained.clipboard=remoteClipboard;retained.videoSettings=videoSettings;retained.receivedFile=receivedFile;
|
||||
retained.width=videoWidth;retained.height=videoHeight;
|
||||
retained.autoLandscape=remoteOrientation.autoLandscape();
|
||||
}
|
||||
private boolean prepareBackgroundSession(boolean inform){
|
||||
if(!ready||session==null||session.isClosed()||isFinishing()||!hasNetworkConsent())return false;
|
||||
@@ -672,10 +744,10 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
backgroundSession=retained;captureBackgroundSession();retained.attach(this);session.setListener(retained);
|
||||
if(!RemoteSessionService.retain(this,retained)){
|
||||
session.setListener(this);retained.detach(this);backgroundSession=null;
|
||||
if(inform)toast("后台保活未能启动,请保持应用在前台");return false;
|
||||
if(inform)toast(L10n.tr("后台保活未能启动,请保持应用在前台"));return false;
|
||||
}
|
||||
if(inform){
|
||||
toast("已启用后台保活,可从连接通知返回或断开");
|
||||
toast(L10n.tr("已启用后台保活,可从连接通知返回或断开"));
|
||||
requestBackgroundNotifications();
|
||||
}
|
||||
return true;
|
||||
@@ -690,7 +762,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
BackgroundSession retained=backgroundSession!=null?backgroundSession:RemoteSessionService.current();
|
||||
if(retained==null)return false;
|
||||
if(retained.ended||retained.session.isClosed()||!hasNetworkConsent()||!host.equals(retained.host)||port!=retained.port){
|
||||
retained.detach(this);retained.finish("后台连接已结束,请重新连接");backgroundSession=null;
|
||||
retained.detach(this);retained.finish(L10n.tr("后台连接已结束,请重新连接"));backgroundSession=null;
|
||||
if(session==retained.session)end(retained.endReason);return false;
|
||||
}
|
||||
boolean rebuild=session!=retained.session||video==null||!page.equals("session");
|
||||
@@ -699,9 +771,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
hostPlatform=retained.platform;displays=retained.displays;muted=retained.muted;selectedDisplay=retained.selectedDisplay;
|
||||
remoteServiceAvailable=retained.remoteServiceAvailable;remoteClipboard=retained.clipboard;receivedFile=retained.receivedFile;
|
||||
videoSettings=retained.videoSettings;ready=true;retained.attach(this);
|
||||
if(rebuild)sessionScreen();
|
||||
if(rebuild)sessionScreen(retained.autoLandscape);
|
||||
if(retained.width>0&&retained.height>0)videoSize(retained.width,retained.height);
|
||||
if(controls!=null){controls.setMuted(muted);controls.videoSettings(videoSettings);controls.fileStatus(retained.fileStatus,receivedFile!=null);}
|
||||
if(controls!=null){controls.setHostPlatform(hostPlatform);controls.setMuted(muted);controls.videoSettings(videoSettings);controls.fileStatus(retained.fileStatus,receivedFile!=null);}
|
||||
retained.setBackground(false);signaling(signalState,localIdentity);
|
||||
return true;
|
||||
}
|
||||
@@ -747,11 +819,11 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
super.onPictureInPictureUiStateChanged(state);
|
||||
if(android.os.Build.VERSION.SDK_INT>=35&&state.isTransitioningToPip())setPictureInPictureUi(true);
|
||||
}
|
||||
@Override protected void onResume(){super.onResume();if(!isInPictureInPictureMode())enteredPictureInPicture=false;setPictureInPictureUi(isInPictureInPictureMode());updatePictureInPicture();if(ready&&!canUsePictureInPicture())prepareBackgroundSession(true);if(backgroundSession!=null)requestBackgroundNotifications();}
|
||||
@Override protected void onResume(){super.onResume();privacyChanged();if(!isInPictureInPictureMode())enteredPictureInPicture=false;setPictureInPictureUi(isInPictureInPictureMode());updatePictureInPicture();if(ready&&!canUsePictureInPicture())prepareBackgroundSession(true);if(backgroundSession!=null)requestBackgroundNotifications();}
|
||||
// onStart marks a new visit; focus/resume callbacks must not reopen a refused notice.
|
||||
@Override protected void onPause(){releaseKeys();if(controls!=null)controls.cancelMouseInput();if(video!=null)video.releaseMouse();super.onPause();}
|
||||
@Override protected void onStart(){super.onStart();foreground=true;restoreBackgroundSession();appUpdates.setEnabled(preferences.getBoolean("networkConsent",false));if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);ensureSignaling();showConnectionMessage();}
|
||||
@Override protected void onStop(){foreground=false;appUpdates.setEnabled(false);if(privacyDialog!=null)privacyDialog.dismiss();pageHost.finish();
|
||||
@Override protected void onStop(){dismissPrivacyUnlockDialog();if(controls!=null)controls.closeKeyboard();foreground=false;appUpdates.setEnabled(false);if(privacyDialog!=null)privacyDialog.dismiss();pageHost.finish();
|
||||
if(connectionDialog!=null)connectionDialog.dismiss();
|
||||
// Closing native PiP is an explicit end, not a request for invisible
|
||||
// fallback. An unavailable/failed PiP can hand its live session off.
|
||||
@@ -760,12 +832,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
mainHandler.removeCallbacks(presenceTick);recentPresence.setConnected(false,android.os.SystemClock.elapsedRealtime());announcementInbox.setConnected(false);
|
||||
cancelVideoSettingsTimeout();if(videoSettings.pending)videoSettings.expire(videoSettings.requestId);
|
||||
resetRemoteVersionCheck();if(disconnectDialog!=null)disconnectDialog.dismiss();
|
||||
}else if(documentSession==null||documentSession!=session||isFinishing()){if(session!=null)end(isFinishing()?"":"远程窗口已关闭,或后台保活未能启动,会话已结束");stopSignaling();}
|
||||
}else if(documentSession==null||documentSession!=session||isFinishing()){if(session!=null)end(isFinishing()?"":L10n.tr("远程窗口已关闭,或后台保活未能启动,会话已结束"));stopSignaling();}
|
||||
super.onStop();}
|
||||
@Override protected void onDestroy(){mainHandler.removeCallbacks(documentTimeout);documentSession=null;documentToSave=null;appUpdates.setEnabled(false);pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();
|
||||
@Override protected void onDestroy(){dismissPrivacyUnlockDialog();mainHandler.removeCallbacks(documentTimeout);documentSession=null;documentToSave=null;appUpdates.setEnabled(false);pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();
|
||||
if(connectionDialog!=null)connectionDialog.dismiss();
|
||||
boolean retained=!isFinishing()&&backgroundSession!=null&&!backgroundSession.ended&&RemoteSessionService.current()==backgroundSession;
|
||||
if(backgroundSession!=null)backgroundSession.detach(this);
|
||||
if(!retained){if(session!=null)session.close();stopSignaling();}else{mainHandler.removeCallbacks(presenceTick);session=signaling=null;}
|
||||
super.onDestroy();}
|
||||
remoteOrientation.end();super.onDestroy();}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ final class MobileUi {
|
||||
}
|
||||
TextView primary(String title,Runnable click) {
|
||||
TextView v=action(title,Color.WHITE,click);
|
||||
v.setMaxLines(2);v.setAutoSizeTextTypeUniformWithConfiguration(11,16,1,android.util.TypedValue.COMPLEX_UNIT_SP);
|
||||
GradientDrawable gradient=new GradientDrawable(GradientDrawable.Orientation.TL_BR,new int[]{BLUE,0xFF1F61C7});gradient.setCornerRadius(dp(12));v.setBackground(gradient);return v;
|
||||
}
|
||||
View iconButton(String icon,String label,Runnable click) {
|
||||
@@ -59,7 +60,7 @@ final class MobileUi {
|
||||
Icon glyph=new Icon(context,icon,INK);FrameLayout.LayoutParams p=new FrameLayout.LayoutParams(dp(22),dp(22),Gravity.CENTER);box.addView(glyph,p);
|
||||
box.setContentDescription(label);box.setClickable(true);box.setFocusable(true);box.setOnClickListener(v->click.run());return box;
|
||||
}
|
||||
View icon(String name,int color) { return new Icon(context,name,color); }
|
||||
Icon icon(String name,int color) { return new Icon(context,name,color); }
|
||||
void gap(LinearLayout parent,int height){parent.addView(new View(context),new LinearLayout.LayoutParams(1,dp(height)));}
|
||||
void divider(LinearLayout parent){View line=new View(context);line.setBackgroundColor(LINE);parent.addView(line,new LinearLayout.LayoutParams(-1,dp(.5f)));}
|
||||
LinearLayout.LayoutParams size(int width,int height){return new LinearLayout.LayoutParams(width<0?width:dp(width),height<0?height:dp(height));}
|
||||
@@ -93,8 +94,9 @@ final class MobileUi {
|
||||
}
|
||||
@android.annotation.SuppressLint("ViewConstructor") // Programmatic icon primitive, not inflated from XML.
|
||||
static final class Icon extends View {
|
||||
private final Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); private final String name;
|
||||
private final Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); private String name;
|
||||
Icon(Context context,String name,int color){super(context);this.name=name;paint.setColor(color);setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);}
|
||||
void setName(String value){if(name.equals(value))return;name=value;invalidate();}
|
||||
@Override protected void onDraw(Canvas c){
|
||||
c.save();c.scale(getWidth()/24f,getHeight()/24f);paint.setStyle(Paint.Style.STROKE);paint.setStrokeWidth(1.7f);paint.setStrokeCap(Paint.Cap.ROUND);paint.setStrokeJoin(Paint.Join.ROUND);
|
||||
switch(name){
|
||||
@@ -118,6 +120,8 @@ final class MobileUi {
|
||||
case "refresh":c.drawArc(4,4,20,20,35,310,false,paint);path(c,20,3,20,9,14,9);break;
|
||||
case "close":path(c,6,6,18,18);path(c,18,6,6,18);break;
|
||||
case "arrow":path(c,4,12,20,12);path(c,14,6,20,12,14,18);break;
|
||||
case "keyboard_hide":
|
||||
c.drawRoundRect(2,2,22,16,2,2,paint);for(int y=6;y<=9;y+=3)for(int x=6;x<=18;x+=4)c.drawPoint(x,y,paint);c.drawLine(7,13,17,13,paint);path(c,7,20,12,23,17,20);break;
|
||||
case "keyboard":
|
||||
c.drawRoundRect(2,5,22,19,3,3,paint);for(int y=9;y<=12;y+=3)for(int x=6;x<=18;x+=4)c.drawPoint(x,y,paint);c.drawLine(7,16,17,16,paint);break;
|
||||
case "display":c.drawRoundRect(2,3,22,17,2,2,paint);path(c,12,17,12,21);path(c,7,21,17,21);break;
|
||||
@@ -134,6 +138,14 @@ final class MobileUi {
|
||||
case "unlock":c.drawRoundRect(5,10,19,22,2,2,paint);c.drawArc(8,2,16,15,180,150,false,paint);c.drawPoint(12,16,paint);break;
|
||||
case "clipboard":c.drawRoundRect(4,4,20,22,2,2,paint);c.drawRoundRect(8,2,16,7,2,2,paint);break;
|
||||
case "eye":path(c,2,12,6,7,12,5,18,7,22,12,18,17,12,19,6,17,2,12);c.drawCircle(12,12,3,paint);break;
|
||||
case "eye_off":
|
||||
Path hiddenEye=new Path();
|
||||
hiddenEye.moveTo(10,5);hiddenEye.cubicTo(15,4,19,8,22,12);
|
||||
hiddenEye.quadTo(20.4f,14.7f,18,17);
|
||||
hiddenEye.moveTo(6,6);hiddenEye.quadTo(3.5f,8.5f,2,12);
|
||||
hiddenEye.cubicTo(7,20,13,21,18,17);
|
||||
hiddenEye.moveTo(10,10);hiddenEye.cubicTo(8,13,11,16,14,14);
|
||||
c.drawPath(hiddenEye,paint);path(c,3,3,21,21);break;
|
||||
default:c.drawCircle(12,12,9,paint);path(c,12,10,12,17);c.drawPoint(12,6,paint);
|
||||
}c.restore();
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@ final class NativeSession implements AutoCloseable {
|
||||
void statistics(RemoteNetworkStatistics.Snapshot value);
|
||||
default void fileTransfer(String name,double progress,boolean sending,File received) { }
|
||||
default void controlFeedback(String message) { }
|
||||
default void privacyChanged() { }
|
||||
default void signaling(int state, String deviceId) { }
|
||||
default void presence(JSONObject message) { }
|
||||
default void announcement(JSONObject message) { }
|
||||
default void announcementFailed(String requestId) { }
|
||||
default void passwordRejected() { ended("远端密码错误"); }
|
||||
default void passwordRejected() { ended(L10n.tr("远端密码错误")); }
|
||||
}
|
||||
// Serializes all peer creation/API calls/destruction, including across activities.
|
||||
private static final ExecutorService RTC = Executors.newSingleThreadExecutor(r -> new Thread(r, "CrossDesk RTC"));
|
||||
@@ -47,7 +48,9 @@ final class NativeSession implements AutoCloseable {
|
||||
private String password, identity = "";
|
||||
private final int port;
|
||||
private long handle;
|
||||
private boolean signalReady, joining, connected, started;
|
||||
private boolean signalReady, joining, started;
|
||||
private volatile boolean connected, supportsTextInput;
|
||||
final RemotePrivacyScreen privacyScreen = new RemotePrivacyScreen();
|
||||
private int signalState;
|
||||
private volatile long signalGeneration;
|
||||
private boolean presenceRequested;
|
||||
@@ -62,12 +65,14 @@ final class NativeSession implements AutoCloseable {
|
||||
private final RemoteNetworkStatistics networkStatistics=new RemoteNetworkStatistics();
|
||||
private final Runnable statisticsTick=new Runnable(){public void run(){
|
||||
if(closed)return;
|
||||
privacyScreen.tick(android.os.SystemClock.elapsedRealtime());
|
||||
listener.privacyChanged();
|
||||
listener.statistics(networkStatistics.snapshot(RemoteNetworkStatistics.now()));
|
||||
if(!closed)main.postDelayed(this,1000);
|
||||
}};
|
||||
private final Runnable timeout = () -> fail(remote.isEmpty()
|
||||
? "服务器连接超时,请检查服务器地址和网络"
|
||||
: "连接超时,请检查设备 ID、服务器及远端在线状态");
|
||||
? L10n.tr("服务器连接超时,请检查服务器地址和网络")
|
||||
: L10n.tr("连接超时,请检查设备 ID、服务器及远端在线状态"));
|
||||
|
||||
NativeSession(Context context, String host, int port, String remote, String password, Listener listener) {
|
||||
this.context = context.getApplicationContext(); this.listener = listener;
|
||||
@@ -87,10 +92,10 @@ final class NativeSession implements AutoCloseable {
|
||||
File logs = new File(context.getNoBackupFilesDir(), "logs");
|
||||
if (!logs.isDirectory() && !logs.mkdirs()) throw new IllegalStateException("Log directory unavailable");
|
||||
handle = nCreate(this, host, port, identity, logs.getAbsolutePath(), certificates.getAbsolutePath());
|
||||
if (handle == 0) { fail("原生连接初始化失败"); return; }
|
||||
if (handle == 0) { fail(L10n.tr("原生连接初始化失败")); return; }
|
||||
nFileDirectory(handle,new File(documents.directory,"received").getAbsolutePath());
|
||||
if (surface != null && surface.isValid()) nSurface(handle, surface);
|
||||
} catch (Exception error) { fail("安全存储或证书初始化失败,请重试或清除应用数据"); }
|
||||
} catch (Exception error) { fail(L10n.tr("安全存储或证书初始化失败,请重试或清除应用数据")); }
|
||||
});
|
||||
}
|
||||
/** Promote the already registered identity peer to an outgoing desktop session. */
|
||||
@@ -142,7 +147,7 @@ final class NativeSession implements AutoCloseable {
|
||||
if (handle == 0 || joining || remote.isEmpty() || !signalReady || identity.isEmpty()) return;
|
||||
joining = true;
|
||||
String base = identity.split("@", 2)[0];
|
||||
if (!nConnect(handle, base, remote, password)) fail("无法发起远程连接");
|
||||
if (!nConnect(handle, base, remote, password)) fail(L10n.tr("无法发起远程连接"));
|
||||
password = "";
|
||||
}
|
||||
// Called on arbitrary native callback threads; borrowed payloads were copied by JNI.
|
||||
@@ -170,10 +175,10 @@ final class NativeSession implements AutoCloseable {
|
||||
publishSignaling();
|
||||
tryJoin();
|
||||
}
|
||||
if (status == 6) fail("服务器 TLS 证书校验失败");
|
||||
else if (status == 2) fail("服务器登录失败,请检查服务器地址和网络");
|
||||
else if (status >= 3 && connected) fail("信令连接中断,请重新连接");
|
||||
else if (status == 0 || status == 4) ui(() -> listener.status("正在连接信令服务器…"));
|
||||
if (status == 6) fail(L10n.tr("服务器 TLS 证书校验失败"));
|
||||
else if (status == 2) fail(L10n.tr("服务器登录失败,请检查服务器地址和网络"));
|
||||
else if (status >= 3 && connected) fail(L10n.tr("信令连接中断,请重新连接"));
|
||||
else if (status == 0 || status == 4) ui(() -> listener.status(L10n.tr("正在连接信令服务器…")));
|
||||
} else if (type == 2) {
|
||||
int status = event.getInt("status");
|
||||
if (status == 1) {
|
||||
@@ -182,14 +187,32 @@ final class NativeSession implements AutoCloseable {
|
||||
audio = new AudioPlayer();
|
||||
nControl(handle, 2, 1);
|
||||
ui(() -> { listener.connected(); if(!closed){statisticsTick.run();fileTick.run();} });
|
||||
} else if (status == 0 || status == 2) ui(() -> listener.status("正在建立安全连接…"));
|
||||
} else if (status == 0 || status == 2) ui(() -> listener.status(L10n.tr("正在建立安全连接…")));
|
||||
else if (status == 6) ui(() -> { listener.passwordRejected(); close(); });
|
||||
else {
|
||||
String[] reasons = {"", "", "", "连接已断开", "连接失败", "远端已关闭连接",
|
||||
"远端密码错误", "找不到该设备 ID", "远端当前不可用"};
|
||||
fail(status >= 0 && status < reasons.length ? reasons[status] : "连接已结束");
|
||||
String[] reasons = {"", "", "", L10n.tr("连接已断开"), L10n.tr("连接失败"), L10n.tr("远端已关闭连接"),
|
||||
L10n.tr("远端密码错误"), L10n.tr("找不到该设备 ID"), L10n.tr("远端当前不可用")};
|
||||
fail(status >= 0 && status < reasons.length ? reasons[status] : L10n.tr("连接已结束"));
|
||||
}
|
||||
} else if (type == 4) ui(() -> listener.data(event));
|
||||
} else if (type == 4) {
|
||||
if(event.optInt("type",-1)==14){
|
||||
networkStatistics.receiveRemoteEncoder(event.getJSONObject("video_codec").getString("encoder"));
|
||||
return;
|
||||
}
|
||||
JSONObject info=event.optJSONObject("host_info");
|
||||
if(info!=null)supportsTextInput=info.optBoolean("supports_text_input",false);
|
||||
ui(() -> {
|
||||
if(event.optInt("type",-1)==10){
|
||||
privacyScreen.receive(event,android.os.SystemClock.elapsedRealtime());
|
||||
listener.privacyChanged();
|
||||
} else if(event.optInt("type",-1)==5){
|
||||
JSONObject service=event.optJSONObject("service_status");
|
||||
privacyScreen.setInteractiveStage(service==null?"":service.optString("interactive_stage",""));
|
||||
listener.privacyChanged();
|
||||
}
|
||||
listener.data(event);
|
||||
});
|
||||
}
|
||||
else if (type == 5) {
|
||||
int width = event.getInt("width"), height = event.getInt("height");
|
||||
ui(() -> listener.videoSize(width, height));
|
||||
@@ -205,7 +228,7 @@ final class NativeSession implements AutoCloseable {
|
||||
listener.fileTransfer(name,progress,sending,path.isEmpty()?null:new File(path));});
|
||||
}
|
||||
} catch (Exception error) {
|
||||
if (type == 3) fail("设备身份保存失败,请检查安全存储");
|
||||
if (type == 3) fail(L10n.tr("设备身份保存失败,请检查安全存储"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -239,11 +262,40 @@ final class NativeSession implements AutoCloseable {
|
||||
void surface(Surface value) { execute(() -> { surface = value; if (handle != 0) nSurface(handle, value); }); }
|
||||
void pointer(float x, float y, int flag, int wheel) { execute(() -> { if (connected) nPointer(handle, x, y, flag, wheel); }); }
|
||||
void key(int code, boolean down) { execute(() -> { if (connected) nKey(handle, code, down); }); }
|
||||
boolean textEdit(int deletions,String text) {
|
||||
byte[] bytes=text.getBytes(StandardCharsets.UTF_8);
|
||||
boolean unicode=supportsTextInput;
|
||||
if(closed||!connected||deletions<0||deletions>4096||bytes.length>16*1024||text.indexOf('\0')>=0)return false;
|
||||
if(!unicode)for(char ch:text.toCharArray())if(KeyMap.ascii(ch)==0)return false;
|
||||
// Validate the entire edit before deleting anything. One RTC task preserves order.
|
||||
execute(()->{
|
||||
if(!connected)return;
|
||||
for(int i=0;i<deletions;i++){nKey(handle,8,true);nKey(handle,8,false);}
|
||||
if(unicode){if(bytes.length>0)nText(handle,bytes);}
|
||||
else for(char ch:text.toCharArray()){
|
||||
int code=KeyMap.ascii(ch);boolean shift=(code&0x100)!=0;
|
||||
if(shift)nKey(handle,16,true);nKey(handle,code&255,true);nKey(handle,code&255,false);if(shift)nKey(handle,16,false);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
void control(int type, int value) {
|
||||
AudioPlayer player = audio;
|
||||
if (type == 2 && player != null) player.setEnabled(value != 0);
|
||||
execute(() -> { if (connected){nControl(handle, type, value);if(type==4)networkStatistics.resetVideo();} });
|
||||
}
|
||||
void lockPrivacy() { sendPrivacy(true); }
|
||||
void togglePrivacy() { sendPrivacy(false); }
|
||||
private void sendPrivacy(boolean lock) {
|
||||
if(closed||!connected)return;
|
||||
boolean enable=!privacyScreen.active();
|
||||
long request=privacyScreen.begin(android.os.SystemClock.elapsedRealtime(),lock);
|
||||
if(request<0)return;
|
||||
listener.privacyChanged();
|
||||
execute(()->{if(connected&&!nPrivacy(handle,lock?3:enable?1:2,true))ui(()->{
|
||||
privacyScreen.failed(request);listener.privacyChanged();
|
||||
});});
|
||||
}
|
||||
void clipboard(String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
if (bytes.length == 0 || bytes.length > 128 * 1024) return;
|
||||
@@ -251,24 +303,24 @@ final class NativeSession implements AutoCloseable {
|
||||
}
|
||||
void secureAttention(){execute(()->{
|
||||
boolean sent=connected&&nSecureAttention(handle);
|
||||
ui(()->listener.controlFeedback(sent?"已发送 Ctrl+Alt+Del":"指令发送失败,请重试"));
|
||||
ui(()->listener.controlFeedback(sent?L10n.tr("已发送 Ctrl+Alt+Del"):L10n.tr("指令发送失败,请重试")));
|
||||
});}
|
||||
void sendFile(android.net.Uri uri){
|
||||
if(closed)return;
|
||||
if(sendingFile){listener.controlFeedback("请等待当前文件发送完成");return;}
|
||||
sendingFile=true;listener.fileTransfer("正在读取所选文件…",0,true,null);
|
||||
if(sendingFile){listener.controlFeedback(L10n.tr("请等待当前文件发送完成"));return;}
|
||||
sendingFile=true;listener.fileTransfer(L10n.tr("正在读取所选文件…"),0,true,null);
|
||||
documents.prepare(uri,new SessionDocuments.Prepared(){
|
||||
public void ready(File file,String name){execute(()->{
|
||||
if(!connected||!nSendFile(handle,file.getAbsolutePath(),name.getBytes(StandardCharsets.UTF_8))){
|
||||
file.delete();ui(()->{sendingFile=false;listener.fileTransfer(name,-1,true,null);});
|
||||
}
|
||||
});}
|
||||
public void failed(){ui(()->{sendingFile=false;listener.fileTransfer("所选文件(无法读取或空间不足)",-1,true,null);});}
|
||||
public void failed(){ui(()->{sendingFile=false;listener.fileTransfer(L10n.tr("所选文件(无法读取或空间不足)"),-1,true,null);});}
|
||||
});
|
||||
}
|
||||
void saveFile(File file,android.net.Uri destination){
|
||||
if(closed)return;
|
||||
documents.save(file,destination,success->ui(()->listener.controlFeedback(success?"文件已保存":"文件保存失败,请重试")));
|
||||
documents.save(file,destination,success->ui(()->listener.controlFeedback(success?L10n.tr("文件已保存"):L10n.tr("文件保存失败,请重试"))));
|
||||
}
|
||||
@Override public void close() {
|
||||
if (closed) return;
|
||||
@@ -290,12 +342,14 @@ final class NativeSession implements AutoCloseable {
|
||||
private static native void nReady(long handle);
|
||||
private static native void nPointer(long handle, float x, float y, int flag, int wheel);
|
||||
private static native void nKey(long handle, int code, boolean down);
|
||||
private static native void nText(long handle,byte[] text);
|
||||
private static native void nControl(long handle, int type, int value);
|
||||
private static native boolean nVideoSettings(long handle,int quality,int frameRate,int preference,long requestId);
|
||||
private static native boolean nAnnouncementRequest(long handle,byte[] data);
|
||||
private static native boolean nPresenceRequest(long handle,byte[] data);
|
||||
private static native void nClipboard(long handle, byte[] data);
|
||||
private static native boolean nSecureAttention(long handle);
|
||||
private static native boolean nPrivacy(long handle,int flag,boolean blockLocalInput);
|
||||
private static native void nFileDirectory(long handle,String directory);
|
||||
private static native boolean nSendFile(long handle,String path,byte[] name);
|
||||
private static native boolean nPumpFiles(long handle);
|
||||
|
||||
@@ -20,16 +20,16 @@ final class PrivacyConsentDialog extends Dialog {
|
||||
PrivacyConsentDialog(Activity activity,MobileUi ui,PrivacyPolicyDocument policy,Runnable accepted){
|
||||
super(activity);this.activity=activity;this.ui=ui;requestWindowFeature(Window.FEATURE_NO_TITLE);setCancelable(false);setCanceledOnTouchOutside(false);
|
||||
LinearLayout sheet=ui.column();sheet.setBackground(ui.background(android.graphics.Color.WHITE,20));sheet.setClipToOutline(true);
|
||||
TextView title=ui.text("隐私政策",17,true);title.setGravity(Gravity.CENTER);sheet.addView(title,ui.size(-1,56));ui.divider(sheet);
|
||||
TextView title=ui.text(L10n.tr("隐私政策"),17,true);title.setGravity(Gravity.CENTER);sheet.addView(title,ui.size(-1,56));ui.divider(sheet);
|
||||
sheet.addView(policy.view(ui),new LinearLayout.LayoutParams(-1,0,1));ui.divider(sheet);
|
||||
LinearLayout actions=ui.column();actions.setPadding(ui.dp(20),ui.dp(12),ui.dp(20),ui.dp(12));sheet.addView(actions);
|
||||
CheckBox hasReadPolicy=new CheckBox(activity);hasReadPolicy.setText("我已阅读并同意《隐私政策》");hasReadPolicy.setTextColor(INK);hasReadPolicy.setTextSize(14);hasReadPolicy.setMinHeight(ui.dp(44));actions.addView(hasReadPolicy,ui.size(-1,-2));ui.gap(actions,12);
|
||||
TextView agree=ui.primary("同意并继续",()->{
|
||||
CheckBox hasReadPolicy=new CheckBox(activity);hasReadPolicy.setText(L10n.tr("我已阅读并同意《隐私政策》"));hasReadPolicy.setTextColor(INK);hasReadPolicy.setTextSize(14);hasReadPolicy.setMinHeight(ui.dp(44));actions.addView(hasReadPolicy,ui.size(-1,-2));ui.gap(actions,12);
|
||||
TextView agree=ui.primary(L10n.tr("同意并继续"),()->{
|
||||
if(!hasReadPolicy.isChecked()||!policy.available)return;
|
||||
dismiss();accepted.run();
|
||||
});agree.setEnabled(false);agree.setAlpha(.45f);actions.addView(agree,ui.size(-1,48));
|
||||
hasReadPolicy.setOnCheckedChangeListener((button,checked)->{boolean enabled=checked&&policy.available;agree.setEnabled(enabled);agree.setAlpha(enabled?1:.45f);});
|
||||
ui.gap(actions,12);actions.addView(ui.action("暂不同意",SECONDARY,this::dismiss),ui.size(-1,44));
|
||||
ui.gap(actions,12);actions.addView(ui.action(L10n.tr("暂不同意"),SECONDARY,this::dismiss),ui.size(-1,44));
|
||||
setContentView(sheet);Window window=getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setDimAmount(.22f);}
|
||||
}
|
||||
@Override public void show(){
|
||||
|
||||
@@ -13,12 +13,14 @@ final class PrivacyPolicyDocument {
|
||||
final boolean available;
|
||||
final String text;
|
||||
|
||||
PrivacyPolicyDocument(Context context){this(load(context));}
|
||||
PrivacyPolicyDocument(String document){
|
||||
PrivacyPolicyDocument(Context context){this(load(context),L10n.isChinese());}
|
||||
PrivacyPolicyDocument(String document){this(document,true);}
|
||||
private PrivacyPolicyDocument(String document,boolean chinese){
|
||||
available=document!=null&&!document.trim().isEmpty();
|
||||
String value=available?document:"隐私政策暂时无法读取,请拒绝联网并重新安装应用。";
|
||||
String value=available?document:L10n.tr("隐私政策暂时无法读取,请拒绝联网并重新安装应用。");
|
||||
int start=value.indexOf("## 中文\n"),end=start<0?-1:value.indexOf("## English",start);
|
||||
text=start>=0&&end>start?value.substring(start+"## 中文\n".length(),end).trim():value;
|
||||
text=chinese&&start>=0&&end>start?value.substring(start+"## 中文\n".length(),end).trim():
|
||||
!chinese&&end>=0?value.substring(end+"## English".length()).trim():value;
|
||||
}
|
||||
private static String load(Context context){
|
||||
try{return LicenseCatalog.read(context,"PRIVACY.md");}catch(IOException error){return null;}
|
||||
@@ -28,7 +30,7 @@ final class PrivacyPolicyDocument {
|
||||
LinearLayout content=ui.column();content.setPadding(ui.dp(20),ui.dp(20),ui.dp(20),ui.dp(20));scroll.addView(content);
|
||||
for(String paragraph:text.split("\n\n")){
|
||||
paragraph=paragraph.trim();if(paragraph.isEmpty()||paragraph.equals("---"))continue;
|
||||
boolean heading=paragraph.startsWith("#"),date=paragraph.startsWith("更新日期:");
|
||||
boolean heading=paragraph.startsWith("#"),date=paragraph.startsWith("更新日期:")||paragraph.startsWith("Last updated:");
|
||||
String value=heading?paragraph.replaceFirst("^#+\\s*",""):paragraph.replace("**","");
|
||||
TextView label=ui.text(value,date?12:16,heading);label.setTextIsSelectable(true);label.setLineSpacing(ui.dp(4),1);
|
||||
if(date)label.setTextColor(SECONDARY);
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.*;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** Docked native IME accessory and two-page desktop keyboard share one toolbar. */
|
||||
@android.annotation.SuppressLint("ViewConstructor")
|
||||
final class RemoteKeyboard extends LinearLayout {
|
||||
private final MobileUi ui;
|
||||
private final SessionControls.Actions actions;
|
||||
final RemoteKeyboardInput editor;
|
||||
private final Runnable changed;
|
||||
private final LinearLayout toolbar,keys;
|
||||
private final TextView switchButton,lettersButton,functionsButton;
|
||||
private final Set<Integer> modifiers=new LinkedHashSet<>();
|
||||
private final SparseIntArray heights=new SparseIntArray();
|
||||
private boolean opened,computer,functions,imeVisible,measuring,switching,targetComputer;
|
||||
private int imeHeight,navigationBottom,layoutWidth,layoutHeight,revision,lastInsetsHeight=-1,lastInsetsKey;
|
||||
private CancellationSignal transition;
|
||||
|
||||
RemoteKeyboard(Context context,MobileUi ui,SessionControls.Actions actions,Runnable changed) {
|
||||
super(context);this.ui=ui;this.actions=actions;this.changed=changed;setOrientation(VERTICAL);
|
||||
setVisibility(GONE);setClickable(true);
|
||||
editor=new RemoteKeyboardInput(context,new RemoteKeyboardInput.Sender(){
|
||||
public boolean edit(int count,String text){return actions.textEdit(count,text);}
|
||||
public void key(int code){actions.key(code);}
|
||||
});
|
||||
FrameLayout header=new FrameLayout(context);addView(header,new LayoutParams(-1,ui.dp(44)));
|
||||
header.addView(editor,new FrameLayout.LayoutParams(1,1,Gravity.BOTTOM));
|
||||
toolbar=ui.row();toolbar.setGravity(Gravity.CENTER_VERTICAL);toolbar.setPadding(ui.dp(6),0,ui.dp(6),0);
|
||||
header.addView(toolbar,new FrameLayout.LayoutParams(-1,-1));
|
||||
switchButton=button(L10n.tr("电脑键盘"),this::switchKeyboard);switchButton.setPadding(ui.dp(8),0,ui.dp(8),0);
|
||||
toolbar.addView(switchButton,new LayoutParams(-2,-1));
|
||||
LinearLayout pages=ui.row();pages.setGravity(Gravity.CENTER);toolbar.addView(pages,new LayoutParams(0,-1,1));
|
||||
lettersButton=button(L10n.tr("字母数字"),()->page(false));functionsButton=button(L10n.tr("符号功能"),()->page(true));
|
||||
pages.addView(lettersButton,new LayoutParams(-2,ui.dp(32)));pages.addView(functionsButton,new LayoutParams(-2,ui.dp(32)));
|
||||
FrameLayout hide=new FrameLayout(context);hide.setContentDescription(L10n.tr("收起键盘"));hide.setClickable(true);hide.setFocusable(true);
|
||||
hide.addView(ui.icon("keyboard_hide",foreground()),new FrameLayout.LayoutParams(ui.dp(24),ui.dp(24),Gravity.CENTER));
|
||||
hide.setOnClickListener(v->{feedback(v);close();});toolbar.addView(hide,new LayoutParams(ui.dp(44),ui.dp(44)));
|
||||
keys=ui.column();keys.setPadding(ui.dp(3),ui.dp(4),ui.dp(3),ui.dp(4));addView(keys,new LayoutParams(-1,0,1));
|
||||
render();
|
||||
}
|
||||
private boolean dark(){return (getResources().getConfiguration().uiMode&Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;}
|
||||
private int foreground(){return dark()?Color.WHITE:0xFF202124;}
|
||||
private TextView button(String label,Runnable action){
|
||||
TextView view=ui.action(label,foreground(),action);view.setTypeface(android.graphics.Typeface.DEFAULT);
|
||||
view.setTextSize(13);view.setMinHeight(0);view.setPadding(ui.dp(8),0,ui.dp(8),0);
|
||||
view.setOnClickListener(v->{feedback(v);action.run();});return view;
|
||||
}
|
||||
private void feedback(View view){view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);}
|
||||
boolean isOpen(){return opened;}
|
||||
boolean isComputer(){return computer;}
|
||||
int bottomInset(){return opened&&!computer?imeHeight:0;}
|
||||
private int sizeKey(){Configuration c=getResources().getConfiguration();return c.screenWidthDp*10000+c.screenHeightDp;}
|
||||
private int keyAreaHeight(){
|
||||
int height=heights.get(sizeKey(),0);
|
||||
if(height==0){Configuration c=getResources().getConfiguration();height=ui.dp(c.screenWidthDp>c.screenHeightDp?200:280);}
|
||||
int available=getParent() instanceof View?((View)getParent()).getMeasuredHeight()-ui.dp(44):height;
|
||||
return available>0?Math.min(height,available):height;
|
||||
}
|
||||
int preferredHeight(){return ui.dp(44)+(computer?keyAreaHeight():0);}
|
||||
void toggle(){if(opened)close();else open();}
|
||||
void open(){
|
||||
opened=true;computer=false;modifiers.clear();editor.start();setVisibility(VISIBLE);render();changed.run();
|
||||
post(()->{if(opened&&!computer)changeIme(true,false,false);});
|
||||
}
|
||||
boolean close(){
|
||||
if(!opened)return false;opened=false;measuring=switching=false;cancelTransition();modifiers.clear();editor.stop();
|
||||
getContext().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getWindowToken(),0);
|
||||
setVisibility(GONE);changed.run();return true;
|
||||
}
|
||||
void resetContext(){editor.resetContext();}
|
||||
private void page(boolean next){functions=next;render();}
|
||||
private void switchKeyboard(){
|
||||
if(!opened||switching)return;
|
||||
targetComputer=!computer;switching=true;modifiers.clear();
|
||||
if(targetComputer){
|
||||
editor.pause();computer=true;render();changed.run();
|
||||
}else editor.start();
|
||||
// Lay out computer keys behind the still-visible IME before onReady,
|
||||
// which runs after layout. Keep them until the native IME is ready too.
|
||||
if(targetComputer&&Build.VERSION.SDK_INT>=30&&isHardwareAccelerated()){
|
||||
int token=revision;
|
||||
// Insets surfaces can update ahead of the app's render thread.
|
||||
// Submit the replacement frame before releasing the IME surface.
|
||||
getViewTreeObserver().registerFrameCommitCallback(()->post(()->{
|
||||
if(opened&&switching&&targetComputer&&token==revision)changeIme(false,true,false);
|
||||
}));
|
||||
invalidate();
|
||||
}else changeIme(!targetComputer,true,false);
|
||||
}
|
||||
private void finishSwitch(){
|
||||
if(!switching)return;switching=false;
|
||||
if(computer!=targetComputer){computer=targetComputer;render();}
|
||||
changed.run();
|
||||
}
|
||||
void updateInsets(int height,boolean visible,int navigation){
|
||||
int key=sizeKey();
|
||||
if(lastInsetsHeight==height&&lastInsetsKey==key&&imeVisible==visible&&navigationBottom==navigation)return;
|
||||
lastInsetsHeight=height;lastInsetsKey=key;
|
||||
navigationBottom=navigation;imeHeight=Build.VERSION.SDK_INT>=30?Math.max(0,height):0;
|
||||
boolean wasVisible=imeVisible;imeVisible=visible;
|
||||
if(visible&&height>ui.dp(80)){
|
||||
heights.put(sizeKey(),height);
|
||||
if(measuring){measuring=false;post(()->{if(opened&&computer)changeIme(false,true,false);});}
|
||||
}
|
||||
if(switching&&visible!=targetComputer&&(!visible||height>ui.dp(80)))finishSwitch();
|
||||
if(opened&&!computer&&!switching&&wasVisible&&!visible&&transition==null){close();return;}
|
||||
if(opened){renderKeysIfNeeded(getMeasuredWidth());changed.run();}
|
||||
}
|
||||
private void cancelTransition(){revision++;if(transition!=null){CancellationSignal old=transition;transition=null;old.cancel();}}
|
||||
private void changeIme(boolean show,boolean immediate,boolean measureOnly){
|
||||
cancelTransition();if(!opened)return;
|
||||
measuring=measureOnly;
|
||||
if(Build.VERSION.SDK_INT>=30&&immediate&&getWindowInsetsController()!=null){
|
||||
// Request control directly: showSoftInput first starts a competing
|
||||
// system animation before we can replace the computer keyboard.
|
||||
if(show)editor.start();else if(measureOnly)editor.requestFocus();
|
||||
int token=revision;transition=new CancellationSignal();
|
||||
getWindowInsetsController().controlWindowInsetsAnimation(WindowInsets.Type.ime(),0,null,transition,new WindowInsetsAnimationControlListener(){
|
||||
public void onReady(WindowInsetsAnimationController controller,int types){
|
||||
if(token!=revision||!opened){controller.finish(false);return;}
|
||||
int height=controller.getShownStateInsets().bottom-navigationBottom;
|
||||
if(height>ui.dp(80)){
|
||||
heights.put(sizeKey(),height);measuring=false;
|
||||
if(!measureOnly){imeHeight=show?height:0;finishSwitch();}
|
||||
controller.finish(show&&!measureOnly);
|
||||
}else if(measureOnly){
|
||||
// Some IMEs report zero until their new input view is laid out.
|
||||
// The first nonzero inset completes measurement and hides it.
|
||||
controller.finish(true);
|
||||
}else controller.finish(show);
|
||||
renderKeysIfNeeded(getMeasuredWidth());changed.run();
|
||||
}
|
||||
public void onFinished(WindowInsetsAnimationController controller){if(token==revision)transition=null;}
|
||||
public void onCancelled(WindowInsetsAnimationController controller){
|
||||
if(token!=revision)return;transition=null;
|
||||
if(opened)normalIme(show||measureOnly);
|
||||
}
|
||||
});
|
||||
}else normalIme(show||measureOnly);
|
||||
}
|
||||
private void normalIme(boolean show){
|
||||
InputMethodManager manager=getContext().getSystemService(InputMethodManager.class);
|
||||
if(show){if(computer)editor.requestFocus();else editor.start();manager.showSoftInput(editor,InputMethodManager.SHOW_IMPLICIT);}
|
||||
else manager.hideSoftInputFromWindow(editor.getWindowToken(),0);
|
||||
}
|
||||
private void render(){
|
||||
setBackgroundColor(dark()?0xFF202124:0xFFE8EAED);
|
||||
switchButton.setText(computer?L10n.tr("系统键盘"):L10n.tr("电脑键盘"));switchButton.setTextColor(foreground());
|
||||
lettersButton.setVisibility(computer?VISIBLE:GONE);functionsButton.setVisibility(computer?VISIBLE:GONE);
|
||||
for(TextView view:new TextView[]{lettersButton,functionsButton}){
|
||||
boolean selected=(view==functionsButton)==functions;view.setSelected(selected);
|
||||
view.setTextColor(selected?(dark()?0xFFA8C7FA:0xFF185ABC):foreground());
|
||||
view.setBackground(ui.background(selected?(dark()?0xFF394457:0xFFD2E3FC):Color.TRANSPARENT,6));
|
||||
}
|
||||
keys.setVisibility(computer?VISIBLE:GONE);layoutWidth=layoutHeight=0;renderKeysIfNeeded(getMeasuredWidth());
|
||||
}
|
||||
private void renderKeysIfNeeded(int width){
|
||||
if(!computer)return;int height=keyAreaHeight();
|
||||
if(layoutWidth==width&&layoutHeight==height)return;layoutWidth=width;layoutHeight=height;keys.removeAllViews();
|
||||
boolean landscape=getResources().getConfiguration().screenWidthDp>getResources().getConfiguration().screenHeightDp;
|
||||
String[][] upper;
|
||||
if(!functions)upper=new String[][]{{"1","2","3","4","5","6","7","8","9","0"},{"q","w","e","r","t","y","u","i","o","p"},{"a","s","d","f","g","h","j","k","l"}};
|
||||
else if(landscape)upper=new String[][]{{"`","-","=","[","]","\\",";","'",",",".","/"},{"F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12"},{"Esc","Tab","Caps","Ins","Del","Home","End"}};
|
||||
else upper=new String[][]{{"`","-","=","[","]","\\",";","'",",",".","/"},{"F1","F2","F3","F4","F5","F6"},{"F7","F8","F9","F10","F11","F12"},{"Esc","Tab","Caps","Ins","Del","Home","End"}};
|
||||
LinearLayout upperKeys=ui.column();keys.addView(upperKeys,new LayoutParams(-1,0,1));
|
||||
for(String[] row:upper)upperKeys.addView(keyRow(row),new LayoutParams(-1,0,1));
|
||||
// The same bottom two rows keep their geometry on both pages.
|
||||
int rowHeight=Math.max(1,(height-ui.dp(8))/5);
|
||||
keys.addView(keyRow(functions?new String[]{"⇧","←","↓","↑","→","PgUp","PgDn","⌫"}:new String[]{"⇧","z","x","c","v","b","n","m","⌫"}),new LayoutParams(-1,rowHeight));
|
||||
keys.addView(keyRow(new String[]{"Ctrl","Win","Alt","空格","Enter"}),new LayoutParams(-1,rowHeight));
|
||||
}
|
||||
private LinearLayout keyRow(String[] values){
|
||||
LinearLayout line=ui.row();line.setGravity(Gravity.CENTER);
|
||||
for(String value:values){
|
||||
boolean shift=modifiers.contains(16);String shown=shift&&value.length()==1?shifted(value):value;
|
||||
TextView key=button(L10n.tr(shown),()->key(value));key.setPadding(0,0,0,0);
|
||||
key.setTextSize(value.length()>1?12:18);key.setSingleLine(true);
|
||||
int modifier=modifier(value);boolean selected=modifier!=0&&modifiers.contains(modifier);key.setSelected(selected);
|
||||
int color=selected?(dark()?0xFF596D8C:0xFFA8C7FA):value.length()>1||value.equals("⇧")||value.equals("⌫")?(dark()?0xFF30343B:0xFFD0D5DC):(dark()?0xFF3C4043:Color.WHITE);
|
||||
StateListDrawable background=new StateListDrawable();background.addState(new int[]{android.R.attr.state_pressed},ui.background(dark()?0xFF68717D:0xFFBDC9D8,7));background.addState(new int[]{},ui.background(color,7));key.setBackground(background);
|
||||
if(value.equals("⇧"))key.setContentDescription("Shift");else if(value.equals("⌫"))key.setContentDescription(L10n.tr("退格"));
|
||||
// Both penultimate rows allocate exactly 1/8 to Shift and Backspace.
|
||||
boolean penultimate=values[0].equals("⇧");float weight=value.equals("空格")?4:1;
|
||||
if(penultimate)weight=value.equals("⇧")||value.equals("⌫")?1:(6f/(values.length-2));
|
||||
boolean edge=penultimate&&(value.equals("⇧")||value.equals("⌫"));
|
||||
int edgeWidth=Math.max(1,(layoutWidth-ui.dp(6))/8-ui.dp(5));
|
||||
LayoutParams p=new LayoutParams(edge?edgeWidth:0,-1,edge?0:weight);p.setMargins(ui.dp(2.5f),ui.dp(3),ui.dp(2.5f),ui.dp(3));line.addView(key,p);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
private static int modifier(String value){return switch(value){case "⇧"->16;case "Ctrl"->17;case "Alt"->18;case "Win"->91;default->0;};}
|
||||
private static String shifted(String value){
|
||||
String plain="1234567890`-=[]\\;',./",shifted="!@#$%^&*()~_+{}|:\"<>?";int index=plain.indexOf(value);
|
||||
return index>=0?shifted.substring(index,index+1):value.toUpperCase(java.util.Locale.ROOT);
|
||||
}
|
||||
private void key(String value){
|
||||
int modifier=modifier(value);if(modifier!=0){if(!modifiers.add(modifier))modifiers.remove(modifier);render();return;}
|
||||
int code=switch(value){case "空格"->32;case "Enter"->13;case "⌫"->8;case "Esc"->27;case "Tab"->9;case "Caps"->20;case "Ins"->45;case "Del"->46;case "Home"->36;case "End"->35;case "PgUp"->33;case "PgDn"->34;case "←"->37;case "↑"->38;case "→"->39;case "↓"->40;default->value.startsWith("F")?111+Integer.parseInt(value.substring(1)):KeyMap.ascii(value.charAt(0))&255;};
|
||||
actions.chord(code,modifiers.stream().mapToInt(Integer::intValue).toArray());
|
||||
}
|
||||
@Override protected void onMeasure(int widthSpec,int heightSpec){
|
||||
renderKeysIfNeeded(MeasureSpec.getSize(widthSpec));super.onMeasure(widthSpec,heightSpec);
|
||||
}
|
||||
@Override protected void onSizeChanged(int width,int height,int oldWidth,int oldHeight){
|
||||
super.onSizeChanged(width,height,oldWidth,oldHeight);
|
||||
if(opened&&computer&&(width!=oldWidth||height!=oldHeight)&&heights.get(sizeKey(),0)==0)post(()->{
|
||||
if(opened&&computer&&!switching&&!measuring&&transition==null&&heights.get(sizeKey(),0)==0)changeIme(false,true,true);
|
||||
});
|
||||
}
|
||||
@Override protected void onDetachedFromWindow(){close();super.onDetachedFromWindow();}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import android.icu.text.BreakIterator;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputConnectionWrapper;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** A real editor keeps IME candidates, corrections and dictation local until committed. */
|
||||
@android.annotation.SuppressLint("ViewConstructor")
|
||||
final class RemoteKeyboardInput extends EditText {
|
||||
interface Sender {
|
||||
boolean edit(int deletions, String text);
|
||||
void key(int code);
|
||||
}
|
||||
private final Sender sender;
|
||||
private final android.os.Handler main=new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
private String sent = "";
|
||||
private boolean accepting, resetting, enterPending;
|
||||
private int batches, generation;
|
||||
private final Runnable synchronize = this::flush;
|
||||
private void flush(){if(synchronize()&&enterPending){sender.key(13);resetContext();}}
|
||||
|
||||
RemoteKeyboardInput(Context context, Sender sender) {
|
||||
super(context);this.sender=sender;
|
||||
setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_NONE);
|
||||
setCursorVisible(false);setBackground(null);setPadding(0,0,0,0);setAlpha(0);
|
||||
setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
|
||||
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
addTextChangedListener(new TextWatcher() {
|
||||
public void beforeTextChanged(CharSequence s,int start,int count,int after) { }
|
||||
public void onTextChanged(CharSequence s,int start,int before,int count) { }
|
||||
public void afterTextChanged(Editable value) { schedule(); }
|
||||
});
|
||||
}
|
||||
void start() {
|
||||
boolean resume=!accepting&&hasFocus();accepting=true;requestFocus();
|
||||
if(resume)getContext().getSystemService(InputMethodManager.class).restartInput(this);
|
||||
}
|
||||
// Keep focus while replacing the IME, but reject its late edits without
|
||||
// restarting it. A restart here can show it again after the controlled hide.
|
||||
void pause() { accepting=false;resetContext(); }
|
||||
void stop() { pause();clearFocus(); }
|
||||
boolean isAcceptingInput() { return accepting; }
|
||||
void resetContext() {
|
||||
generation++;batches=0;main.removeCallbacks(synchronize);resetting=true;
|
||||
// EditText.setText replaces the buffer and internally restarts the IME,
|
||||
// even while paused. Clear in place so only the explicit restart below
|
||||
// can renew the input connection.
|
||||
enterPending=false;sent="";BaseInputConnection.removeComposingSpans(getText());getText().clear();setSelection(0);resetting=false;
|
||||
if(accepting&&hasFocus())getContext().getSystemService(InputMethodManager.class).restartInput(this);
|
||||
}
|
||||
private void schedule() { if(!resetting&&accepting){main.removeCallbacks(synchronize);main.post(synchronize);} }
|
||||
private boolean composing() { return BaseInputConnection.getComposingSpanStart(getText())>=0; }
|
||||
private static List<String> characters(String text) {
|
||||
BreakIterator iterator=BreakIterator.getCharacterInstance();iterator.setText(text);
|
||||
List<String> result=new ArrayList<>();int start=iterator.first();
|
||||
for(int end=iterator.next();end!=BreakIterator.DONE;start=end,end=iterator.next())result.add(text.substring(start,end));
|
||||
return result;
|
||||
}
|
||||
private boolean synchronize() {
|
||||
if(!accepting||resetting||batches>0||composing())return false;
|
||||
String current=getText().toString();if(current.equals(sent))return true;
|
||||
List<String> old=characters(sent),next=characters(current);int prefix=0,offset=0;
|
||||
while(prefix<old.size()&&prefix<next.size()&&old.get(prefix).equals(next.get(prefix))){offset+=next.get(prefix).length();prefix++;}
|
||||
if(!sender.edit(old.size()-prefix,current.substring(offset))){resetContext();return false;}
|
||||
sent=current;
|
||||
// Bound the correction context without changing anything on the remote desktop.
|
||||
if(current.length()>2048||current.indexOf('\n')>=0)resetContext();
|
||||
return true;
|
||||
}
|
||||
private void enter() {
|
||||
if(!accepting)return;
|
||||
BaseInputConnection.removeComposingSpans(getText());
|
||||
enterPending=true;schedule();
|
||||
}
|
||||
@Override public boolean onKeyDown(int code,KeyEvent event) {
|
||||
if(accepting&&code==KeyEvent.KEYCODE_ENTER){enter();return true;}
|
||||
if(accepting&&code==KeyEvent.KEYCODE_DEL&&length()==0){sender.key(8);return true;}
|
||||
return super.onKeyDown(code,event);
|
||||
}
|
||||
@Override public boolean onKeyUp(int code,KeyEvent event) {
|
||||
if(accepting&&(code==KeyEvent.KEYCODE_ENTER||code==KeyEvent.KEYCODE_DEL))return true;
|
||||
return super.onKeyUp(code,event);
|
||||
}
|
||||
@Override public InputConnection onCreateInputConnection(EditorInfo info) {
|
||||
InputConnection base=super.onCreateInputConnection(info);if(base==null)return null;
|
||||
int revision=++generation;batches=0;
|
||||
return new InputConnectionWrapper(base,false) {
|
||||
private boolean active(){return accepting&&generation==revision;}
|
||||
@Override public boolean beginBatchEdit(){if(!active())return false;batches++;return super.beginBatchEdit();}
|
||||
@Override public boolean endBatchEdit(){if(!active())return false;boolean result=super.endBatchEdit();batches=Math.max(0,batches-1);schedule();return result;}
|
||||
@Override public boolean setComposingText(CharSequence text,int cursor){if(!active())return false;boolean result=super.setComposingText(text,cursor);schedule();return result;}
|
||||
@Override public boolean setComposingRegion(int start,int end){return active()&&super.setComposingRegion(start,end);}
|
||||
@Override public boolean commitText(CharSequence text,int cursor){if(!active())return false;boolean result=super.commitText(text,cursor);schedule();return result;}
|
||||
@Override public boolean finishComposingText(){if(!active())return false;boolean result=super.finishComposingText();schedule();return result;}
|
||||
@Override public boolean deleteSurroundingText(int before,int after){
|
||||
if(!active())return false;
|
||||
if(length()==0&&before>0&&after==0){for(int i=0;i<Math.min(before,2048);i++)sender.key(8);return true;}
|
||||
boolean result=super.deleteSurroundingText(before,after);schedule();return result;
|
||||
}
|
||||
@Override public boolean deleteSurroundingTextInCodePoints(int before,int after){
|
||||
if(!active())return false;
|
||||
if(length()==0&&before>0&&after==0){for(int i=0;i<Math.min(before,2048);i++)sender.key(8);return true;}
|
||||
boolean result=super.deleteSurroundingTextInCodePoints(before,after);schedule();return result;
|
||||
}
|
||||
@Override public boolean performEditorAction(int action){if(!active())return false;enter();return true;}
|
||||
@Override public boolean sendKeyEvent(KeyEvent event){return active()&&super.sendKeyEvent(event);}
|
||||
@Override public boolean setSelection(int start,int end){return active()&&super.setSelection(start,end);}
|
||||
@Override public void closeConnection(){if(active())super.closeConnection();}
|
||||
@android.annotation.SuppressLint("NewApi") // Framework calls these overloads only on API 33+.
|
||||
@Override public boolean commitText(CharSequence text,int cursor,android.view.inputmethod.TextAttribute attributes){
|
||||
if(!active())return false;boolean result=super.commitText(text,cursor,attributes);schedule();return result;
|
||||
}
|
||||
@android.annotation.SuppressLint("NewApi") // Framework calls these overloads only on API 33+.
|
||||
@Override public boolean setComposingText(CharSequence text,int cursor,android.view.inputmethod.TextAttribute attributes){
|
||||
if(!active())return false;boolean result=super.setComposingText(text,cursor,attributes);schedule();return result;
|
||||
}
|
||||
@android.annotation.SuppressLint("NewApi") // Framework calls these overloads only on API 33+.
|
||||
@Override public boolean setComposingRegion(int start,int end,android.view.inputmethod.TextAttribute attributes){return active()&&super.setComposingRegion(start,end,attributes);}
|
||||
@android.annotation.SuppressLint("NewApi") // Framework calls this overload only on API 34+.
|
||||
@Override public boolean replaceText(int start,int end,CharSequence text,int cursor,android.view.inputmethod.TextAttribute attributes){
|
||||
if(!active())return false;boolean result=super.replaceText(start,end,text,cursor,attributes);schedule();return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package cn.crossdesk.mobile;
|
||||
/** Shared button ownership and cursor position for the video surface and mouse bar. */
|
||||
final class RemoteMouseInput {
|
||||
interface Sender { void send(float x, float y, int flag, int wheel); }
|
||||
interface RelativeMotion { void move(float dx, float dy); }
|
||||
RelativeMotion relativeMotion;
|
||||
private final Sender sender;
|
||||
private float x = .5f, y = .5f;
|
||||
private boolean gestureLeft, virtualLeft, virtualRight;
|
||||
@@ -19,7 +21,8 @@ final class RemoteMouseInput {
|
||||
}
|
||||
void drag(float dx, float dy) {
|
||||
if (!hasVirtualButton() || viewportWidth <= 0 || viewportHeight <= 0) return;
|
||||
move(x + dx / viewportWidth, y + dy / viewportHeight);
|
||||
if (relativeMotion != null) relativeMotion.move(dx, dy);
|
||||
else move(x + dx / viewportWidth, y + dy / viewportHeight);
|
||||
}
|
||||
void gestureLeft(boolean down) {
|
||||
if (down && hasVirtualButton()) return;
|
||||
|
||||
@@ -12,31 +12,32 @@ final class RemoteNetworkStatistics {
|
||||
}
|
||||
static final class Report {
|
||||
final Traffic[] traffic=new Traffic[4];final int mode;final boolean srtp;final double rtt;
|
||||
final String decoder;
|
||||
Report(JSONObject json){
|
||||
String[] keys={"video","audio","data","total"};
|
||||
for(int i=0;i<keys.length;i++){JSONObject item=json.optJSONObject(keys[i]);traffic[i]=item==null?null:new Traffic(item);}
|
||||
mode=json.optInt("mode",2);srtp=json.optBoolean("srtp");rtt=json.optDouble("rtt",Double.NaN);
|
||||
decoder=json.optString("decoder","");
|
||||
}
|
||||
}
|
||||
static final class Snapshot {
|
||||
final Report report;final int fps,width,height;final double latency,rtt;
|
||||
Snapshot(Report report,int fps,int width,int height,double latency,double rtt){this.report=report;this.fps=fps;this.width=width;this.height=height;this.latency=latency;this.rtt=rtt;}
|
||||
String mode(){return report==null?"—":report.mode==0?"P2P 直连":report.mode==1?"TURN 中继":"—";}
|
||||
final String remoteEncoder,localDecoder;
|
||||
Snapshot(Report report,int fps,int width,int height,double latency,double rtt,String remoteEncoder,String localDecoder){this.report=report;this.fps=fps;this.width=width;this.height=height;this.latency=latency;this.rtt=rtt;this.remoteEncoder=remoteEncoder;this.localDecoder=localDecoder;}
|
||||
String mode(){return report==null?"—":report.mode==0?L10n.tr("P2P 直连"):report.mode==1?L10n.tr("TURN 中继"):"—";}
|
||||
String resolution(){return width>0&&height>0?width+" × "+height:"—";}
|
||||
String summary(){
|
||||
String value=mode()+(width>0&&height>0?" · "+width+"×"+height:"")+" · "+bitrate(report==null||report.traffic[3]==null?-1:report.traffic[3].inbound);
|
||||
if(report!=null&&report.traffic[0]!=null&&report.traffic[0].loss>0)value+=" · 丢包 "+loss(report.traffic[0].loss);return value;
|
||||
}
|
||||
}
|
||||
private static final class Frame {
|
||||
final double time,latency;Frame(double time,double latency){this.time=time;this.latency=latency;}
|
||||
}
|
||||
private Report report;
|
||||
private String remoteEncoder="";
|
||||
private double reportTime,rttTime,averageRtt=Double.NaN;
|
||||
private long lastFrameId;
|
||||
private int width,height;
|
||||
private final ArrayDeque<Frame> frames=new ArrayDeque<>();
|
||||
static double now(){return System.nanoTime()/1_000_000_000.0;}
|
||||
synchronized void receiveRemoteEncoder(String encoder){remoteEncoder=encoder;}
|
||||
synchronized void receive(Report value,double now){
|
||||
report=value;reportTime=now;
|
||||
if(!Double.isFinite(value.rtt)||value.rtt<0||value.rtt>2000)return;
|
||||
@@ -53,7 +54,7 @@ final class RemoteNetworkStatistics {
|
||||
synchronized Snapshot snapshot(double now){
|
||||
prune(now);double total=0;int count=0;
|
||||
for(Frame frame:frames)if(Double.isFinite(frame.latency)){total+=frame.latency;count++;}
|
||||
return new Snapshot(now-reportTime<3?report:null,frames.size(),width,height,count==0?Double.NaN:total/count,now-rttTime<3?averageRtt:Double.NaN);
|
||||
return new Snapshot(now-reportTime<3?report:null,frames.size(),width,height,count==0?Double.NaN:total/count,now-rttTime<3?averageRtt:Double.NaN,remoteEncoder,report==null?"":report.decoder);
|
||||
}
|
||||
private void prune(double now){while(!frames.isEmpty()&&now-frames.peekFirst().time>=1)frames.removeFirst();}
|
||||
static String bitrate(long value){return value<0?"—":value>=1_000_000?String.format(Locale.ROOT,"%.1f Mbps",value/1_000_000.0):value>=1000?String.format(Locale.ROOT,"%.0f Kbps",value/1000.0):value+" bps";}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
|
||||
/** Applies the selected orientation policy for the whole remote session. */
|
||||
final class RemoteOrientation {
|
||||
private final Activity activity;
|
||||
private boolean autoLandscape;
|
||||
|
||||
RemoteOrientation(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
void begin(boolean autoLandscape, boolean fullScreenPhone) {
|
||||
this.autoLandscape = autoLandscape;
|
||||
update(fullScreenPhone);
|
||||
}
|
||||
|
||||
boolean autoLandscape() { return autoLandscape; }
|
||||
|
||||
void end() { autoLandscape = false; }
|
||||
|
||||
void update(boolean fullScreenPhone) {
|
||||
int orientation = autoLandscape && fullScreenPhone
|
||||
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
: ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
|
||||
if (activity.getRequestedOrientation() != orientation) activity.setRequestedOrientation(orientation);
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ final class RemotePreviewStore {
|
||||
new AtomicFile(new File(directory,key+".jpg")).delete();
|
||||
synchronized(this){
|
||||
boolean saved=index.edit().putStringSet("files",new HashSet<>(files)).commit();
|
||||
if(!saved||new File(directory,key+".jpg").exists())cleanupError="部分预览图未能清除,请重试。";
|
||||
if(!saved||new File(directory,key+".jpg").exists())cleanupError=L10n.tr("部分预览图未能清除,请重试。");
|
||||
}
|
||||
changed();
|
||||
});changed();
|
||||
@@ -139,7 +139,7 @@ final class RemotePreviewStore {
|
||||
try{deleteTree(directory);}catch(IOException error){success=false;}
|
||||
synchronized(this){
|
||||
if(epoch!=generation)return;
|
||||
cleaning=false;cleanupError=success?"":"部分预览图未能清除,请重试。";
|
||||
cleaning=false;cleanupError=success?"":L10n.tr("部分预览图未能清除,请重试。");
|
||||
}
|
||||
changed();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Host-confirmed privacy state; all access belongs to the main thread. */
|
||||
final class RemotePrivacyScreen {
|
||||
boolean desktopAvailable = true;
|
||||
boolean received, supported, inputBlockSupported, overlay, inputBlocked, remotePaused, pending;
|
||||
boolean unlockNotice;
|
||||
long localUnlockRevision;
|
||||
int state;
|
||||
long revision, requestRevision, requestId, receivedAt, requestedAt;
|
||||
String reason = "", failure = "";
|
||||
|
||||
void setInteractiveStage(String stage) {
|
||||
desktopAvailable = !"lock-screen".equals(stage) &&
|
||||
!"credential-ui".equals(stage) && !"secure-desktop".equals(stage);
|
||||
}
|
||||
|
||||
void receive(JSONObject message, long now) {
|
||||
try {
|
||||
if (message.getInt("type") != 10) return;
|
||||
JSONObject value = message.getJSONObject("privacy_status");
|
||||
int next = value.getInt("state");
|
||||
long rev = value.getLong("revision");
|
||||
if (next < 0 || next > 5 || rev < 0 || rev > 0xffffffffL ||
|
||||
received && (int)(rev - revision) < 0) return;
|
||||
boolean support = value.getBoolean("supported"), cover = value.getBoolean("overlay_active");
|
||||
boolean paused = value.getBoolean("remote_paused");
|
||||
if (next == 3 && (!support || !cover || paused)) return;
|
||||
boolean blockSupport = value.getBoolean("input_block_supported"), blocked = value.getBoolean("input_blocked");
|
||||
String detail = value.getString("reason");
|
||||
long unlock = value.has("local_unlock_revision") ? value.getLong("local_unlock_revision") : 0;
|
||||
if (unlock < 0 || unlock > 0xffffffffL) return;
|
||||
if (received && (int)(unlock - localUnlockRevision) > 0) unlockNotice = true;
|
||||
if (next == 3) unlockNotice = false;
|
||||
localUnlockRevision = unlock;
|
||||
state = next; revision = rev; supported = support; overlay = cover; remotePaused = paused;
|
||||
inputBlockSupported = blockSupport; inputBlocked = blocked; reason = detail;
|
||||
received = true; receivedAt = now; failure = "";
|
||||
if (pending && (int)(revision - requestRevision) > 0 && state != 2 && state != 4) pending = false;
|
||||
} catch (org.json.JSONException ignored) { }
|
||||
}
|
||||
boolean active() { return overlay || remotePaused || state == 2 || state == 3 || state == 4; }
|
||||
boolean fresh(long now) { return received && now - receivedAt <= 5000; }
|
||||
boolean enabled(long now) { return fresh(now) && state == 3 && overlay && !remotePaused; }
|
||||
String buttonTitle(long now) {
|
||||
if (enabled(now)) return L10n.tr("隐私屏已开启");
|
||||
if (fresh(now) && supported && state == 1 && !active()) return L10n.tr("隐私屏已关闭");
|
||||
return L10n.tr("隐私屏");
|
||||
}
|
||||
String feedback(long now) {
|
||||
if (!desktopAvailable) return "";
|
||||
if (failure.isEmpty() && (pending || fresh(now) && (state == 2 || state == 4))) return "";
|
||||
if (failure.isEmpty() && !pending && fresh(now) && supported &&
|
||||
(enabled(now) || state == 1 && !active())) return "";
|
||||
String detail = detail(now);
|
||||
return status(now) + (detail.isEmpty() ? "" : ":" + detail);
|
||||
}
|
||||
boolean canToggle(long now) {
|
||||
return desktopAvailable && !pending && fresh(now) && state != 4 && (active() || supported && state == 1);
|
||||
}
|
||||
boolean canLock(long now) {
|
||||
return desktopAvailable && !pending && fresh(now) && supported;
|
||||
}
|
||||
long begin(long now) { return begin(now, false); }
|
||||
long begin(long now, boolean lock) {
|
||||
tick(now);
|
||||
if (!(lock ? canLock(now) : canToggle(now))) return -1;
|
||||
pending = true; requestedAt = now; requestRevision = revision; failure = "";
|
||||
return ++requestId;
|
||||
}
|
||||
void failed(long id) {
|
||||
if (pending && requestId == id) { pending = false; failure = L10n.tr("指令发送失败,请重试"); }
|
||||
}
|
||||
void tick(long now) {
|
||||
if (pending && now - requestedAt >= 15000) {
|
||||
pending = false; failure = L10n.tr("请求超时,尚未确认电脑的隐私屏状态");
|
||||
}
|
||||
}
|
||||
String status(long now) {
|
||||
if (!failure.isEmpty()) return failure;
|
||||
if (!received) return L10n.tr("等待电脑返回隐私屏状态;旧版电脑端可能不支持");
|
||||
if (!fresh(now)) return L10n.tr("隐私屏状态已过期,等待电脑确认");
|
||||
if (state == 2) return L10n.tr("正在开启隐私屏…");
|
||||
if (state == 4) return L10n.tr("正在关闭隐私屏…");
|
||||
if (pending) return L10n.tr("指令已发送,等待电脑确认…");
|
||||
if (state == 5) return L10n.tr("隐私屏异常");
|
||||
if (!supported || state == 0) return L10n.tr("隐私屏暂不可用");
|
||||
if (state == 3 && overlay && !remotePaused)
|
||||
return L10n.tr("隐私屏已开启");
|
||||
return state == 1 && !active() ? L10n.tr("隐私屏已关闭") : L10n.tr("隐私屏状态待确认");
|
||||
}
|
||||
String detail(long now) {
|
||||
return fresh(now) && (state == 0 || state == 5 || !supported) ? reason : "";
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public final class RemoteSessionService extends Service {
|
||||
@Override public void onCreate() {
|
||||
super.onCreate(); running = this;
|
||||
getSystemService(NotificationManager.class).createNotificationChannel(
|
||||
new NotificationChannel(CHANNEL, "远程连接", NotificationManager.IMPORTANCE_LOW));
|
||||
new NotificationChannel(CHANNEL, L10n.tr("远程连接"), NotificationManager.IMPORTANCE_LOW));
|
||||
wakeLock = getSystemService(PowerManager.class).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CrossDesk:RemoteSession");
|
||||
wakeLock.setReferenceCounted(false);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public final class RemoteSessionService extends Service {
|
||||
if (value == null || value.ended || value.session.isClosed()) { stopSelf(startId); return START_NOT_STICKY; }
|
||||
if (intent == null || !value.token.equals(intent.getStringExtra("session"))) return START_NOT_STICKY;
|
||||
owned = value;
|
||||
if (STOP.equals(intent.getAction())) { value.finish("已从通知断开远程连接"); return START_NOT_STICKY; }
|
||||
if (STOP.equals(intent.getAction())) { value.finish(L10n.tr("已从通知断开远程连接")); return START_NOT_STICKY; }
|
||||
try {
|
||||
Notification notification = notification();
|
||||
if (Build.VERSION.SDK_INT >= 34) startForeground(NOTIFICATION, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
@@ -68,7 +68,7 @@ public final class RemoteSessionService extends Service {
|
||||
update();
|
||||
} catch (RuntimeException error) {
|
||||
Log.w("CrossDesk", "Background session foreground promotion failed", error);
|
||||
value.finish("后台保活未能启动,请保持应用在前台后重新连接");
|
||||
value.finish(L10n.tr("后台保活未能启动,请保持应用在前台后重新连接"));
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@@ -80,12 +80,12 @@ public final class RemoteSessionService extends Service {
|
||||
.setData(android.net.Uri.parse("crossdesk-session:" + owned.token)).putExtra("session", owned.token);
|
||||
PendingIntent stop = PendingIntent.getService(this, 1, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new Notification.Builder(this, CHANNEL).setSmallIcon(R.drawable.ic_remote_session)
|
||||
.setContentTitle("CrossDesk 远程连接")
|
||||
.setContentText(owned.background ? "后台保持连接,点按返回远程桌面" : "已启用后台保活,切换应用可继续保持连接")
|
||||
.setContentTitle(L10n.tr("CrossDesk 远程连接"))
|
||||
.setContentText(owned.background ? L10n.tr("后台保持连接,点按返回远程桌面") : L10n.tr("已启用后台保活,切换应用可继续保持连接"))
|
||||
.setContentIntent(open).setOngoing(true).setOnlyAlertOnce(true).setShowWhen(false)
|
||||
.setCategory(Notification.CATEGORY_SERVICE).setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.addAction(new Notification.Action.Builder(null, "返回", open).build())
|
||||
.addAction(new Notification.Action.Builder(null, "断开", stop).build()).build();
|
||||
.addAction(new Notification.Action.Builder(null, L10n.tr("返回"), open).build())
|
||||
.addAction(new Notification.Action.Builder(null, L10n.tr("断开"), stop).build()).build();
|
||||
}
|
||||
@android.annotation.SuppressLint("WakelockTimeout") // Released on foreground return, disconnect, task removal and service destruction.
|
||||
private void update() {
|
||||
@@ -96,14 +96,14 @@ public final class RemoteSessionService extends Service {
|
||||
}
|
||||
private void releaseWakeLock() { if (wakeLock != null && wakeLock.isHeld()) wakeLock.release(); }
|
||||
@Override public void onTaskRemoved(Intent rootIntent) {
|
||||
if (current == owned && owned != null) owned.finish("远程任务已关闭");
|
||||
if (current == owned && owned != null) owned.finish(L10n.tr("远程任务已关闭"));
|
||||
stopSelf();
|
||||
}
|
||||
@Override public void onDestroy() {
|
||||
if (running == this) running = null;
|
||||
releaseWakeLock(); stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
BackgroundSession value = owned;
|
||||
if (current == value && value != null) { current = null; value.finish("后台连接服务已停止,请重新连接"); }
|
||||
if (current == value && value != null) { current = null; value.finish(L10n.tr("后台连接服务已停止,请重新连接")); }
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import org.json.JSONObject;
|
||||
|
||||
/** Same request/acknowledgement and rollback rules as the iOS controller. */
|
||||
final class RemoteVideoSettings {
|
||||
static final String[] PREFERENCES={"帧率优先","画质优先","平衡"};
|
||||
static final String[] DETAILS={
|
||||
"优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。",
|
||||
"始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。",
|
||||
"兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。"};
|
||||
static String[] preferences(){return new String[]{L10n.tr("帧率优先"),L10n.tr("画质优先"),L10n.tr("平衡")};}
|
||||
static String[] details(){return new String[]{
|
||||
L10n.tr("优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。"),
|
||||
L10n.tr("始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。"),
|
||||
L10n.tr("兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。")};}
|
||||
static final class Values {
|
||||
final int quality,frameRate,preference;
|
||||
Values(int quality,int frameRate,int preference){this.quality=quality;this.frameRate=frameRate;this.preference=preference;}
|
||||
@@ -54,5 +54,5 @@ final class RemoteVideoSettings {
|
||||
}catch(org.json.JSONException ignored){ }
|
||||
}
|
||||
void expire(long expiredId){if(pending&&expiredId==requestId){selection=applied;pending=false;failed=true;}}
|
||||
String feedback(){return !supported?"连接建立后可调整,需被控端支持。":failed?"调整失败,请重试。":"";}
|
||||
String feedback(){return !supported?L10n.tr("连接建立后可调整,需被控端支持。"):failed?L10n.tr("调整失败,请重试。"):"";}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
|
||||
private final Cursor cursor;
|
||||
private final NativeSession session;
|
||||
final RemoteMouseInput mouseInput;
|
||||
Runnable remoteFocusChanged;
|
||||
private final GestureDetector gestures;
|
||||
private final ScaleGestureDetector scales;
|
||||
private final RemoteViewport viewport = new RemoteViewport();
|
||||
@@ -50,6 +51,7 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
|
||||
RemoteVideoView(Context context, NativeSession session) {
|
||||
super(context); this.session = session;
|
||||
mouseInput = new RemoteMouseInput(this::sendMouse);
|
||||
mouseInput.relativeMotion = this::moveRelative;
|
||||
setBackgroundColor(Color.BLACK);
|
||||
video = new SurfaceView(context); video.getHolder().addCallback(this);
|
||||
addView(video, new LayoutParams(1, 1, android.view.Gravity.CENTER));
|
||||
@@ -76,7 +78,8 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
|
||||
scales = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@Override public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
if (mouseInput.hasVirtualButton()) return false;
|
||||
if (!viewport.contains(detector.getFocusX(), detector.getFocusY())) return false;
|
||||
// Pinches can start anywhere on the touch surface, including letterboxing.
|
||||
if (viewport.width() <= 0 || viewport.height() <= 0) return false;
|
||||
viewportGesture = true;
|
||||
viewportPanActive = true;
|
||||
return true;
|
||||
@@ -101,7 +104,7 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setContentDescription("远程桌面:轻触点击,拖动移动,长按右键,双指捏合缩放,放大后双指拖动画面,原始比例下双指上下滑动滚动");
|
||||
setContentDescription(L10n.tr("远程桌面:轻触点击,拖动移动,长按右键,双指捏合缩放,放大后双指拖动画面,原始比例下双指上下滑动滚动"));
|
||||
}
|
||||
void videoSize(int width, int height) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
@@ -141,18 +144,26 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
|
||||
}
|
||||
private void sendMouse(float x, float y, int flag, int wheel) {
|
||||
this.x = x; this.y = y;
|
||||
if((flag==1||flag==3||flag==5)&&remoteFocusChanged!=null)remoteFocusChanged.run();
|
||||
session.pointer(x,y,flag,wheel);
|
||||
cursor.invalidate();
|
||||
}
|
||||
@Override protected void onSizeChanged(int w,int h,int oldw,int oldh) { post(this::fit); }
|
||||
private void moveRelative(float dx, float dy) {
|
||||
if (pictureInPicture || viewport.width() <= 0 || viewport.height() <= 0) return;
|
||||
viewport.panForPointer(y, dy);
|
||||
applyViewport();
|
||||
mouseInput.move(x + dx / viewport.width(), y + dy / viewport.height());
|
||||
}
|
||||
private boolean point(MotionEvent e) {
|
||||
boolean touchpad = relative && e.getToolType(0) != MotionEvent.TOOL_TYPE_MOUSE;
|
||||
float px=e.getX(),py=e.getY();
|
||||
if (!touchpad && !viewport.contains(px,py)) return false;
|
||||
if(touchpad){
|
||||
if(e.getActionMasked()==MotionEvent.ACTION_MOVE){
|
||||
x=Math.max(0,Math.min(1,x+(px-lastX)/Math.max(1,viewport.width())));
|
||||
y=Math.max(0,Math.min(1,y+(py-lastY)/Math.max(1,viewport.height())));
|
||||
moveRelative(px-lastX,py-lastY);
|
||||
lastX=px;lastY=py;
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
x=viewport.normalizedX(px);
|
||||
@@ -233,7 +244,7 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
|
||||
private final Path arrow = new Path();
|
||||
private final float arrowWidth = 12, arrowHeight = 15;
|
||||
Cursor(Context context) {
|
||||
super(context); setClickable(true); setContentDescription("远程桌面触控区域");
|
||||
super(context); setClickable(true); setContentDescription(L10n.tr("远程桌面触控区域"));
|
||||
// Match the iOS RemoteCursorArrowShape in density-independent units.
|
||||
arrow.moveTo(arrowWidth*.08f,arrowHeight*.04f);
|
||||
arrow.lineTo(arrowWidth*.08f,arrowHeight*.78f);
|
||||
|
||||
@@ -3,11 +3,14 @@ package cn.crossdesk.mobile;
|
||||
/** Geometry shared by the video Surface, cursor and remote pointer input. */
|
||||
final class RemoteViewport {
|
||||
private static final float MAXIMUM_SCALE = 10;
|
||||
private int baseWidth, baseHeight, baseLeft, baseTop;
|
||||
private static final float MINIMUM_VISIBLE_HEIGHT_FRACTION = .2f;
|
||||
private int baseWidth, baseHeight, baseLeft, baseTop, containerHeight;
|
||||
private boolean edgePanned;
|
||||
private float scale = 1, offsetX, offsetY;
|
||||
|
||||
void fit(int width, int height, int videoWidth, int videoHeight) {
|
||||
if (width <= 0 || height <= 0 || videoWidth <= 0 || videoHeight <= 0) return;
|
||||
containerHeight = height;
|
||||
double fitScale = Math.min((double) width / videoWidth, (double) height / videoHeight);
|
||||
baseWidth = Math.max(1, (int) (videoWidth * fitScale));
|
||||
baseHeight = Math.max(1, (int) (videoHeight * fitScale));
|
||||
@@ -54,18 +57,46 @@ final class RemoteViewport {
|
||||
constrainOffset();
|
||||
}
|
||||
|
||||
void finishGesture() {
|
||||
if (!isZoomed()) reset();
|
||||
void panForPointer(float y, float dy) {
|
||||
if (baseHeight == 0 || !Float.isFinite(dy)) return;
|
||||
float overflowY = pointerOverflow(top() + y * height(), dy,
|
||||
Math.max(0, top()), Math.min(containerHeight, top() + height()));
|
||||
if (overflowY == 0) return;
|
||||
edgePanned = true;
|
||||
panBy(0, -overflowY);
|
||||
}
|
||||
|
||||
void reset() { scale = 1; offsetX = offsetY = 0; }
|
||||
// Only the outward portion of this movement pans; an offscreen cursor must
|
||||
// not make a tiny movement jump the viewport by its entire hidden distance.
|
||||
private static float pointerOverflow(float position, float delta, float low, float high) {
|
||||
if (delta > 0) return Math.min(delta, Math.max(0, position + delta - high));
|
||||
if (delta < 0) return Math.max(delta, Math.min(0, position + delta - low));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void finishGesture() {
|
||||
if (!isZoomed()) {
|
||||
scale = 1;
|
||||
constrainOffset();
|
||||
}
|
||||
}
|
||||
|
||||
void reset() { scale = 1; offsetX = offsetY = 0; edgePanned = false; }
|
||||
|
||||
private void constrainOffset() {
|
||||
// Keep the original fitted area covered, as on iOS, even with letterboxing.
|
||||
float maximumX = baseWidth * (scale - 1) / 2;
|
||||
float maximumY = baseHeight * (scale - 1) / 2;
|
||||
float minimumY = -baseHeight * (scale - 1) / 2;
|
||||
float maximumY = -minimumY;
|
||||
if (edgePanned) {
|
||||
// Reserve 20% of the viewport height regardless of zoom. If the
|
||||
// whole image is shorter than that, keep its full height visible.
|
||||
float visibleHeight = Math.min(height(), containerHeight * MINIMUM_VISIBLE_HEIGHT_FRACTION);
|
||||
float centerY = baseTop + baseHeight / 2f;
|
||||
minimumY = visibleHeight - centerY - height() / 2;
|
||||
maximumY = containerHeight - visibleHeight - centerY + height() / 2;
|
||||
}
|
||||
offsetX = clamp(offsetX, -maximumX, maximumX);
|
||||
offsetY = clamp(offsetY, -maximumY, maximumY);
|
||||
offsetY = clamp(offsetY, minimumY, maximumY);
|
||||
}
|
||||
|
||||
private static float clamp(float value, float minimum, float maximum) {
|
||||
|
||||
@@ -31,7 +31,7 @@ final class SegmentedControl extends LinearLayout {
|
||||
setGravity(Gravity.CENTER_VERTICAL);setPadding(inset,inset,inset,inset);setBackground(ui.background(0xFFE9E9EF,MobileUi.SEGMENT_HEIGHT/2));paint.setColor(Color.WHITE);
|
||||
for(int i=0;i<titles.length;i++){
|
||||
final int index=i;TextView choice=ui.action(titles[i],MobileUi.INK,()->select(index));
|
||||
choice.setTextSize(13);choice.setMinHeight(height);choice.setSelected(i==selected);choice.setEnabled(enabled);choice.setAlpha(enabled?1:.45f);
|
||||
choice.setTextSize(13);choice.setSingleLine();choice.setAutoSizeTextTypeUniformWithConfiguration(9,13,1,android.util.TypedValue.COMPLEX_UNIT_SP);choice.setMinHeight(height);choice.setSelected(i==selected);choice.setEnabled(enabled);choice.setAlpha(enabled?1:.45f);
|
||||
addView(choice,new LayoutParams(0,height,1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,29 +6,41 @@ import android.view.*;
|
||||
import android.widget.*;
|
||||
import static cn.crossdesk.mobile.MobileUi.*;
|
||||
|
||||
/** Safe-area overlay: a draggable 54 dp orb, an adaptive menu and a floating keyboard. */
|
||||
/** Safe-area overlay: a draggable 54 dp orb, an adaptive menu and a docked keyboard. */
|
||||
@android.annotation.SuppressLint("ViewConstructor")
|
||||
final class SessionControls extends FrameLayout {
|
||||
interface Actions {
|
||||
void disconnect(); void display(); void clipboard(); void key(int code);
|
||||
void chord(int code,int[] modifiers); void shortcut(int code); void text(String value); void mute(); void mouse();
|
||||
default boolean textEdit(int deletions,String value){return false;}
|
||||
void videoSettings(int field,int value);
|
||||
void sendFile(); void saveFile(); void secureAttention();
|
||||
default void togglePrivacy() { }
|
||||
}
|
||||
private final MobileUi ui;
|
||||
private final Actions actions;
|
||||
private final View orb;
|
||||
private final LinearLayout waiting;
|
||||
private View shield;
|
||||
private LinearLayout menu,keyboard;
|
||||
private LinearLayout menu;
|
||||
private final RemoteKeyboard keyboard;
|
||||
private VirtualMouseBar mouseBar;
|
||||
private FrameLayout mouseHost;
|
||||
private boolean mouseVisible,pictureInPicture;
|
||||
private ScrollView keyboardKeys;
|
||||
private ScrollView menuScroll;
|
||||
private int keyboardPreferredHeight;
|
||||
private int menuColumns=3;
|
||||
private TextView networkLabel,videoFeedback;
|
||||
private final ViewTreeObserver.OnGlobalLayoutListener legacyKeyboardLayout=this::legacyKeyboardLayout;
|
||||
private void legacyKeyboardLayout(){
|
||||
if(android.os.Build.VERSION.SDK_INT>=30)return;
|
||||
android.graphics.Rect visible=new android.graphics.Rect();getWindowVisibleDisplayFrame(visible);
|
||||
int height=Math.max(0,getRootView().getHeight()-visible.bottom);
|
||||
keyboardInsets(height,height>ui.dp(80),0);
|
||||
}
|
||||
private TextView videoFeedback;
|
||||
private RemotePrivacyScreen privacy=new RemotePrivacyScreen();
|
||||
private View privacyButton;
|
||||
private MobileUi.Icon privacyIcon;
|
||||
private TextView privacyFeedback,privacyCaption;
|
||||
private TextView transferLabel,saveFileButton;
|
||||
private String transferStatus="";
|
||||
private boolean hasReceivedFile;
|
||||
@@ -40,20 +52,27 @@ final class SessionControls extends FrameLayout {
|
||||
private LinearLayout[] trafficRows;
|
||||
private TextView[] networkDetails;
|
||||
private View encryptionLock,encryptionOpen;
|
||||
private boolean muted,relative=true,shift,computerKeyboard,orbPositioned;
|
||||
private final java.util.Set<Integer> modifiers=new java.util.LinkedHashSet<>();
|
||||
private boolean muted,relative=true,orbPositioned,secureAttentionVisible;
|
||||
SessionControls(Context context,MobileUi ui,Actions actions){
|
||||
super(context);this.ui=ui;this.actions=actions;
|
||||
keyboard=new RemoteKeyboard(context,ui,actions,()->{requestLayout();positionMouse();});
|
||||
addView(keyboard,new LayoutParams(-1,ui.dp(44),Gravity.BOTTOM));
|
||||
// This container must remain transparent to remote touch outside its controls.
|
||||
waiting=ui.column();waiting.setMinimumWidth(ui.dp(200));waiting.setGravity(Gravity.CENTER);waiting.setPadding(ui.dp(20),ui.dp(20),ui.dp(20),ui.dp(20));waiting.setBackground(ui.background(0xB0202024,14));
|
||||
ProgressBar progress=new ProgressBar(context);progress.setIndeterminateTintList(android.content.res.ColorStateList.valueOf(Color.WHITE));waiting.addView(progress,ui.size(28,28));ui.gap(waiting,12);TextView message=ui.text("等待远端画面…",14,false);message.setTextColor(Color.WHITE);message.setGravity(Gravity.CENTER);waiting.addView(message);addView(waiting,new LayoutParams(-2,-2,Gravity.CENTER));
|
||||
orb=ui.iconButton("display","展开远程控制菜单",()->{if(menu==null)openMenu("controls");else closeMenu();});
|
||||
ProgressBar progress=new ProgressBar(context);progress.setIndeterminateTintList(android.content.res.ColorStateList.valueOf(Color.WHITE));waiting.addView(progress,ui.size(28,28));ui.gap(waiting,12);TextView message=ui.text(L10n.tr("等待远端画面…"),14,false);message.setTextColor(Color.WHITE);message.setGravity(Gravity.CENTER);waiting.addView(message);addView(waiting,new LayoutParams(-2,-2,Gravity.CENTER));
|
||||
orb=ui.iconButton("display",L10n.tr("展开远程控制菜单"),()->{if(menu==null)openMenu("controls");else closeMenu();});
|
||||
ImageView brand=new ImageView(context);brand.setImageResource(R.drawable.mobile_brand);brand.setScaleType(ImageView.ScaleType.CENTER_CROP);brand.setBackground(ui.background(Color.WHITE,23));brand.setClipToOutline(true);brand.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
((FrameLayout)orb).removeAllViews();((FrameLayout)orb).addView(brand,new LayoutParams(ui.dp(46),ui.dp(46),Gravity.CENTER));
|
||||
orb.setBackground(ui.background(0xFFF5F5F7,27));orb.setElevation(ui.dp(7));addView(orb,new LayoutParams(ui.dp(54),ui.dp(54)));
|
||||
drag(orb,orb,()->{if(menu==null)openMenu("controls");else closeMenu();});
|
||||
}
|
||||
void setRelative(boolean value){relative=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");}
|
||||
void setHostPlatform(String platform){
|
||||
boolean visible="windows".equals(platform);
|
||||
if(secureAttentionVisible==visible)return;
|
||||
secureAttentionVisible=visible;
|
||||
if(menu!=null&&menuPage.equals("controls"))openMenu("controls");
|
||||
}
|
||||
void attachMouseBar(FrameLayout host, VirtualMouseBar bar) {
|
||||
mouseHost=host;mouseBar=bar;
|
||||
// A direct, tightly bounded sibling of the video is essential: otherwise
|
||||
@@ -65,7 +84,7 @@ final class SessionControls extends FrameLayout {
|
||||
void cancelMouseInput(){if(mouseBar!=null)mouseBar.cancelInput();}
|
||||
void setPictureInPicture(boolean value){
|
||||
pictureInPicture=value;
|
||||
if(value){cancelMouseInput();closeMenu();closeKeyboard();modifiers.clear();}
|
||||
if(value){cancelMouseInput();closeMenu();closeKeyboard();}
|
||||
setVisibility(value?GONE:VISIBLE);positionMouse();
|
||||
}
|
||||
private void toggleMouse(){cancelMouseInput();mouseVisible=!mouseVisible;closeMenu();positionMouse();}
|
||||
@@ -80,7 +99,7 @@ final class SessionControls extends FrameLayout {
|
||||
// When the orb is near the middle of a narrow phone, use the adjacent
|
||||
// row rather than letting the two controls overlap.
|
||||
if(x+width>getWidth()-margin){x=bound(orb.getX(),margin,getWidth()-width-margin);y=orb.getY()+orb.getHeight()+gap;if(y+height>getHeight()-margin)y=orb.getY()-gap-height;}
|
||||
if(keyboard!=null&&x<keyboard.getX()+keyboard.getWidth()&&x+width>keyboard.getX()&&y<keyboard.getY()+keyboard.getHeight()&&y+height>keyboard.getY()){
|
||||
if(keyboard.isOpen()&&x<keyboard.getX()+keyboard.getWidth()&&x+width>keyboard.getX()&&y<keyboard.getY()+keyboard.getHeight()&&y+height>keyboard.getY()){
|
||||
y=keyboard.getY()-gap-height;if(y<margin)y=keyboard.getY()+keyboard.getHeight()+gap;
|
||||
}
|
||||
mouseBar.setX(location[0]-hostLocation[0]+bound(x,margin,getWidth()-width-margin));
|
||||
@@ -89,23 +108,24 @@ final class SessionControls extends FrameLayout {
|
||||
}
|
||||
void setMuted(boolean value){muted=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");}
|
||||
void videoSize(int width,int height){waiting.setVisibility(GONE);positionMouse();}
|
||||
void resetVideoStatistics(){statistics(new RemoteNetworkStatistics.Snapshot(network.report,0,0,0,Double.NaN,network.rtt));}
|
||||
void resetVideoStatistics(){statistics(new RemoteNetworkStatistics.Snapshot(network.report,0,0,0,Double.NaN,network.rtt,network.remoteEncoder,network.localDecoder));}
|
||||
void statistics(RemoteNetworkStatistics.Snapshot value){network=value;renderNetwork();}
|
||||
void videoSettings(RemoteVideoSettings value){settings=value;renderVideoSettings();}
|
||||
void privacyScreen(RemotePrivacyScreen value){privacy=value;renderPrivacy();}
|
||||
void fileStatus(String status,boolean received){transferStatus=status;hasReceivedFile=received;renderFileStatus();}
|
||||
private void renderFileStatus(){
|
||||
if(transferLabel!=null){transferLabel.setText(transferStatus);transferLabel.setVisibility(transferStatus.isEmpty()?GONE:VISIBLE);}
|
||||
if(saveFileButton!=null)saveFileButton.setVisibility(hasReceivedFile?VISIBLE:GONE);
|
||||
}
|
||||
private void closeMenu(){if(menu!=null){removeView(menu);menu=null;menuScroll=null;}if(shield!=null){removeView(shield);shield=null;}networkLabel=videoFeedback=transferLabel=saveFileButton=null;trafficCells=null;trafficRows=null;networkDetails=null;encryptionLock=encryptionOpen=null;settingRows.clear();orb.setContentDescription("展开远程控制菜单");positionMouse();}
|
||||
private void closeMenu(){if(menu!=null){removeView(menu);menu=null;menuScroll=null;}if(shield!=null){removeView(shield);shield=null;}videoFeedback=transferLabel=saveFileButton=null;privacyFeedback=privacyCaption=null;privacyButton=null;privacyIcon=null;trafficCells=null;trafficRows=null;networkDetails=null;encryptionLock=encryptionOpen=null;settingRows.clear();orb.setContentDescription(L10n.tr("展开远程控制菜单"));positionMouse();}
|
||||
private void openMenu(String page){
|
||||
cancelMouseInput();
|
||||
closeMenu();menuPage=page;shield=new View(getContext());shield.setOnClickListener(v->closeMenu());addView(shield,new LayoutParams(-1,-1));
|
||||
menu=ui.column();menu.setPadding(ui.dp(12),ui.dp(12),ui.dp(12),ui.dp(12));ui.card(menu,18);menu.setElevation(ui.dp(12));
|
||||
LinearLayout header=ui.row();
|
||||
if(!page.equals("controls"))header.addView(ui.iconButton("back","返回控制栏",()->openMenu("controls")),ui.size(28,32));
|
||||
LinearLayout titles=ui.column();TextView title=ui.text(page.equals("controls")?"● 已连接":page.equals("video")?"画面设置":"网络状态",14,true);titles.addView(title);
|
||||
if(page.equals("controls")){ui.gap(titles,3);networkLabel=ui.text(network.summary(),10,false);networkLabel.setFontFeatureSettings("tnum");networkLabel.setTextColor(SECONDARY);titles.addView(networkLabel);}header.addView(titles,new LinearLayout.LayoutParams(0,-2,1));header.addView(ui.iconButton("close","关闭控制栏",this::closeMenu),ui.size(30,32));menu.addView(header);ui.gap(menu,9);
|
||||
if(!page.equals("controls"))header.addView(ui.iconButton("back",L10n.tr("返回控制栏"),()->openMenu("controls")),ui.size(28,32));
|
||||
TextView title=ui.text(page.equals("controls")?L10n.tr("● 已连接"):page.equals("video")?L10n.tr("画面设置"):L10n.tr("网络状态"),14,true);
|
||||
header.addView(title,new LinearLayout.LayoutParams(0,-2,1));header.addView(ui.iconButton("close",L10n.tr("关闭控制栏"),this::closeMenu),ui.size(30,32));menu.addView(header);ui.gap(menu,9);
|
||||
if(page.equals("video")){videoFeedback=ui.text("",12,false);videoFeedback.setTextColor(SECONDARY);videoFeedback.setPadding(0,0,0,ui.dp(9));menu.addView(videoFeedback);}
|
||||
ScrollView scroll=new ScrollView(getContext());menuScroll=scroll;menu.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));LinearLayout body=ui.column();scroll.addView(body);
|
||||
if(page.equals("network")){
|
||||
@@ -113,26 +133,48 @@ final class SessionControls extends FrameLayout {
|
||||
scroll.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET);body.setPadding(0,0,ui.dp(6),0);
|
||||
}
|
||||
if(page.equals("controls")){
|
||||
String[] labels={"键盘",relative?"相对鼠标":"绝对鼠标",mouseVisible?"收起鼠标":"虚拟鼠标","显示器","画面设置",muted?"静音":"声音","发送文件","网络状态","Ctrl+Alt+Del","剪贴板"};
|
||||
String[] icons={"keyboard","mouse","mouse","display","sliders","audio","folder","chart","lock","clipboard"};
|
||||
Runnable[] clicks={()->{closeMenu();toggleKeyboard();},actions::mouse,this::toggleMouse,actions::display,()->openMenu("video"),actions::mute,actions::sendFile,()->openMenu("network"),actions::secureAttention,actions::clipboard};
|
||||
String[] labels={L10n.tr("键盘"),mouseVisible?L10n.tr("收起鼠标"):L10n.tr("虚拟鼠标"),relative?L10n.tr("相对鼠标"):L10n.tr("绝对鼠标"),L10n.tr("显示器"),L10n.tr("画面设置"),muted?L10n.tr("静音"):L10n.tr("声音"),L10n.tr("发送文件"),L10n.tr("网络状态"),"Ctrl+Alt+Del",L10n.tr("剪贴板"),L10n.tr("隐私屏")};
|
||||
String[] icons={"keyboard","mouse","mouse","display","sliders","audio","folder","chart","lock","clipboard","eye"};
|
||||
Runnable[] clicks={()->{closeMenu();toggleKeyboard();},this::toggleMouse,actions::mouse,actions::display,()->openMenu("video"),actions::mute,actions::sendFile,()->openMenu("network"),actions::secureAttention,actions::clipboard,actions::togglePrivacy};
|
||||
menuColumns=controlColumns();
|
||||
for(int row=0;row<(labels.length+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col<menuColumns;col++){int index=row*menuColumns+col;if(index>=labels.length){View spacer=new View(getContext());LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(spacer,p);continue;}LinearLayout control=ui.column();control.setGravity(Gravity.CENTER);control.setBackground(ui.background(0xFFF0F0F2,10));control.addView(ui.icon(icons[index],INK),ui.size(19,19));ui.gap(control,4);TextView caption=ui.text(labels[index],10,false);caption.setSingleLine();caption.setGravity(Gravity.CENTER);control.addView(caption);control.setContentDescription(labels[index]);control.setClickable(true);control.setFocusable(true);control.setOnClickListener(v->clicks[index].run());
|
||||
java.util.List<Integer> visibleControls=new java.util.ArrayList<>();
|
||||
for(int index=0;index<labels.length;index++){
|
||||
if(index==8&&!secureAttentionVisible||index==10&&!privacy.desktopAvailable)continue;
|
||||
visibleControls.add(index);
|
||||
}
|
||||
int controlCount=visibleControls.size();
|
||||
for(int row=0;row<(controlCount+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col<menuColumns;col++){int position=row*menuColumns+col;if(position>=controlCount){View spacer=new View(getContext());LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(spacer,p);continue;}int index=visibleControls.get(position);LinearLayout control=ui.column();control.setGravity(Gravity.CENTER);control.setBackground(ui.background(0xFFF0F0F2,10));MobileUi.Icon icon=ui.icon(icons[index],INK);control.addView(icon,ui.size(19,19));ui.gap(control,4);TextView caption=ui.text(labels[index],10,false);caption.setMaxLines(2);caption.setAutoSizeTextTypeUniformWithConfiguration(8,10,1,android.util.TypedValue.COMPLEX_UNIT_SP);caption.setGravity(Gravity.CENTER);control.addView(caption);control.setContentDescription(labels[index]);control.setClickable(true);control.setFocusable(true);control.setOnClickListener(v->clicks[index].run());
|
||||
if(index==10){privacyButton=control;privacyIcon=icon;privacyCaption=caption;}
|
||||
LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(control,p);
|
||||
}body.addView(line);if((row+1)*menuColumns<labels.length)ui.gap(body,7);}
|
||||
}body.addView(line);if((row+1)*menuColumns<controlCount)ui.gap(body,7);}
|
||||
privacyFeedback=ui.text("",11,false);privacyFeedback.setTextColor(SECONDARY);privacyFeedback.setPadding(0,ui.dp(7),0,0);privacyFeedback.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);body.addView(privacyFeedback);renderPrivacy();
|
||||
ui.gap(body,7);transferLabel=ui.text(transferStatus,11,false);transferLabel.setTextColor(SECONDARY);transferLabel.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);body.addView(transferLabel);
|
||||
saveFileButton=ui.action("保存收到的文件",BLUE,actions::saveFile);body.addView(saveFileButton);renderFileStatus();
|
||||
saveFileButton=ui.action(L10n.tr("保存收到的文件"),BLUE,actions::saveFile);body.addView(saveFileButton);renderFileStatus();
|
||||
}else if(page.equals("network")){
|
||||
networkPage(body);
|
||||
}else{
|
||||
settingRows.add(new SettingRow(body,0,"画面质量",new String[]{"低","中","高"},new int[]{0,1,2}));ui.gap(body,12);
|
||||
settingRows.add(new SettingRow(body,1,"画面采集帧率",new String[]{"30 fps","60 fps"},new int[]{30,60}));ui.gap(body,12);
|
||||
settingRows.add(new SettingRow(body,2,"画面偏好",RemoteVideoSettings.PREFERENCES,new int[]{0,1,2}));renderVideoSettings();
|
||||
settingRows.add(new SettingRow(body,0,L10n.tr("画面质量"),new String[]{L10n.tr("低"),L10n.tr("中"),L10n.tr("高")},new int[]{0,1,2}));ui.gap(body,12);
|
||||
settingRows.add(new SettingRow(body,1,L10n.tr("画面采集帧率"),new String[]{"30 fps","60 fps"},new int[]{30,60}));ui.gap(body,12);
|
||||
settingRows.add(new SettingRow(body,2,L10n.tr("画面偏好"),RemoteVideoSettings.preferences(),new int[]{0,1,2}));renderVideoSettings();
|
||||
}
|
||||
ui.gap(menu,9);TextView disconnect=ui.action("断开连接",Color.WHITE,()->{closeMenu();closeKeyboard();actions.disconnect();});disconnect.setMinHeight(ui.dp(34));disconnect.setTextSize(14);disconnect.setBackground(ui.background(RED,8));menu.addView(disconnect,ui.size(-1,34));
|
||||
addView(menu,new LayoutParams(panelWidth(getWidth()),panelHeight(getHeight())));orb.bringToFront();orb.setContentDescription("收起远程控制菜单");
|
||||
ui.gap(menu,9);TextView disconnect=ui.action(L10n.tr("断开连接"),Color.WHITE,()->{closeMenu();closeKeyboard();actions.disconnect();});disconnect.setMinHeight(ui.dp(34));disconnect.setTextSize(14);disconnect.setBackground(ui.background(RED,8));menu.addView(disconnect,ui.size(-1,34));
|
||||
addView(menu,new LayoutParams(panelWidth(getWidth()),panelHeight(getHeight())));orb.bringToFront();orb.setContentDescription(L10n.tr("收起远程控制菜单"));
|
||||
positionMouse();
|
||||
}
|
||||
private void renderPrivacy(){
|
||||
if(menu!=null&&menuPage.equals("controls")&&(privacyButton!=null)!=privacy.desktopAvailable){
|
||||
openMenu("controls");return;
|
||||
}
|
||||
long now=android.os.SystemClock.elapsedRealtime();privacy.tick(now);
|
||||
if(privacyFeedback!=null){String feedback=privacy.feedback(now);privacyFeedback.setText(feedback);privacyFeedback.setVisibility(feedback.isEmpty()?GONE:VISIBLE);}
|
||||
if(privacyButton==null)return;
|
||||
boolean enabled=privacy.enabled(now);String action=privacy.active()?L10n.tr("关闭隐私屏"):L10n.tr("开启隐私屏");
|
||||
privacyIcon.setName(enabled?"eye_off":"eye");
|
||||
privacyCaption.setText(privacy.buttonTitle(now));
|
||||
privacyButton.setSelected(enabled);
|
||||
privacyButton.setContentDescription(action+","+privacy.status(now));
|
||||
privacyButton.setEnabled(privacy.canToggle(now));privacyButton.setAlpha(privacyButton.isEnabled()?1f:.45f);
|
||||
}
|
||||
private final class SettingRow {
|
||||
final int field;final int[] values;final SegmentedControl group;
|
||||
SettingRow(LinearLayout body,int field,String title,String[] labels,int[] values){
|
||||
@@ -141,7 +183,7 @@ final class SessionControls extends FrameLayout {
|
||||
group=new SegmentedControl(ui,labels,selectionIndex(),settings.supported,index->actions.videoSettings(field,values[index]));
|
||||
group.setPadding(ui.dp(2),ui.dp(2),ui.dp(2),ui.dp(2));body.addView(group);
|
||||
for(int i=0;i<labels.length;i++){
|
||||
TextView button=(TextView)group.getChildAt(i);button.setTextSize(12);button.setMinHeight(ui.dp(28));button.setSingleLine();button.setContentDescription(title+","+labels[i]);
|
||||
TextView button=(TextView)group.getChildAt(i);button.setTextSize(12);button.setMinHeight(ui.dp(28));button.setSingleLine();button.setAutoSizeTextTypeUniformWithConfiguration(9,12,1,android.util.TypedValue.COMPLEX_UNIT_SP);button.setContentDescription(title+","+labels[i]);
|
||||
button.setLayoutParams(new LinearLayout.LayoutParams(0,ui.dp(28),1));
|
||||
}
|
||||
}
|
||||
@@ -155,15 +197,17 @@ final class SessionControls extends FrameLayout {
|
||||
for(SettingRow row:settingRows)row.render();
|
||||
}
|
||||
private void networkPage(LinearLayout body){
|
||||
trafficCells=new TextView[4][3];trafficRows=new LinearLayout[4];String[] names={"视频","音频","数据","合计"};
|
||||
LinearLayout heading=trafficRow(body,"",new TextView[3],false);((TextView)heading.getChildAt(1)).setText("接收");((TextView)heading.getChildAt(2)).setText("发送");((TextView)heading.getChildAt(3)).setText("丢包率");
|
||||
trafficCells=new TextView[4][3];trafficRows=new LinearLayout[4];String[] names={L10n.tr("视频"),L10n.tr("音频"),L10n.tr("数据"),L10n.tr("合计")};
|
||||
LinearLayout heading=trafficRow(body,"",new TextView[3],false);((TextView)heading.getChildAt(1)).setText(L10n.tr("接收"));((TextView)heading.getChildAt(2)).setText(L10n.tr("发送"));((TextView)heading.getChildAt(3)).setText(L10n.tr("丢包率"));
|
||||
for(int i=0;i<heading.getChildCount();i++)((TextView)heading.getChildAt(i)).setTextColor(SECONDARY);
|
||||
for(int i=0;i<4;i++)trafficRows[i]=trafficRow(body,names[i],trafficCells[i],i==3);
|
||||
ui.gap(body,10);ui.divider(body);ui.gap(body,10);
|
||||
String[] titles={"帧率","分辨率","画面延时","连接延时(RTT)","连接方式","媒体加密"};networkDetails=new TextView[titles.length];
|
||||
for(int i=0;i<titles.length;i++){LinearLayout row=ui.row();TextView title=ui.text(titles[i],12,false);title.setTextColor(SECONDARY);row.addView(title,new LinearLayout.LayoutParams(0,-2,1));
|
||||
String[] titles={L10n.tr("帧率"),L10n.tr("分辨率"),L10n.tr("画面延时"),L10n.tr("连接延时(RTT)"),L10n.tr("连接方式"),L10n.tr("媒体加密"),L10n.tr("被控端编码器"),L10n.tr("控制端解码器")};networkDetails=new TextView[titles.length];
|
||||
for(int i=0;i<titles.length;i++){LinearLayout row=ui.row();TextView title=ui.text(titles[i],12,false);title.setTextColor(SECONDARY);row.addView(title,i>=6?new LinearLayout.LayoutParams(-2,-2):new LinearLayout.LayoutParams(0,-2,1));
|
||||
if(i==5){encryptionLock=ui.icon("lock",GREEN);encryptionOpen=ui.icon("unlock",SECONDARY);for(View icon:new View[]{encryptionLock,encryptionOpen}){LinearLayout.LayoutParams p=ui.size(14,14);p.rightMargin=ui.dp(4);row.addView(icon,p);}}
|
||||
TextView value=ui.text("—",12,false);value.setFontFeatureSettings("tnum");row.addView(value);networkDetails[i]=value;body.addView(row);if(i<titles.length-1)ui.gap(body,7);}
|
||||
TextView value=ui.text("—",12,false);value.setFontFeatureSettings("tnum");
|
||||
if(i>=6){value.setGravity(Gravity.END);value.setSingleLine();value.setEllipsize(android.text.TextUtils.TruncateAt.MARQUEE);value.setMarqueeRepeatLimit(-1);value.setSelected(true);LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,-2,1);p.leftMargin=ui.dp(8);row.addView(value,p);}else row.addView(value);
|
||||
networkDetails[i]=value;body.addView(row);if(i<titles.length-1)ui.gap(body,7);}
|
||||
renderNetwork();
|
||||
}
|
||||
private LinearLayout trafficRow(LinearLayout body,String title,TextView[] cells,boolean total){
|
||||
@@ -174,16 +218,16 @@ final class SessionControls extends FrameLayout {
|
||||
return row;
|
||||
}
|
||||
private void renderNetwork(){
|
||||
if(networkLabel!=null)networkLabel.setText(network.summary());
|
||||
if(trafficCells==null)return;
|
||||
String[] names={"视频","音频","数据","合计"};
|
||||
String[] names={L10n.tr("视频"),L10n.tr("音频"),L10n.tr("数据"),L10n.tr("合计")};
|
||||
for(int i=0;i<4;i++){
|
||||
RemoteNetworkStatistics.Traffic traffic=network.report==null?null:network.report.traffic[i];
|
||||
String in=RemoteNetworkStatistics.bitrate(traffic==null?-1:traffic.inbound),out=RemoteNetworkStatistics.bitrate(traffic==null?-1:traffic.outbound),loss=RemoteNetworkStatistics.loss(traffic==null?Double.NaN:traffic.loss);
|
||||
trafficCells[i][0].setText(in);trafficCells[i][1].setText(out);trafficCells[i][2].setText(loss);trafficRows[i].setContentDescription(names[i]+",接收 "+in+",发送 "+out+",丢包率 "+loss);
|
||||
trafficCells[i][0].setText(in);trafficCells[i][1].setText(out);trafficCells[i][2].setText(loss);trafficRows[i].setContentDescription(L10n.format("{0},接收 {1},发送 {2},丢包率 {3}",names[i],in,out,loss));
|
||||
}
|
||||
String[] values={network.fps+" FPS",network.resolution(),RemoteNetworkStatistics.latency(network.latency),RemoteNetworkStatistics.latency(network.rtt),network.mode(),network.report==null?"—":network.report.srtp?"SRTP 已启用":"未启用"};
|
||||
for(int i=0;i<values.length;i++)networkDetails[i].setText(values[i]);networkDetails[5].setTextColor(network.report!=null&&network.report.srtp?GREEN:SECONDARY);
|
||||
String[] values={network.fps+" FPS",network.resolution(),RemoteNetworkStatistics.latency(network.latency),RemoteNetworkStatistics.latency(network.rtt),network.mode(),network.report==null?"—":network.report.srtp?L10n.tr("SRTP 已启用"):L10n.tr("未启用"),network.remoteEncoder.isEmpty()?"—":network.remoteEncoder,network.localDecoder.isEmpty()?"—":network.localDecoder};
|
||||
// Reassigning identical text every statistics tick restarts the marquee.
|
||||
for(int i=0;i<values.length;i++)if(!android.text.TextUtils.equals(networkDetails[i].getText(),values[i]))networkDetails[i].setText(values[i]);networkDetails[5].setTextColor(network.report!=null&&network.report.srtp?GREEN:SECONDARY);
|
||||
encryptionLock.setVisibility(network.report!=null&&network.report.srtp?VISIBLE:GONE);encryptionOpen.setVisibility(network.report!=null&&!network.report.srtp?VISIBLE:GONE);
|
||||
}
|
||||
private int controlColumns(){return getMeasuredHeight()<ui.dp(400)&&getMeasuredWidth()>=ui.dp(400)?4:3;}
|
||||
@@ -201,38 +245,11 @@ final class SessionControls extends FrameLayout {
|
||||
return Math.min(contentHeight,Math.max(1,height-ui.dp(16)));
|
||||
}
|
||||
private void positionMenu(){if(menu==null)return;float ox=orb.getX()+orb.getWidth()/2f;float x=ox>getWidth()/2f?orb.getX()-ui.dp(12)-menu.getWidth():orb.getX()+orb.getWidth()+ui.dp(12);menu.setX(bound(x,ui.dp(8),getWidth()-menu.getWidth()-ui.dp(8)));menu.setY(bound(orb.getY(),ui.dp(8),getHeight()-menu.getHeight()-ui.dp(8)));}
|
||||
private int keyboardWidth(int width){return Math.min(ui.dp(computerKeyboard?520:440),Math.max(1,width-ui.dp(16)));}
|
||||
private int keyboardHeight(int height){return Math.min(keyboardPreferredHeight,Math.max(1,height-ui.dp(16)));}
|
||||
private void positionKeyboard(){if(keyboard!=null){keyboard.setX(bound(keyboard.getX(),ui.dp(8),getWidth()-keyboard.getWidth()-ui.dp(8)));keyboard.setY(bound(keyboard.getY(),ui.dp(8),getHeight()-keyboard.getHeight()-ui.dp(8)));}positionMouse();}
|
||||
private void closeKeyboard(){if(keyboard!=null){removeView(keyboard);keyboard=null;keyboardKeys=null;}}
|
||||
private void toggleKeyboard(){if(keyboard!=null){closeKeyboard();modifiers.clear();return;}modifiers.clear();showKeyboard();}
|
||||
private void showKeyboard(){
|
||||
float oldX=keyboard==null?-1:keyboard.getX(),oldY=keyboard==null?-1:keyboard.getY();int oldScroll=keyboardKeys==null?0:keyboardKeys.getScrollY();closeKeyboard();keyboard=ui.column();keyboard.setPadding(ui.dp(5),ui.dp(5),ui.dp(5),ui.dp(5));keyboard.setBackground(ui.background(0xFFD1D6DE,12));keyboard.setElevation(ui.dp(10));
|
||||
LinearLayout top=ui.row();TextView handle=ui.text("━",19,true);handle.setGravity(Gravity.CENTER);handle.setTextColor(SECONDARY);handle.setContentDescription("拖动键盘");top.addView(handle,new LinearLayout.LayoutParams(0,ui.dp(28),1));top.addView(ui.iconButton("close","收起键盘",this::closeKeyboard),ui.size(30,28));keyboard.addView(top);drag(handle,keyboard,()->{});
|
||||
// Keep the drag/close row fixed while short split windows can scroll every key into view.
|
||||
keyboardKeys=new ScrollView(getContext());keyboard.addView(keyboardKeys,new LinearLayout.LayoutParams(-1,0,1));LinearLayout keys=ui.column();keyboardKeys.addView(keys);
|
||||
String[][] rows=computerKeyboard?new String[][]{{"Esc","F1","F2","F3","F4","F5","F6"},{"Tab","Ctrl","Alt","Win","↑","⌫"},{"复制","粘贴","全选","←","↓","→"},{"ABC","空格","Enter"}}:new String[][]{{"1","2","3","4","5","6","7","8","9","0"},{"q","w","e","r","t","y","u","i","o","p"},{"a","s","d","f","g","h","j","k","l"},{"⇧","z","x","c","v","b","n","m","⌫"},{"电脑","剪贴板","空格",".","Enter"}};
|
||||
for(String[] values:rows){LinearLayout line=ui.row();for(String value:values){String shown=shift&&value.length()==1?value.toUpperCase(java.util.Locale.ROOT):value;TextView key=ui.action(shown,INK,()->keyboardKey(value));key.setTextSize(value.length()>2?11:14);key.setMinHeight(ui.dp(31));int modifier=value.equals("Ctrl")?17:value.equals("Alt")?18:value.equals("Win")?91:0;key.setBackground(ui.background(modifiers.contains(modifier)?0xFF9BC7F7:value.length()>1||value.equals("⇧")?0xFFAFB8C5:Color.WHITE,5));LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(31),value.equals("空格")?3:1);p.setMargins(ui.dp(1.5f),0,ui.dp(1.5f),0);line.addView(key,p);}keys.addView(line);ui.gap(keys,3);}
|
||||
keyboardPreferredHeight=ui.dp(5)*2+ui.dp(28)+rows.length*(ui.dp(31)+ui.dp(3));
|
||||
addView(keyboard,new LayoutParams(keyboardWidth(getWidth()),keyboardHeight(getHeight())));LinearLayout shownKeyboard=keyboard;ScrollView shownKeys=keyboardKeys;
|
||||
keyboard.post(()->{if(keyboard==shownKeyboard){keyboard.setX(oldX<0?(getWidth()-keyboard.getWidth())/2f:oldX);keyboard.setY(oldY<0?getHeight()-keyboard.getHeight()-ui.dp(20):oldY);shownKeys.scrollTo(0,oldScroll);constrainPanels();}});
|
||||
}
|
||||
private void keyboardKey(String value){
|
||||
switch(value){
|
||||
case "⇧":shift=!shift;showKeyboard();return;
|
||||
case "电脑":computerKeyboard=true;showKeyboard();return;
|
||||
case "ABC":computerKeyboard=false;showKeyboard();return;
|
||||
case "剪贴板":actions.clipboard();return;
|
||||
case "复制":actions.shortcut(0x43);return;case "粘贴":actions.shortcut(0x56);return;case "全选":actions.shortcut(0x41);return;
|
||||
case "空格":sendKey(32);return;case "⌫":sendKey(8);return;case "Enter":sendKey(13);return;case "Esc":sendKey(27);return;case "Tab":sendKey(9);return;
|
||||
case "↑":sendKey(38);return;case "↓":sendKey(40);return;case "←":sendKey(37);return;case "→":sendKey(39);return;
|
||||
// Latch locally; the next key sends a complete chord without leaving remote modifiers down.
|
||||
case "Ctrl":toggleModifier(17);return;case "Alt":toggleModifier(18);return;case "Win":toggleModifier(91);return;
|
||||
default:if(value.matches("F[1-6]")){sendKey(111+Integer.parseInt(value.substring(1)));return;}if(!modifiers.isEmpty())sendKey(KeyMap.ascii(value.charAt(0))&255);else actions.text(shift?value.toUpperCase(java.util.Locale.ROOT):value);
|
||||
}
|
||||
}
|
||||
private void toggleModifier(int code){if(!modifiers.add(code))modifiers.remove(code);showKeyboard();}
|
||||
private void sendKey(int code){if(modifiers.isEmpty())actions.key(code);else{actions.chord(code,modifiers.stream().mapToInt(Integer::intValue).toArray());modifiers.clear();showKeyboard();}}
|
||||
private void positionKeyboard(){positionMouse();}
|
||||
boolean closeKeyboard(){return keyboard.close();}
|
||||
void resetKeyboardContext(){keyboard.resetContext();}
|
||||
void keyboardInsets(int height,boolean visible,int navigation){keyboard.updateInsets(height,visible,navigation);}
|
||||
private void toggleKeyboard(){keyboard.bringToFront();keyboard.toggle();}
|
||||
@android.annotation.SuppressLint("ClickableViewAccessibility") // Tap dispatches performClick; dragging is an additional action.
|
||||
private void drag(View handle,View target,Runnable tap){
|
||||
handle.setOnClickListener(v->tap.run());handle.setOnTouchListener(new View.OnTouchListener(){float startX,startY,originX,originY;boolean moved;
|
||||
@@ -251,7 +268,7 @@ final class SessionControls extends FrameLayout {
|
||||
cancelMouseInput();
|
||||
if(menu!=null&&menuPage.equals("controls")&&menuColumns!=controlColumns())openMenu("controls");
|
||||
orb.setX(bound(orb.getX(),ui.dp(8),getWidth()-orb.getWidth()-ui.dp(8)));orb.setY(bound(orb.getY(),ui.dp(8),getHeight()-orb.getHeight()-ui.dp(8)));
|
||||
if(menu!=null||keyboard!=null)requestLayout();
|
||||
if(menu!=null||keyboard.isOpen())requestLayout();
|
||||
positionKeyboard();
|
||||
positionMouse();
|
||||
}
|
||||
@@ -265,9 +282,10 @@ final class SessionControls extends FrameLayout {
|
||||
// measure again with the final cap, even when it did not change.
|
||||
p.width=width;p.height=height;resized=true;
|
||||
}
|
||||
if(keyboard!=null){
|
||||
ViewGroup.LayoutParams p=keyboard.getLayoutParams();int width=keyboardWidth(getMeasuredWidth()),height=keyboardHeight(getMeasuredHeight());
|
||||
if(p.width!=width||p.height!=height){p.width=width;p.height=height;resized=true;}
|
||||
if(keyboard.isOpen()){
|
||||
LayoutParams p=(LayoutParams)keyboard.getLayoutParams();
|
||||
int height=keyboard.preferredHeight(),inset=keyboard.bottomInset();
|
||||
if(p.height!=height||p.bottomMargin!=inset){p.height=height;p.bottomMargin=inset;resized=true;}
|
||||
}
|
||||
if(resized)super.onMeasure(widthSpec,heightSpec);
|
||||
}
|
||||
@@ -280,7 +298,8 @@ final class SessionControls extends FrameLayout {
|
||||
positionKeyboard();
|
||||
positionMouse();
|
||||
}
|
||||
@Override protected void onDetachedFromWindow(){cancelMouseInput();super.onDetachedFromWindow();}
|
||||
@Override protected void onAttachedToWindow(){super.onAttachedToWindow();if(android.os.Build.VERSION.SDK_INT<30)getViewTreeObserver().addOnGlobalLayoutListener(legacyKeyboardLayout);}
|
||||
@Override protected void onDetachedFromWindow(){getViewTreeObserver().removeOnGlobalLayoutListener(legacyKeyboardLayout);cancelMouseInput();super.onDetachedFromWindow();}
|
||||
@Override protected void onSizeChanged(int width,int height,int oldWidth,int oldHeight){super.onSizeChanged(width,height,oldWidth,oldHeight);post(this::constrainPanels);}
|
||||
private static float bound(float value,float min,float max){return Math.max(min,Math.min(value,Math.max(min,max)));}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ final class VirtualMouseBar extends LinearLayout {
|
||||
shell.setStroke(ui.dp(.5f), 0xFFD4DAE2);
|
||||
setBackground(shell);
|
||||
setMotionEventSplittingEnabled(true);
|
||||
keys[0] = key("鼠标左键,按住并拖动,松开结束拖拽", 0);
|
||||
keys[1] = key("鼠标滚轮,上下滑动,可滑出按键范围", 1);
|
||||
keys[2] = key("鼠标右键,按住并拖动,松开结束拖拽", 2);
|
||||
keys[0] = key(L10n.tr("鼠标左键,按住并拖动,松开结束拖拽"), 0);
|
||||
keys[1] = key(L10n.tr("鼠标滚轮,上下滑动,可滑出按键范围"), 1);
|
||||
keys[2] = key(L10n.tr("鼠标右键,按住并拖动,松开结束拖拽"), 2);
|
||||
input.didCancel = this::resetTouches;
|
||||
setContentDescription("虚拟鼠标");
|
||||
setContentDescription(L10n.tr("虚拟鼠标"));
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility") // Touch sends real down/up; performClick serves accessibility only.
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · 已完成"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" 传输失败"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" 台设备"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" 条 · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" 条未读"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"CrossDesk 远程连接"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL 第 3 版"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"OpenFEC 告知"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"OpenFEC 许可与源码"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"P2P 直连"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP 已启用"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"TURN 中继"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"{0} 传输失败"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"{0} 台设备"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0} 条未读"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0},接收 {1},发送 {2},丢包率 {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● 已连接"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"下拉刷新"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"丢包率"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"中"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。"</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"低"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"使用自定义服务器"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"保存密码"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"保存收到的文件"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"保存远程画面预览"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"保存远程画面预览?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"信令连接中断,请重新连接"</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"公告加载失败,请重试。"</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"公告详情"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"共 "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"共 {0} 条 · {1} 条未读"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"关于"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"关于 CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"关于,有新版本"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"关闭"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"关闭控制栏"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"关闭隐私屏"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。"</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"分辨率"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"删除"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"删除公告"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"删除记录"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"删除这条公告?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"刷新公告"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"剪贴板"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"加载中…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"加载更多"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"升级提示"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"原生连接初始化失败"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"发送"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"发送 "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"发送后,在远程键盘中点击“粘贴”。接收的远端文本可手动复制到本机。"</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"发送文件"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"发送文本"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"取消"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"取消连接"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"各组件遵循所列开源许可证,其授予的权利不受本应用普通使用条款限制。查看源码需要网络连接。"</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"合计"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"同意并继续"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"后台保持连接,点按返回远程桌面"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"后台保活未能启动,请保持应用在前台"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"后台保活未能启动,请保持应用在前台后重新连接"</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"后台连接已结束,请重新连接"</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"后台连接服务已停止,请重新连接"</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"后续连接仍会保存新的预览图。"</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"在线"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"填写主机名或 IPv4 地址,不含协议和路径"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"声音"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"复制远端文本"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。"</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"媒体加密"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"字母数字"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"安全存储或证书初始化失败,请重试或清除应用数据"</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"完成"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"完成隐私授权后可检查更新"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"完成隐私授权后登记本机身份"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"密码保存失败,本次连接仍可使用"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"密码将加密保存在本机 Android Keystore 中"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"对端 ID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"对端 ID "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"对端 ID {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"将删除本机保存的所有远程预览图,保留连接记录和密码。"</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"屏幕方向"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"展开远程控制菜单"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"已从通知断开远程连接"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"已保存密码"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"已保存密码无法读取,请重新输入"</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"已保存的密码未能保留,请重新输入一次"</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"已发送 Ctrl+Alt+Del"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"已启用后台保活,切换应用可继续保持连接"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"已启用后台保活,可从连接通知返回或断开"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"已授权"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"已读"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"已连接 "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"已连接服务器"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"帧率"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"帧率优先"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"平衡"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"应用中的许可文件缺失或损坏,请重新安装应用。"</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"开发版本包含尚未发布的修改,以下链接为基础版本源码。"</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"开启保存"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"开启隐私屏"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"开源组件"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"开源软件权利说明"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"当前已是最新版本"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送"</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"成功连接后,这里会显示最近的远程设备。"</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"我已阅读并同意《隐私政策》"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"所选文件(无法读取或空间不足)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"找不到该设备 ID"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"指令发送失败,请重试"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"指令已发送,等待电脑确认…"</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"授权状态"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"接收"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"接收 "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"控制端解码器"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"撤回"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"撤回后将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。"</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"撤回联网授权"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"撤回联网授权?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"收起远程控制菜单"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"收起键盘"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"收起鼠标"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"数据"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"文件保存失败,请重试"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"文件已保存"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"文件选择已超时,会话已结束"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"文本超过 128 KB"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"断开"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"断开后将返回首页"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"断开远程连接?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"断开连接"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"新版本"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"新版本可用:v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"新版本可用:v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"无未读公告"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"无法保存本地阅读状态,请重试。"</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"无法删除这条公告,请重试。"</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"无法发起远程连接"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"无法读取开源许可"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"显示器"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"显示密码"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"暂不同意"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"暂无公告"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"最近连接"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"服务器"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"服务器 TLS 证书校验失败"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"服务器地址"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"服务器登录失败,请检查服务器地址和网络"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"服务器端口"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"服务器连接超时,请检查服务器地址和网络"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"未启用"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"未找到可打开链接的浏览器"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"未授权"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"未读"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"未连接服务器 · 点击重试"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。"</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"本机 ID "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"本机 ID {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"本版本源码与构建说明"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"本版本源码信息暂不可用。"</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"松开刷新"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"构建说明"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"构建配方与项目补丁"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"查看源码"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"检查失败,请稍后重试"</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"检查更新"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"正在关闭隐私屏…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"正在建立安全连接…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"正在开启隐私屏…"</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"正在检查更新…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"正在清除…"</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"正在获取本机 ID…"</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"正在读取所选文件…"</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"正在连接"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"正在连接信令服务器…"</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"正在连接服务器…"</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"正在重连服务器…"</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"此设备没有可用的文件选择器"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"清除"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"清除预览图"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"清除预览图?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"源码与构建说明"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"源码版本"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"版本"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"版权与许可声明"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"电脑键盘"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"画质优先"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"画面偏好"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"画面延时"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"画面设置"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"画面质量"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"画面采集帧率"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"相对位置"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"相对鼠标"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"知道了"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"确定"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"确认"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"确认连接"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"离线"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"空格"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"端口范围为 1–65535"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"符号功能"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"等待电脑返回隐私屏状态;旧版电脑端可能不支持"</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"等待远端屏幕信息"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"等待远端画面…"</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"等待隐私授权"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"系统键盘"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"绝对位置"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"绝对鼠标"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"网络状态"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"自定义"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"虚拟鼠标"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。"</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"被控端版本过低,请升级到最新版本。"</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"被控端编码器"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"要发送到远端的文本"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"视频"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"许可"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"许可与版权声明"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"许可与版权声明可离线查阅。"</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"设备身份保存失败,请检查安全存储"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"设备身份及选择保存的访问密码使用 Android Keystore 加密。连接记录仅保存在本机,不纳入设备备份。剪贴板内容在结束会话时清除。离开应用后通过画中画或连接通知保持会话,可在应用或通知中断开。"</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"设置"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"设置,有新版本"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"访问密码"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"访问密码错误,请重新输入"</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"证书校验失败 · 点击重试"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"请先完成联网授权"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"请在远端启用 CrossDesk 系统服务后重试"</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"请求超时,尚未确认电脑的隐私屏状态"</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"请等待当前文件发送完成"</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"请输入 1–65535 范围内的端口"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"请输入 9 位设备 ID"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"请输入有效访问密码"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"调整失败,请重试。"</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"软件许可"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"返回"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"返回控制栏"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"还没有连接记录"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"这条公告已更新或删除,请返回公告列表查看。"</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"远程任务已关闭"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"远程桌面"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"远程桌面触控区域"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"远程桌面:轻触点击,拖动移动,长按右键,双指捏合缩放,放大后双指拖动画面,原始比例下双指上下滑动滚动"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"远程电脑"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"远程窗口已关闭,或后台保活未能启动,会话已结束"</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"远程连接"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"远端密码错误"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"远端已关闭连接"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"远端当前不可用"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"远端访问密码已失效,请重新连接"</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"连接"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"连接 "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"连接失败"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"连接失败 · 点击重试"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"连接已断开"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"连接已结束"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"连接延时(RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"连接建立后可调整,需被控端支持。"</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"连接建立后锁定横屏"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"连接提示"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"连接方式"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"连接服务器"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"连接服务器后可获取最新公告。"</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"连接超时,请检查设备 ID、服务器及远端在线状态"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"连接远程桌面"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"退格"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"通知公告"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"部分预览图未能清除,请重试。"</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"重试"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"重试保存阅读状态"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"锁定"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"键盘"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"阅读并授权"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"隐私"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"隐私与授权"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"隐私屏"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"隐私屏已关闭"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"隐私屏已开启"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"隐私屏已被解锁"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"隐私屏异常"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"隐私屏暂不可用"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"隐私屏状态已过期,等待电脑确认"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"隐私屏状态待确认"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"隐私政策"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"隐私政策暂时无法读取,请拒绝联网并重新安装应用。"</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"静音"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"音频"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"项目主页"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。"</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"高"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"默认"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"默认关闭。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受影响。"</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"鼠标右键,按住并拖动,松开结束拖拽"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"鼠标左键,按住并拖动,松开结束拖拽"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"鼠标控制"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"鼠标滚轮,上下滑动,可滑出按键范围"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">",丢包率 "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">",发送 "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">",已保存密码"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">",接收 "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">",点击重试"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · 已完成"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" 傳輸失敗"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" 臺裝置"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" 條 · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" 條未讀"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"CrossDesk 遠端連線"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL 第 3 版"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"OpenFEC 告知"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"OpenFEC 許可與原始碼"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"P2P 直連"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP 已啟用"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"TURN 中繼"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"{0} 傳輸失敗"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"{0} 臺裝置"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0} 條未讀"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0},接收 {1},傳送 {2},丟包率 {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● 已連線"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"下拉重新整理"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"丟包率"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"中"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"優先保持畫面流暢,頻寬或編碼壓力較大時會更積極地降低解析度。"</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"低"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"使用自定義伺服器"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"儲存密碼"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"儲存收到的檔案"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"儲存遠端畫面預覽"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"儲存遠端畫面預覽?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"信令連線中斷,請重新連線"</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"公告載入失敗,請重試。"</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"公告詳情"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"共 "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"共 {0} 條 · {1} 條未讀"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"關於"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"關於 CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"關於,有新版本"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"關閉"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"關閉控制欄"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"關閉隱私屏"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"兼顧清晰度和流暢度,只在持續壓力下逐步降低解析度。"</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"解析度"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"刪除"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"刪除公告"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"刪除記錄"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"刪除這條公告?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"重新整理公告"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"剪貼簿"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"載入中…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"載入更多"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"更新提示"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"原生連線初始化失敗"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"傳送"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"傳送 "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"傳送後,在遠端鍵盤中點選“貼上”。接收的遠端文字可手動複製到本機。"</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"傳送檔案"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"傳送文字"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"取消"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"取消連線"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"各元件遵循所列開源許可證,其授予的權利不受本應用普通使用條款限制。檢視原始碼需要網路連線。"</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"合計"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"同意並繼續"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"後臺保持連線,點按返回遠端桌面"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"後臺保活未能啟動,請保持應用在前臺"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"後臺保活未能啟動,請保持應用在前臺後重新連線"</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"後臺連線已結束,請重新連線"</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"後臺連線服務已停止,請重新連線"</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"後續連線仍會儲存新的預覽圖。"</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"線上"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"填寫主機名或 IPv4 地址,不含協議和路徑"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"聲音"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"複製遠端文字"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"始終保持較高解析度,頻寬不足時透過降低幀率來保證畫面清晰度。"</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"媒體加密"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"字母數字"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"安全儲存或證書初始化失敗,請重試或清除應用資料"</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"完成"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"完成隱私授權後可檢查更新"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"完成隱私授權後登記本機身份"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"密碼儲存失敗,本次連線仍可使用"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"密碼將加密儲存在本機 Android Keystore 中"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"對端 ID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"對端 ID "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"對端 ID {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"將刪除本機儲存的所有遠端預覽圖,保留連線記錄和密碼。"</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"螢幕方向"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"展開遠端控制選單"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"已從通知斷開遠端連線"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"已儲存密碼"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"已儲存密碼無法讀取,請重新輸入"</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"已儲存的密碼未能保留,請重新輸入一次"</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"已傳送 Ctrl+Alt+Del"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"已啟用後臺保活,切換應用可繼續保持連線"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"已啟用後臺保活,可從連線通知返回或斷開"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"已授權"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"已讀"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"已連線 "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"已連線伺服器"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"幀率"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"影格率優先"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"平衡"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"應用中的許可檔案缺失或損壞,請重新安裝應用。"</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"開發版本包含尚未釋出的修改,以下連結為基礎版本原始碼。"</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"啟用儲存"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"開啟隱私屏"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"開源元件"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"開源軟體權利說明"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"當前已是最新版本"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"當前遠端不支援直接輸入此文字,請使用支援文字輸入的被控端版本,或透過剪貼簿傳送"</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"成功連線後,這裡會顯示最近的遠端裝置。"</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"我已閱讀並同意《隱私政策》"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"所選檔案(無法讀取或空間不足)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"找不到該裝置 ID"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"指令傳送失敗,請重試"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"指令已傳送,等待電腦確認…"</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"授權狀態"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"接收"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"接收 "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"控制端解碼器"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"撤回"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"撤回後將停止遠端連線、關閉畫面預覽儲存並清除已有預覽。連線記錄和密碼會保留,重新連線前需再次授權。"</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"撤回聯網授權"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"撤回聯網授權?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"收起遠端控制選單"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"收起鍵盤"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"收起滑鼠"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"資料"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"檔案儲存失敗,請重試"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"檔案已儲存"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"檔案選擇已超時,會話已結束"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"文字超過 128 KB"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"斷開"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"斷開後將返回首頁"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"斷開遠端連線?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"中斷連線"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"新版本"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"新版本可用:v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"新版本可用:v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"無未讀公告"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"無法儲存本地閱讀狀態,請重試。"</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"無法刪除這條公告,請重試。"</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"無法發起遠端連線"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"無法讀取開源許可"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"顯示器"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"顯示密碼"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"暫不同意"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"暫無公告"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"最近連線"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"伺服器"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"伺服器 TLS 證書校驗失敗"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"伺服器地址"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"伺服器登入失敗,請檢查伺服器地址和網路"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"伺服器埠"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"伺服器連線超時,請檢查伺服器地址和網路"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"未啟用"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"未找到可開啟連結的瀏覽器"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"未授權"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"未讀"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"未連線伺服器 · 點選重試"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"本應用使用 OpenFEC 提供前向糾錯功能,遵循 CeCILL-C 1.0 許可。該許可規定了有限保證及責任限制,詳情見許可全文。"</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"本機 ID "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"本機 ID {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"本版本原始碼與構建說明"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"本版本原始碼資訊暫不可用。"</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"鬆開重新整理"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"構建說明"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"構建配方與專案補丁"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"檢視原始碼"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"檢查失敗,請稍後重試"</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"檢查更新"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"正在關閉隱私屏…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"正在建立安全連線…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"正在開啟隱私屏…"</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"正在檢查更新…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"正在清除…"</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"正在獲取本機 ID…"</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"正在讀取所選檔案…"</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"正在連線"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"正在連線信令伺服器…"</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"正在連線伺服器…"</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"正在重連伺服器…"</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"此裝置沒有可用的檔案選擇器"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"清除"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"清除預覽圖"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"清除預覽圖?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"原始碼與構建說明"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"原始碼版本"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"版本"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"版權與許可宣告"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"電腦鍵盤"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"畫質優先"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"畫面偏好"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"畫面延遲"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"畫面設定"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"畫面質量"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"畫面採集幀率"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"相對位置"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"相對滑鼠"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"知道了"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"確定"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"確認"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"確認連線"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"離線"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"空白鍵"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"埠範圍為 1–65535"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"符號功能"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"等待電腦返回隱私屏狀態;舊版電腦端可能不支援"</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"等待遠端螢幕資訊"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"等待遠端畫面…"</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"等待隱私授權"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"系統鍵盤"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"絕對位置"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"絕對滑鼠"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"網路狀態"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"自定義"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"虛擬滑鼠"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"被控端已透過組合鍵解除隱私屏。點選“鎖定”將重新開啟隱私屏,並禁止本機使用組合鍵解鎖。"</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"被控端版本過低,請升級到最新版本。"</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"被控端編碼器"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"要傳送到遠端的文字"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"影片"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"許可"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"許可與版權宣告"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"許可與版權宣告可離線查閱。"</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"裝置身份儲存失敗,請檢查安全儲存"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"裝置身份及選擇儲存的訪問密碼使用 Android Keystore 加密。連線記錄僅儲存在本機,不納入裝置備份。剪貼簿內容在結束會話時清除。離開應用後透過畫中畫或連線通知保持會話,可在應用或通知中斷開。"</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"設定"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"設定,有新版本"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"訪問密碼"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"訪問密碼錯誤,請重新輸入"</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"證書校驗失敗 · 點選重試"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"請先完成聯網授權"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"請在遠端啟用 CrossDesk 系統服務後重試"</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"請求超時,尚未確認電腦的隱私屏狀態"</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"請等待當前檔案傳送完成"</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"請輸入 1–65535 範圍內的埠"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"請輸入 9 位裝置 ID"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"請輸入有效訪問密碼"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"調整失敗,請重試。"</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"軟體許可"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"返回"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"返回控制欄"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"還沒有連線記錄"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"這條公告已更新或刪除,請返回公告列表檢視。"</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"遠端任務已關閉"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"遠端桌面"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"遠端桌面觸控區域"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"遠端桌面:輕觸點選,拖動移動,長按右鍵,雙指捏合縮放,放大後雙指拖動畫面,原始比例下雙指上下滑動滾動"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"遠端電腦"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"遠端視窗已關閉,或後臺保活未能啟動,會話已結束"</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"遠端連線"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"遠端密碼錯誤"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"遠端已關閉連線"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"遠端當前不可用"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"遠端訪問密碼已失效,請重新連線"</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"連線"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"連線 "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"連線失敗"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"連線失敗 · 點選重試"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"連線已斷開"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"連線已結束"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"連線延時(RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"連線建立後可調整,需被控端支援。"</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"連線建立後鎖定橫屏"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"連線提示"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"連線方式"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"連線伺服器"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"連線伺服器後可獲取最新公告。"</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"連線超時,請檢查裝置 ID、伺服器及遠端線上狀態"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"連線遠端桌面"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"退格"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"通知公告"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"部分預覽圖未能清除,請重試。"</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"重試"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"重試儲存閱讀狀態"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"鎖定"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"鍵盤"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"閱讀並授權"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"隱私"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"隱私與授權"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"隱私屏"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"隱私屏已關閉"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"隱私屏已開啟"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"隱私屏已被解鎖"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"隱私屏異常"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"隱私屏暫不可用"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"隱私屏狀態已過期,等待電腦確認"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"隱私屏狀態待確認"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"隱私政策"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"隱私政策暫時無法讀取,請拒絕聯網並重新安裝應用。"</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"靜音"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"音訊"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"專案主頁"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"預覽可能包含遠端電腦上的個人或工作資訊。開啟後,每次連線會自動將一張畫面儲存在本機並顯示在最近連線中。你可以隨時關閉並清除。"</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"高"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"預設"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"預設關閉。開啟後,手機連線時進入並保持橫屏;關閉時跟隨裝置方向。平板和多視窗不受影響。"</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"滑鼠右鍵,按住並拖動,鬆開結束拖拽"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"滑鼠左鍵,按住並拖動,鬆開結束拖拽"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"滑鼠控制"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"滑鼠滾輪,上下滑動,可滑出按鍵範圍"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">",丟包率 "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">",傳送 "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">",已儲存密碼"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">",接收 "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">",點選重試"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · Abgeschlossen"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" Übertragung fehlgeschlagen"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" Geräte"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" Artikel · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" ungelesene Artikel"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"CrossDesk Remote-Verbindung"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL Version 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"OpenFEC-Hinweis"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"OpenFEC Lizenz und Quelle"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"P2P-Direktverbindung"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP enabled"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"TURN-Relais"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"{0}-Übertragung fehlgeschlagen"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"Geräte: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0} ungelesene Elemente"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, Empfangen von {1}, Senden von {2}, Paketverlustrate {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● Verbunden"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"Zum Aktualisieren nach unten ziehen"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"Verlustrate"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"Mittel"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"Priorisiert die Aufrechterhaltung eines flüssigen Bildes und reduziert die Auflösung stärker, wenn die Bandbreite oder der Codierungsdruck hoch ist."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"Niedrig"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"Verwendung eines benutzerdefinierten Servers"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"Passwort speichern"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"Empfangene Datei speichern"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"Remote-Bildschirmvorschauen speichern"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"Remote-Bildschirmvorschau speichern?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"Die Signalverbindung ist unterbrochen. Bitte stellen Sie die Verbindung wieder her."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"Das Laden der Ankündigung ist fehlgeschlagen. Bitte versuchen Sie es erneut."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"Einzelheiten zur Ankündigung"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"Insgesamt "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"Insgesamt {0} Artikel ·{1} ungelesene Artikel"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"Ungefähr"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"Über CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"Ungefähr, es gibt eine neue Version"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"Schließen"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"Bedienbügel schließen"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"Privatsphäre-Bildschirm deaktivieren"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"Gleicht Klarheit und Glätte aus und verringert die Auflösung bei konstantem Druck nur allmählich."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"Auflösung"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"Löschen"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"Ansage löschen"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"Datensatz löschen"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"Diese Ankündigung löschen?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"Ankündigung aktualisieren"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"Zwischenablage"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"Laden…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"Mehr laden"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"Update empfohlen"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"Die Initialisierung der nativen Verbindung ist fehlgeschlagen"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"Senden"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"Senden "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"Klicken Sie nach dem Senden auf der Remote-Tastatur auf „Einfügen“. Der empfangene Remote-Text kann manuell auf den lokalen Computer kopiert werden."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"Datei senden"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"Text senden"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"Abbrechen"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"Verbindung abbrechen"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"Jede Komponente unterliegt der aufgeführten Open-Source-Lizenz und die gewährten Rechte unterliegen nicht den allgemeinen Nutzungsbedingungen dieser Anwendung. Für die Anzeige des Quellcodes ist eine Internetverbindung erforderlich."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"Insgesamt"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"Zustimmen und fortfahren"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"Bleiben Sie im Hintergrund verbunden. Klicken Sie, um zum Remote-Desktop zurückzukehren"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"Hintergrund-Keepalive konnte nicht gestartet werden. Bitte lassen Sie die Anwendung im Vordergrund"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"Hintergrund-Keepalive konnte nicht gestartet werden. Bitte lassen Sie die Anwendung im Vordergrund und stellen Sie die Verbindung erneut her."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"Die Hintergrundverbindung wurde beendet. Bitte stellen Sie die Verbindung wieder her."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"Der Hintergrundverbindungsdienst wurde beendet. Bitte stellen Sie die Verbindung wieder her."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"Neue Vorschauen werden bei nachfolgenden Verbindungen weiterhin gespeichert."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"Online"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"Geben Sie den Hostnamen oder die IPv4-Adresse ein, ohne Protokoll und Pfad"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"Ton"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"Remote-Text kopieren"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"Behalten Sie immer eine höhere Auflösung bei und reduzieren Sie die Bildrate, um die Bildschärfe zu gewährleisten, wenn die Bandbreite nicht ausreicht."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"Medienverschlüsselung"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"Buchstaben & Zahlen"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"Der sichere Speicher oder die Initialisierung des Zertifikats ist fehlgeschlagen. Bitte versuchen Sie es erneut oder löschen Sie die Anwendungsdaten"</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"Fertig"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"Sie können nach Abschluss der Datenschutzautorisierung nach Updates suchen"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"Registrieren Sie die lokale Identität, nachdem Sie die Datenschutzautorisierung abgeschlossen haben"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"Das Speichern des Passworts ist fehlgeschlagen. Diese Verbindung kann weiterhin verwendet werden"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"Passwörter werden verschlüsselt im nativen Android Keystore gespeichert"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"Remote-ID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"Peer-ID "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"Peer-ID {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"Alle auf diesem Computer gespeicherten Remote-Vorschauen werden gelöscht und die Verbindungsdatensätze und Passwörter bleiben erhalten."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"Bildschirmausrichtung"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"Fernbedienungsmenü erweitern"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"Remote-Verbindung wegen Benachrichtigung getrennt"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"Passwort gespeichert"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"Das gespeicherte Passwort kann nicht gelesen werden, bitte geben Sie es erneut ein."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"Das gespeicherte Passwort kann nicht beibehalten werden, bitte geben Sie es erneut ein."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del gesendet"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"Hintergrund-Keep-Alive ist aktiviert, wechselnde Anwendungen können die Verbindung weiterhin aufrechterhalten"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"Hintergrund-Keepalive aktiviert, um Verbindungsbenachrichtigungen zurückzugeben oder zu trennen"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"Zugegeben"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"Lesen"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"Verbunden "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"Mit Server verbunden"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"Bildrate"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"Flüssig"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"Balance"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"Die Lizenzdatei in der Anwendung fehlt oder ist beschädigt. Bitte installieren Sie die Anwendung neu."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"Die Entwicklungsversion enthält Änderungen, die noch nicht veröffentlicht wurden. Der folgende Link ist der Quellcode der Basisversion."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"Speichern aktivieren"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"Privatsphärenschutz aktivieren"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"Open-Source-Komponenten"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"Beschreibung der Open-Source-Softwarerechte"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"Dies ist derzeit die neueste Version"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"Derzeit unterstützt die Gegenstelle keine direkte Eingabe dieses Textes. Bitte verwenden Sie eine Version des gesteuerten Endes, die die Texteingabe unterstützt, oder senden Sie sie über die Zwischenablage."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"Nach erfolgreicher Verbindung wird hier das nächstgelegene Remote-Gerät angezeigt."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"Ich habe die Datenschutzerklärung gelesen und bin damit einverstanden"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"Ausgewählte Datei (kann nicht gelesen werden oder nicht genügend Speicherplatz)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"Die Geräte-ID wurde nicht gefunden"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"Das Senden des Befehls ist fehlgeschlagen. Bitte versuchen Sie es erneut"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"Der Befehl wurde gesendet und wartet auf die Bestätigung des Computers ..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"Autorisierungsstatus"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"Empfangen"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"erhalten "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"Lokaler Decoder"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"Widerrufen"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"Nach dem Abheben wird die Fernverbindung beendet, die Bildschirmvorschau geschlossen und die bestehende Vorschau gelöscht. Der Verbindungsdatensatz und das Passwort bleiben erhalten und vor der erneuten Verbindung ist eine erneute Autorisierung erforderlich."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"Netzwerkberechtigung entziehen"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"Netzwerkberechtigung entziehen?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"Fernbedienungsmenü einklappen"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"Tastatur ausblenden"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"Maus ausblenden"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"Daten"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"Das Speichern der Datei ist fehlgeschlagen. Bitte versuchen Sie es erneut"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"Datei gespeichert"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"Zeitüberschreitung bei der Dateiauswahl und Sitzung beendet"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"Der Text ist größer als 128 KB"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"Trennen"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"Kehrt nach dem Trennen der Verbindung zur Startseite zurück"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"Remote-Verbindung trennen?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"Trennen"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"Neue Version"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"Neue Version verfügbar: v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"Neue Version verfügbar: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"Keine ungelesenen Ankündigungen"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"Lesestatus kann nicht lokal gespeichert werden. Bitte versuchen Sie es erneut."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"Diese Ankündigung kann nicht gelöscht werden. Bitte versuchen Sie es erneut."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"Remote-Verbindung konnte nicht initiiert werden"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"Open-Source-Lizenz kann nicht gelesen werden"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"Bildschirm"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"Passwort anzeigen"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"Jetzt nicht"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"Keine Ankündigungen"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"Letzte Verbindungen"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"Server"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"Server TLS certificate-Überprüfung fehlgeschlagen"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"Serveradresse"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"Die Serveranmeldung ist fehlgeschlagen. Bitte überprüfen Sie die Serveradresse und das Netzwerk"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"Server-Port"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"Zeitüberschreitung bei der Serververbindung. Bitte überprüfen Sie die Serveradresse und das Netzwerk"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"Nicht aktiviert"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"Es wurde kein Browser zum Öffnen des Links gefunden"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"Nicht autorisiert"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"Ungelesen"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"Server nicht verbunden · Klicken Sie, um es erneut zu versuchen"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"Diese Anwendung verwendet OpenFEC zur Vorwärtsfehlerkorrektur unterCeCILL-C1.0, was eingeschränkte Garantien und Haftungsbeschränkungen bietet. Einzelheiten finden Sie in der vollständigen Lizenz."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"Lokale ID "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"Lokale ID {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"Quellcode und Build-Anweisungen für diese Version"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"Die Quellrevision ist für diesen Build nicht verfügbar."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"Zum Aktualisieren loslassen"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"Bauanleitung"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"Erstellen Sie Rezepte und Projekt-Patches"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"Quellcode anzeigen"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"Prüfung fehlgeschlagen. Bitte versuchen Sie es später noch einmal."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"Suchen Sie nach Updates"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"Sichtschutz wird geschlossen…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"Sichere Verbindung wird hergestellt…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"Privatsphärenschutz einschalten..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"Suche nach Updates…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"Löschen..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"Lokale ID erhalten..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"Ausgewählte Datei wird gelesen..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"Verbinden"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"Verbindung zum Signalisierungsserver wird hergestellt..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"Verbindung zum Server herstellen..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"Verbindung zum Server wird wiederhergestellt..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"Für dieses Gerät ist keine Dateiauswahl verfügbar"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"Klar"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"Vorschaubilder löschen"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"Vorschaubilder löschen?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"Quellcode und Build-Anweisungen"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"Quellcode-Version"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"Version"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"Urheberrechts- und Lizenzerklärung"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"Computertastatur"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"Qualität"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"Videopräferenz"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"Videolatenz"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"Videoeinstellungen"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"Videoqualität"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"Bildrate der Videoaufnahme"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"Touchpad"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"Relative Maus"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"Verstanden"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"Okay"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"Bestätigen"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"Verbindung bestätigen"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"Offline"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"Leertaste"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"Der Portbereich liegt zwischen 1 und 65535"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"Symbole & Tasten"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"Warten Sie, bis der Computer in den Datenschutzstatus zurückkehrt. Ältere Computer unterstützen dies möglicherweise nicht."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"Warten auf Remote-Bildschirminformationen"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"Warten auf den Remote-Bildschirm ..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"Datenschutzzustimmung ausstehend"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"Systemtastatur"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"Direkte Berührung"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"Absolute Maus"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"Netzwerk"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"Benutzerdefiniert"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"Virtuelle Maus"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"Der Remote-Datenschutzbildschirm wurde über die lokale Verknüpfung entsperrt. Sperren Sie es erneut, um die Privatsphäre wiederherzustellen und das Entsperren lokaler Verknüpfungen zu deaktivieren."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"Die kontrollierte Version ist zu niedrig, bitte aktualisieren Sie auf die neueste Version."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"Remote-Encoder"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"Text, der an die Gegenstelle gesendet werden soll"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"Video"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"Lizenz"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"Lizenzen und Urheberrechtshinweise"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"Die Lizenz- und Urheberrechtshinweise sind offline verfügbar."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"Die Geräteidentität konnte nicht gespeichert werden. Bitte überprüfen Sie den sicheren Speicher"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"Geräteidentität und ausgewählte gespeicherte Zugangskennwörter werden mit Android Keystore verschlüsselt. Verbindungsdatensätze werden nur lokal gespeichert und sind nicht in der Gerätesicherung enthalten. Der Inhalt der Zwischenablage wird gelöscht, wenn die Sitzung beendet wird. Bleiben Sie mit Bild-in-Bild- oder verbundenen Benachrichtigungen in der Sitzung, nachdem Sie die App verlassen haben, und trennen Sie die Verbindung innerhalb der App oder der Benachrichtigungen."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"Einstellungen"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"Einstellungen, es gibt eine neue Version"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"Zugangspasswort"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"Das Zugangspasswort ist falsch, bitte geben Sie es erneut ein."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"Zertifikatsüberprüfung fehlgeschlagen · Klicken Sie, um es erneut zu versuchen"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"Bitte füllen Sie zunächst die Online-Autorisierung aus"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"Bitte aktivieren Sie den CrossDesk-Systemdienst auf der Gegenseite und versuchen Sie es erneut"</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"Die Anfrage ist abgelaufen und der Datenschutzstatus des Computers wurde nicht bestätigt."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"Bitte warten Sie, bis die aktuelle Datei gesendet wird."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"Bitte geben Sie einen Port im Bereich 1–65535 ein"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"Geben Sie eine 9-stellige Geräte-ID ein"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"Bitte geben Sie ein gültiges Zugangspasswort ein"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"Einstellungen konnten nicht angewendet werden. Versuchen Sie es erneut."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"Softwarelizenz"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"Zurück"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"Zurück zur Steuerleiste"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"Noch kein Verbindungsdatensatz"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"Diese Ankündigung wurde aktualisiert oder gelöscht. Bitte kehren Sie zur Ankündigungsliste zurück, um sie anzuzeigen."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"Remote-Aufgabe ist geschlossen"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"Remotedesktop"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"Remote-Desktop-Touchbereich"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"Remote-Desktop: Berühren und klicken, zum Verschieben ziehen, die rechte Taste gedrückt halten, zum Zoomen zusammenziehen, hineinzoomen und den Bildschirm mit zwei Fingern ziehen, mit zwei Fingern nach oben und unten streichen, um im Originalmaßstab zu scrollen"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"Remote-Computer"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"Das Remote-Fenster wurde geschlossen oder der Hintergrund-Keepalive konnte nicht gestartet werden und die Sitzung wurde beendet."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"Remote-Verbindung"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"Falsches Remote-Passwort"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"Die Gegenstelle hat die Verbindung geschlossen"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"Die Gegenstelle ist derzeit nicht verfügbar"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"Das Passwort für den Fernzugriff ist abgelaufen. Bitte verbinden Sie sich erneut."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"Verbinden"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"Verbindung "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"Verbindung fehlgeschlagen"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"Verbindung fehlgeschlagen · Klicken Sie, um es erneut zu versuchen"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"Verbindung verloren"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"Verbindung beendet"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"Verbindungsverzögerung (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"Verfügbar bei Verbindung mit einem unterstützten Remote-Gerät."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"Querformatbildschirm sperren, nachdem die Verbindung hergestellt wurde"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"Verbindungsaufforderung"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"Verbindungsmethode"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"Mit dem Server verbinden"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"Stellen Sie eine Verbindung zum Server her, um die neuesten Ankündigungen zu erhalten."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"Zeitüberschreitung bei der Verbindung. Bitte überprüfen Sie die Geräte-ID, den Server und den Remote-Online-Status"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"Mit Remote-Desktop verbinden"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"Rücktaste"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"Mitteilungen"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"Einige Vorschauen konnten nicht gelöscht werden. Bitte versuchen Sie es erneut."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"Versuchen Sie es erneut"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"Versuchen Sie erneut, den Lesestatus zu speichern"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"Sperren"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"Tastatur"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"Lesen und autorisieren"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"Datenschutz"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"Datenschutz und Autorisierung"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"Sichtschutz"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"Sichtschutz aus"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"Sichtschutz an"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"Datenschutzbildschirm entsperrt"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"Anomalie des Sichtschutzbildschirms"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"Der Datenschutzbildschirm ist vorübergehend nicht verfügbar"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"Der Status des Datenschutzbildschirms ist abgelaufen und wartet auf Computerbestätigung"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"Status des Datenschutzbildschirms muss bestätigt werden"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"Datenschutzrichtlinie"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"Die Datenschutzerklärung kann vorübergehend nicht gelesen werden. Bitte verweigern Sie die Verbindung zum Internet und installieren Sie die Anwendung neu."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"Stumm"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"Audio"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"Projektwebsite"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"Vorschauen können persönliche oder geschäftliche Informationen vom Remote-Computer enthalten. Nach dem Einschalten speichert jede Verbindung automatisch ein Bild auf dem Gerät und zeigt es in den letzten Verbindungen an. Sie können jederzeit schließen und löschen."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"Hoch"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"Standard"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"Standardmäßig deaktiviert. Wenn es eingeschaltet ist, wird der Bildschirm angezeigt und bleibt im Querformat, wenn das Telefon angeschlossen ist. Wenn es ausgeschaltet ist, folgt es der Richtung des Geräts. Tablets und Multi-Windows sind nicht betroffen."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"Rechte Maustaste, drücken und ziehen, loslassen, um das Ziehen zu beenden"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"Linke Maustaste, drücken und ziehen, loslassen, um das Ziehen zu beenden"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"Maussteuerung"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"Mausrad, nach oben und unten schieben, um aus dem Tastenbereich zu gleiten"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", Paketverlustrate "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", senden "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", Passwort gespeichert"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", empfangen "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">", klicken Sie auf Wiederholen"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · Completado"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" Transferencia fallida"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" dispositivos"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" Artículo · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" elementos no leídos"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"Conexión remota CrossDesk"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL versión 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"Aviso OpenFEC"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"Licencia y fuente OpenFEC"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"Conexión directa P2P"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP habilitado"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"Relé de GIRO"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"Error en la transmisión {0}"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"Dispositivos: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0} elementos no leídos"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, recepción de {1}, envío de {2}, tasa de pérdida de paquetes {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● Conectado"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"Tire hacia abajo para actualizar"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"Tasa de pérdidas"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"Media"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"Da prioridad a mantener la imagen fluida y reducirá la resolución de manera más agresiva cuando el ancho de banda o la presión de codificación sean altos."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"Baja"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"Usando un servidor personalizado"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"Guardar contraseña"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"Guardar archivo recibido"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"Guardar vistas previas de pantalla remotas"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"¿Guardar vista previa de pantalla remota?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"La conexión de señalización está interrumpida, vuelva a conectarse."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"Error al cargar el anuncio, inténtelo de nuevo."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"Detalles del anuncio"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"totales "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"Total de elementos {0}· Elementos no leídos {1}"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"Acerca de"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"Acerca de CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"Acerca de, hay una nueva versión."</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"Cerrar"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"Cerrar barra de control"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"Desactivar la pantalla de privacidad"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"Equilibra la claridad y la suavidad, reduciendo solo gradualmente la resolución bajo presión constante."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"Resolución"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"Eliminar"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"Eliminar anuncio"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"Eliminar registro"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"¿Eliminar este anuncio?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"Actualizar anuncio"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"Portapapeles"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"Cargando…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"Cargar más"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"Actualización recomendada"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"Falló la inicialización de la conexión nativa"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"Enviar"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"Enviar "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"Después de enviar, haga clic en \"Pegar\" en el teclado remoto. El texto remoto recibido se puede copiar manualmente a la máquina local."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"Enviar archivo"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"Enviar texto"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"Cancelar"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"Cancelar conexión"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"Cada componente está sujeto a la licencia de código abierto enumerada y los derechos otorgados no están sujetos a los términos generales de uso de esta aplicación. Para ver el código fuente se requiere una conexión a Internet."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"Total"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"Aceptar y continuar"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"Manténgase conectado en segundo plano, haga clic para regresar al escritorio remoto"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"No se pudo iniciar el mantenimiento en segundo plano; mantenga la aplicación en primer plano"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"No se pudo iniciar el mantenimiento en segundo plano. Mantenga la aplicación en primer plano y vuelva a conectarse."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"La conexión en segundo plano ha finalizado; vuelva a conectarse."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"El servicio de conexión en segundo plano se detuvo; vuelva a conectarse."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"Las nuevas vistas previas aún se guardarán en conexiones posteriores."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"En línea"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"Complete el nombre del host o la dirección IPv4, sin protocolo ni ruta"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"Sonido"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"Copiar texto remoto"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"Mantenga siempre una resolución más alta y reduzca la velocidad de fotogramas para garantizar la claridad de la imagen cuando el ancho de banda sea insuficiente."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"Cifrado de medios"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"Letras y números"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"Error en el almacenamiento seguro o en la inicialización del certificado. Inténtelo de nuevo o borre los datos de la aplicación."</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"Hecho"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"Puede buscar actualizaciones después de completar la autorización de privacidad"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"Registre la identidad local después de completar la autorización de privacidad"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"Error al guardar la contraseña, esta conexión aún se puede utilizar"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"Las contraseñas se guardan cifradas en el almacén de claves nativo de Android"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"Identificación remota"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"Identificación de pares "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"ID de igual {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"Se eliminarán todas las vistas previas remotas guardadas en esta máquina y se conservarán los registros de conexión y las contraseñas."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"Orientación de la pantalla"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"Ampliar el menú del control remoto"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"Conexión remota desconectada de la notificación"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"Contraseña guardada"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"La contraseña guardada no se puede leer; vuelva a ingresarla."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"La contraseña guardada no se puede conservar; vuelva a ingresarla."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del enviado"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"Mantener vivo en segundo plano está habilitado, cambiar de aplicación puede continuar manteniendo la conexión"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"Keepalive en segundo plano habilitado para regresar o desconectarse de las notificaciones de conexión"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"Concedido"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"Leer"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"Conectado "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"Conectado al servidor"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"Velocidad de fotogramas"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"Fluidez"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"Equilibrado"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"El archivo de licencia de la aplicación falta o está dañado; reinstale la aplicación."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"La versión de desarrollo contiene modificaciones que aún no han sido publicadas. El siguiente enlace es el código fuente de la versión básica."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"Activar guardado"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"Habilitar pantalla de privacidad"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"Componentes de código abierto"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"Descripción de los derechos del software de código abierto"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"Esta es actualmente la última versión."</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"Actualmente, el extremo remoto no admite la entrada directa de este texto. Utilice una versión del extremo controlado que admita la entrada de texto o envíela a través del portapapeles."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"Tras una conexión exitosa, aquí se mostrará el dispositivo remoto más cercano."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"He leído y acepto la Política de Privacidad"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"Archivo seleccionado (no se puede leer o no hay suficiente espacio)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"No se encontró el ID del dispositivo"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"Error al enviar el comando, inténtelo de nuevo"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"El comando ha sido enviado, esperando confirmación por computadora..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"Estado de autorización"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"Recibir"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"recibir "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"Decodificador local"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"Retirar"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"Después de retirarse, se detendrá la conexión remota, se cerrará la vista previa de la pantalla y se borrará la vista previa existente. El registro de conexión y la contraseña se conservarán y se requerirá autorización nuevamente antes de volver a conectarse."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"Retirar la autorización de red"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"¿Retirar la autorización de la red?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"Contraer menú de control remoto"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"Ocultar teclado"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"Ocultar ratón"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"Datos"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"Error al guardar el archivo, inténtelo de nuevo"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"Archivo guardado"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"Se agotó el tiempo de selección de archivos y la sesión finalizó"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"El texto supera los 128 KB"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"Desconectar"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"Volverá a la página de inicio después de desconectarse"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"¿Desconectar la conexión remota?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"Desconectar"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"Nueva versión"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"Nueva versión disponible :v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"Nueva versión disponible: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"No hay anuncios no leídos"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"No se puede guardar el estado de lectura localmente. Por favor inténtalo de nuevo."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"No se puede eliminar este anuncio. Por favor inténtalo de nuevo."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"No se puede iniciar la conexión remota"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"No se puede leer la licencia de código abierto"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"Pantalla"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"Mostrar contraseña"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"Ahora no"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"Sin anuncios"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"Conexiones recientes"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"Servidor"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"Error en la verificación del certificado Server TLS"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"Dirección del servidor"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"Error al iniciar sesión en el servidor; verifique la dirección y la red del servidor"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"Puerto del servidor"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"Se agotó el tiempo de espera de la conexión del servidor; verifique la dirección y la red del servidor."</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"No habilitado"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"No se encontró ningún navegador para abrir el enlace."</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"No autorizado"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"No leído"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"Servidor no conectado · Haga clic para intentar nuevamente"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"Esta aplicación utiliza OpenFEC para la corrección de errores directos bajoCeCILL-C1.0, que proporciona garantías limitadas y limita la responsabilidad. Consulte la licencia completa para obtener más detalles."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"Identificación local "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"ID local {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"Código fuente e instrucciones de compilación para esta versión"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"La revisión fuente no está disponible para esta compilación."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"Suelte para actualizar"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"Instrucciones de construcción"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"Crear recetas y parches de proyectos"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"Ver código fuente"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"La verificación falló. Inténtelo de nuevo más tarde."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"Buscar actualizaciones"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"Cerrando la pantalla de privacidad..."</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"Estableciendo una conexión segura..."</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"Activando la pantalla de privacidad..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"Buscando actualizaciones..."</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"Borrando..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"Obtención de identificación local..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"Leyendo el archivo seleccionado..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"Conexión"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"Conectándose al servidor de señalización..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"Conectándose al servidor..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"Reconectándose al servidor..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"No hay selector de archivos disponible para este dispositivo"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"Borrar"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"Borrar vistas previas"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"¿Borrar vistas previas?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"Código fuente e instrucciones de compilación"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"Versión del código fuente"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"Versión"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"Declaración de derechos de autor y licencia"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"Teclado de computadora"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"Calidad"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"Preferencia de vídeo"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"Latencia de vídeo"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"Configuración de vídeo"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"Calidad de vídeo"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"Velocidad de fotogramas de captura de vídeo"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"Panel táctil"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"Ratón relativo"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"Entendido"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"Aceptar"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"Confirmar"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"Confirmar conexión"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"Sin conexión"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"Espacio"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"El rango de puertos es 1–65535"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"Símbolos y teclas"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"Espere a que la computadora vuelva al estado de pantalla de privacidad; Es posible que las computadoras más antiguas no lo admitan."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"Esperando información de la pantalla remota"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"Esperando la pantalla remota..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"Esperando autorización de privacidad"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"Teclado del sistema"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"Toque directo"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"Ratón absoluto"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"Red"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"Personalizado"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"Ratón virtual"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"La pantalla de privacidad remota se desbloqueó usando el acceso directo local. Bloquéelo nuevamente para restaurar la privacidad y deshabilitar el desbloqueo de accesos directos locales."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"La versión controlada es demasiado baja; actualice a la última versión."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"Codificador remoto"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"Texto a enviar al extremo remoto"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"Vídeo"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"Licencia"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"Licencias y avisos de derechos de autor"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"La licencia y el aviso de derechos de autor están disponibles sin conexión."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"No se pudo guardar la identidad del dispositivo; verifique el almacenamiento seguro"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"La identidad del dispositivo y las contraseñas de acceso guardadas seleccionadas se cifran mediante Android Keystore. Los registros de conexión solo se guardan localmente y no se incluyen en la copia de seguridad del dispositivo. El contenido del portapapeles se borra cuando finaliza la sesión. Manténgase en sesión con imagen en imagen o notificaciones conectadas después de salir de la aplicación y desconéctese dentro de la aplicación o las notificaciones."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"Ajustes"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"Configuración, hay una nueva versión."</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"Contraseña de acceso"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"La contraseña de acceso es incorrecta, por favor vuelva a ingresarla."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"Error en la verificación del certificado · Haga clic para intentar nuevamente"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"Primero complete la autorización en línea"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"Habilite el servicio del sistema CrossDesk en el extremo remoto y vuelva a intentarlo."</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"Se agotó el tiempo de espera de la solicitud y no se ha confirmado el estado de la pantalla de privacidad de la computadora."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"Espere a que se envíe el archivo actual."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"Ingrese un puerto en el rango 1–65535"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"Ingrese una ID de dispositivo de 9 dígitos"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"Por favor ingrese una contraseña de acceso válida"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"No se pudieron aplicar las configuraciones. Intentar otra vez."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"Licencia de software"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"Volver"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"Volver a la barra de control"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"Aún no hay registro de conexión"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"Este anuncio ha sido actualizado o eliminado, regrese a la lista de anuncios para verlo."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"La tarea remota está cerrada"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"Escritorio remoto"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"Área táctil del escritorio remoto"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"Escritorio remoto: toque y haga clic, arrastre para mover, presione y mantenga presionado el botón derecho, pellizque para hacer zoom, acerque y arrastre la pantalla con dos dedos, deslice hacia arriba y hacia abajo con dos dedos para desplazarse a la escala original"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"Computadora remota"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"La ventana remota se cerró o el mantenimiento en segundo plano no se pudo iniciar y la sesión finalizó."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"Conexión remota"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"Contraseña remota incorrecta"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"El extremo remoto ha cerrado la conexión"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"El extremo remoto no está disponible actualmente"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"La contraseña de acceso remoto ha caducado, vuelva a conectarse."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"Conectar"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"Conexión "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"Error de conexión"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"Error de conexión · Haga clic para intentarlo de nuevo"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"Conexión perdida"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"Conexión finalizada"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"Retardo de conexión (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"Disponible cuando se conecta a un dispositivo remoto compatible."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"Bloquear la pantalla horizontal después de establecer la conexión"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"Aviso de conexión"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"Método de conexión"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"Conectarse al servidor"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"Conéctese al servidor para obtener los últimos anuncios."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"Se agotó el tiempo de conexión; verifique el ID del dispositivo, el servidor y el estado en línea remoto"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"Conectarse al escritorio remoto"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"Retroceso"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"Anuncios"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"Algunas vistas previas no se pudieron borrar, inténtalo de nuevo."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"Inténtalo de nuevo"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"Reintentar guardar el estado de lectura"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"Bloquear"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"Teclado"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"Leer y autorizar"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"Privacidad"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"Privacidad y autorización"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"Pantalla de privacidad"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"Privacidad desactivada"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"Privacidad activada"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"Pantalla de privacidad desbloqueada"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"Anormalidad en la pantalla de privacidad"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"La pantalla de privacidad no está disponible temporalmente"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"El estado de la pantalla de privacidad ha caducado, esperando confirmación por computadora"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"Estado de la pantalla de privacidad por confirmar"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"Política de privacidad"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"La política de privacidad no se puede leer temporalmente. Rechace conectarse a Internet y reinstale la aplicación."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"Silencio"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"Audio"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"Sitio web del proyecto"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"Las vistas previas pueden contener información personal o laboral de la computadora remota. Después de encenderlo, cada conexión guardará automáticamente una imagen en la máquina y la mostrará en las conexiones recientes. Puedes cerrar y borrar en cualquier momento."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"Alta"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"Predeterminado"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"Desactivado de forma predeterminada. Cuando esté encendido, la pantalla entrará y permanecerá en orientación horizontal cuando el teléfono esté conectado; cuando esté apagado, seguirá la dirección del dispositivo. Las tabletas y las ventanas múltiples no se ven afectadas."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"Botón derecho del mouse, presione y arrastre, suelte para finalizar el arrastre"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"Botón izquierdo del mouse, presione y arrastre, suelte para finalizar el arrastre"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"Control del ratón"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"Rueda del mouse, deslícela hacia arriba y hacia abajo para salir del rango de teclas"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", tasa de pérdida de paquetes "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", enviar "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", contraseña guardada"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", recibir "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">", haga clic en Reintentar"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · Terminé"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" Échec du transfert"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" appareils"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" Article · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" éléments non lus"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"Connexion à distance CrossDesk"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL version 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"Avis OpenFEC"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"Licence et source OpenFEC"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"Connexion directe P2P"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP activé"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"Relais TOUR"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"Échec de la transmission {0}"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"Appareils : {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"Éléments non lus {0}"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, réception de {1}, envoi de {2}, taux de perte de paquets {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● Connecté"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"Déroulez pour actualiser"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"Taux de perte"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"Moyenne"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"Donne la priorité au maintien de la fluidité de l\'image et réduira la résolution de manière plus agressive lorsque la bande passante ou la pression d\'encodage est élevée."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"Faible"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"Utilisation d\'un serveur personnalisé"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"Enregistrer le mot de passe"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"Enregistrer le fichier reçu"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"Enregistrer les aperçus de l\'écran distant"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"Enregistrer l\'aperçu de l\'écran distant ?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"La connexion de signalisation est interrompue, veuillez vous reconnecter."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"Le chargement de l\'annonce a échoué, veuillez réessayer."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"Détails de l\'annonce"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"Total "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"Total des éléments {0}· Éléments non lus {1}"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"À propos"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"À propos de CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"À propos, il existe une nouvelle version"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"Fermer"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"Fermer la barre de contrôle"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"Désactiver l\'écran de confidentialité"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"Équilibre la clarté et la douceur, en réduisant seulement progressivement la résolution sous une pression constante."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"Résolution"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"Supprimer"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"Supprimer l\'annonce"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"Supprimer l\'enregistrement"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"Supprimer cette annonce ?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"Actualiser l\'annonce"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"Presse-papiers"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"Chargement…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"Charger plus"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"Mise à jour conseillée"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"L\'initialisation de la connexion native a échoué"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"Envoi"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"Envoyer "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"Après l\'envoi, cliquez sur \"Coller\" sur le clavier distant. Le texte distant reçu peut être copié manuellement sur la machine locale."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"Envoyer le fichier"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"Envoyer un SMS"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"Annuler"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"Annuler la connexion"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"Chaque composant est soumis à la licence open source répertoriée, et les droits accordés ne sont pas soumis aux conditions générales d\'utilisation de cette application. La visualisation du code source nécessite une connexion Internet."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"Total"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"Accepter et continuer"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"Restez connecté en arrière-plan, cliquez pour revenir au bureau distant"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"Échec du démarrage de Keepalive en arrière-plan, veuillez conserver l\'application au premier plan"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"L\'application Keepalive en arrière-plan n\'a pas pu démarrer. Veuillez conserver l\'application au premier plan et vous reconnecter."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"La connexion en arrière-plan est terminée, veuillez vous reconnecter."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"Le service de connexion en arrière-plan s\'est arrêté, veuillez vous reconnecter."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"Les nouveaux aperçus seront toujours enregistrés lors des connexions ultérieures."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"En ligne"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"Remplissez le nom d\'hôte ou l\'adresse IPv4, sans protocole ni chemin"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"Son"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"Copier le texte distant"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"Maintenez toujours une résolution plus élevée et réduisez la fréquence d\'images pour garantir la clarté de l\'image lorsque la bande passante est insuffisante."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"Cryptage multimédia"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"Lettres et chiffres"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"Échec du stockage sécurisé ou de l\'initialisation du certificat. Veuillez réessayer ou effacer les données de l\'application."</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"Terminé"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"Vous pouvez vérifier les mises à jour après avoir terminé l\'autorisation de confidentialité"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"Enregistrez l\'identité locale après avoir terminé l\'autorisation de confidentialité"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"L\'enregistrement du mot de passe a échoué, cette connexion peut toujours être utilisée"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"Les mots de passe sont enregistrés cryptés dans le Keystore Android natif"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"ID à distance"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"Identification des pairs "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"ID homologue {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"Tous les aperçus à distance enregistrés sur cette machine seront supprimés et les enregistrements de connexion et les mots de passe seront conservés."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"Orientation de l\'écran"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"Développer le menu de la télécommande"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"Connexion à distance déconnectée de la notification"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"Mot de passe enregistré"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"Le mot de passe enregistré ne peut pas être lu, veuillez le saisir à nouveau."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"Le mot de passe enregistré ne peut pas être conservé, veuillez le saisir à nouveau."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del envoyé"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"Le maintien de l\'activité en arrière-plan est activé, les applications de commutation peuvent continuer à maintenir la connexion."</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"Keepalive en arrière-plan activé pour revenir ou se déconnecter des notifications de connexion"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"Accordé"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"Lire"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"Connecté "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"Connecté au serveur"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"Fréquence d\'images"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"Fluidité"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"Équilibré"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"Le fichier de licence dans l\'application est manquant ou endommagé, veuillez réinstaller l\'application."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"La version de développement contient des modifications qui n\'ont pas encore été publiées. Le lien suivant est le code source de la version de base."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"Activer l’enregistrement"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"Activer l\'écran de confidentialité"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"Composants open source"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"Description des droits sur les logiciels open source"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"Il s\'agit actuellement de la dernière version"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"Actuellement, l\'extrémité distante ne prend pas en charge la saisie directe de ce texte. Veuillez utiliser une version de l\'extrémité contrôlée qui prend en charge la saisie de texte ou envoyez-la via le presse-papiers."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"Une fois la connexion réussie, l\'appareil distant le plus proche sera affiché ici."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"J\'ai lu et accepté la politique de confidentialité"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"Fichier sélectionné (impossible en lecture ou espace insuffisant)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"L\'ID de l\'appareil n\'a pas été trouvé"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"L\'envoi de la commande a échoué, veuillez réessayer"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"La commande a été envoyée, en attente de confirmation informatique..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"Statut d\'autorisation"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"Réception"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"recevoir "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"Décodeur local"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"Retirer"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"Après le retrait, la connexion à distance sera arrêtée, l\'aperçu de l\'écran sera fermé et l\'aperçu existant sera effacé. L\'enregistrement de connexion et le mot de passe seront conservés et une autorisation sera à nouveau requise avant de se reconnecter."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"Retirer l\'autorisation réseau"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"Retirer l\'autorisation réseau ?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"Réduire le menu de la télécommande"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"Masquer le clavier"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"Masquer la souris"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"Données"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"L\'enregistrement du fichier a échoué, veuillez réessayer"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"Fichier enregistré"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"La sélection de fichier a expiré et la session s\'est terminée"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"Le texte dépasse 128 Ko"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"Déconnecter"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"Reviendra à la page d\'accueil après la déconnexion"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"Déconnecter la connexion à distance ?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"Déconnecter"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"Nouvelle version"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"Nouvelle version disponible : v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"Nouvelle version disponible : v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"Aucune annonce non lue"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"Impossible d\'enregistrer l\'état de lecture localement. Veuillez réessayer."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"Impossible de supprimer cette annonce. Veuillez réessayer."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"Impossible d\'établir une connexion à distance"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"Impossible de lire la licence open source"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"Écran"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"Afficher le mot de passe"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"Pas maintenant"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"Aucune annonce"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"Connexions récentes"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"Serveur"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"La vérification du certificat Server TLS a échoué"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"Adresse du serveur"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"Échec de la connexion au serveur, veuillez vérifier l\'adresse du serveur et le réseau"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"Port du serveur"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"La connexion au serveur a expiré, veuillez vérifier l\'adresse du serveur et le réseau"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"Non activé"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"Aucun navigateur trouvé pour ouvrir le lien"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"Non autorisé"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"Non lu"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"Serveur non connecté · Cliquez pour réessayer"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"Cette application utilise OpenFEC pour la correction d\'erreur directe sousCeCILL-C1.0, qui offre des garanties limitées et limite la responsabilité. Consultez la licence complète pour plus de détails."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"ID local "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"ID local {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"Code source et instructions de construction pour cette version"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"La révision source n\'est pas disponible pour cette version."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"Relâchez pour actualiser"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"Instructions de construction"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"Créer des recettes et des correctifs de projet"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"Afficher le code source"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"La vérification a échoué. Veuillez réessayer plus tard."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"Rechercher des mises à jour"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"Fermeture de l\'écran de confidentialité…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"Établir une connexion sécurisée…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"Activation de l\'écran de confidentialité..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"Vérification des mises à jour…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"Effacement..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"Obtention d\'une pièce d\'identité locale..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"Lecture du fichier sélectionné..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"Connexion"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"Connexion au serveur de signalisation..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"Connexion au serveur..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"Reconnexion au serveur..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"Aucun sélecteur de fichiers disponible pour cet appareil"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"Effacer"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"Supprimer les aperçus"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"Supprimer les aperçus ?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"Code source et instructions de construction"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"Version du code source"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"Version"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"Déclaration de droits d\'auteur et de licence"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"Clavier d\'ordinateur"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"Qualité"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"Préférence vidéo"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"Latence vidéo"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"Paramètres vidéo"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"Qualité vidéo"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"Fréquence d\'images de capture vidéo"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"Pavé tactile"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"Souris relative"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"Compris"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"D\'accord"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"Confirmer"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"Confirmer la connexion"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"Hors ligne"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"Espace"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"La plage de ports est comprise entre 1 et 65 535."</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"Symboles et touches"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"Attendez que l\'ordinateur revienne à l\'état de l\'écran de confidentialité ; les ordinateurs plus anciens peuvent ne pas le prendre en charge."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"En attente d\'informations sur l\'écran distant"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"En attente de l\'écran distant..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"En attente d\'une autorisation de confidentialité"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"Clavier système"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"Toucher direct"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"Souris absolue"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"Réseau"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"Personnalisé"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"Souris virtuelle"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"L\'écran de confidentialité à distance a été déverrouillé à l\'aide du raccourci local. Verrouillez-le à nouveau pour restaurer la confidentialité et désactiver le déverrouillage des raccourcis locaux."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"La version contrôlée est trop basse, veuillez mettre à niveau vers la dernière version."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"Encodeur déporté"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"Texte à envoyer à l\'extrémité distante"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"Vidéo"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"Licence"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"Licences et mentions de droits d\'auteur"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"La licence et la notice de copyright sont disponibles hors ligne."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"L\'identité de l\'appareil n\'a pas pu être enregistrée. Veuillez vérifier le stockage sécurisé."</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"L\'identité de l\'appareil et les mots de passe d\'accès enregistrés sélectionnés sont cryptés à l\'aide d\'Android Keystore. Les enregistrements de connexion sont uniquement enregistrés localement et ne sont pas inclus dans la sauvegarde de l\'appareil. Le contenu du presse-papiers est effacé à la fin de la session. Restez en session avec des notifications d\'image dans l\'image ou connectées après avoir quitté l\'application, et déconnectez-vous de l\'application ou des notifications."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"Réglages"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"Paramètres, il existe une nouvelle version"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"Mot de passe d\'accès"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"Le mot de passe d\'accès est erroné, veuillez le saisir à nouveau."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"Échec de la vérification du certificat · Cliquez pour réessayer"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"Veuillez d\'abord remplir l\'autorisation en ligne"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"Veuillez activer le service système CrossDesk sur l\'extrémité distante et réessayer."</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"Le délai de demande a expiré et l\'état de l\'écran de confidentialité de l\'ordinateur n\'a pas été confirmé."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"Veuillez attendre que le fichier actuel soit envoyé."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"Veuillez saisir un port compris entre 1 et 65535."</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"Saisissez un identifiant d\'appareil à 9 chiffres"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"Veuillez saisir un mot de passe d\'accès valide"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"Impossible d\'appliquer les paramètres. Essayer à nouveau."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"Licence du logiciel"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"Retour"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"Retour à la barre de contrôle"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"Aucun enregistrement de connexion pour l\'instant"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"Cette annonce a été mise à jour ou supprimée, veuillez revenir à la liste des annonces pour la consulter."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"La tâche distante est fermée"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"Bureau à distance"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"Zone tactile du bureau à distance"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"Bureau à distance : touchez et cliquez, faites glisser pour déplacer, appuyez et maintenez le bouton droit enfoncé, pincez pour zoomer, zoomez et faites glisser l\'écran avec deux doigts, faites glisser de haut en bas avec deux doigts pour faire défiler à l\'échelle d\'origine."</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"Ordinateur distant"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"La fenêtre distante a été fermée ou le keepalive en arrière-plan n\'a pas pu démarrer et la session est terminée."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"Connexion à distance"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"Mot de passe distant incorrect"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"L\'extrémité distante a fermé la connexion"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"L\'extrémité distante est actuellement indisponible"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"Le mot de passe d\'accès à distance a expiré, veuillez vous reconnecter."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"Connecter"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"Connexion "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"Échec de la connexion"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"Échec de la connexion · Cliquez pour réessayer"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"Connexion perdue"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"Connexion terminée"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"Délai de connexion (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"Disponible lors de la connexion à un appareil distant pris en charge."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"Verrouiller l\'écran paysage une fois la connexion établie"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"Invite de connexion"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"Méthode de connexion"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"Connectez-vous au serveur"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"Connectez-vous au serveur pour recevoir les dernières annonces."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"La connexion a expiré. Veuillez vérifier l\'ID de l\'appareil, le serveur et l\'état en ligne à distance."</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"Connectez-vous au bureau à distance"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"Retour arrière"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"Annonces"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"Certains aperçus n\'ont pas pu être effacés, veuillez réessayer."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"Réessayez"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"Réessayez d\'enregistrer l\'état de lecture"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"Verrouillage"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"Clavier"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"Lire et autoriser"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"Confidentialité"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"Confidentialité et autorisation"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"Écran de confidentialité"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"Confidentialité inactive"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"Confidentialité active"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"Écran de confidentialité déverrouillé"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"Anomalie de l\'écran de confidentialité"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"L\'écran de confidentialité est temporairement indisponible"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"L\'état de l\'écran de confidentialité a expiré, en attente de confirmation par l\'ordinateur"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"Statut de l\'écran de confidentialité à confirmer"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"Politique de confidentialité"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"La politique de confidentialité ne peut pas être lue temporairement. Veuillez refuser de vous connecter à Internet et réinstaller l\'application."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"Muet"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"Audio"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"Site Web du projet"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"Les aperçus peuvent contenir des informations personnelles ou professionnelles provenant de l\'ordinateur distant. Après l\'avoir allumé, chaque connexion enregistrera automatiquement une image sur la machine et l\'affichera dans les connexions récentes. Vous pouvez fermer et effacer à tout moment."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"Élevée"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"Par défaut"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"Désactivé par défaut. Lorsqu\'il est allumé, l\'écran entre et reste en orientation paysage lorsque le téléphone est connecté ; lorsqu\'il est éteint, il suivra la direction de l\'appareil. Les tablettes et multi-fenêtres ne sont pas concernées."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"Bouton droit de la souris, appuyez et faites glisser, relâchez pour terminer le glisser"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"Bouton gauche de la souris, appuyez et faites glisser, relâchez pour terminer le glisser"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"Contrôle de la souris"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"Molette de la souris, faites-la glisser de haut en bas pour sortir de la plage de touches"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", taux de perte de paquets "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", envoyer "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", mot de passe enregistré"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", recevoir "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">", cliquez sur Réessayer"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" ・完了"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" 転送に失敗しました"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" 台のデバイス"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" 記事 ・ "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" 未読項目"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"CrossDeskリモート接続"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL バージョン 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"OpenFEC通知"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"OpenFECライセンスとソース"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"P2P直接接続"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRT有効"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"ターンリレー"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"{0}送信失敗"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"デバイス数: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0}の未読アイテム"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}、受信{1}、送信{2}、パケットロス率{3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● 接続されました"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"プルダウンして更新します"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"損失率"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"中"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"画像を滑らかに保つことを優先し、帯域幅やエンコード圧力が高い場合には、より積極的に解像度を下げます。"</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"低"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"カスタムサーバーの使用"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"パスワードの保存"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"受信したファイルを保存する"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"リモート画面のプレビューを保存する"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"リモート画面のプレビューを保存しますか?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"信号接続が中断されました。再接続してください。"</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"お知らせを読み込めませんでした。再試行してください。"</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"発表内容"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"合計 "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"合計{0}個のアイテム ·{1}個の未読アイテム"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"概要"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"CrossDesk について"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"について、新しいバージョンがあります"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"閉じる"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"コントロールバーを閉じる"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"プライバシー画面を無効にする"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"明瞭さと滑らかさのバランスを保ち、一定の圧力下で解像度を徐々に低下させるだけです。"</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"解像度"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"削除"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"お知らせの削除"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"レコードの削除"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"このお知らせを削除しますか?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"リフレッシュのお知らせ"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"クリップボード"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"読み込み中…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"さらに読み込む"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"更新のご案内"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"ネイティブ接続の初期化に失敗しました"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"送信"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"送信 "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"送信後、リモートキーボードの「貼り付け」をクリックしてください。受信したリモート テキストはローカル マシンに手動でコピーできます。"</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"ファイル送信"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"テキストを送信する"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"キャンセル"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"接続をキャンセルする"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"各コンポーネントは、リストされているオープン ソース ライセンスの対象となり、付与された権利は、このアプリケーションの一般的な使用条件の対象ではありません。ソース コードを表示するには、インターネット接続が必要です。"</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"合計"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"同意して続行"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"バックグラウンドで接続を維持し、クリックしてリモート デスクトップに戻ります"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"バックグラウンドのキープアライブを開始できませんでした。アプリケーションをフォアグラウンドで維持してください。"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"バックグラウンド キープアライブの開始に失敗しました。アプリケーションをフォアグラウンドに保ち、再接続してください。"</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"バックグラウンド接続が終了しました。再接続してください。"</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"バックグラウンド接続サービスが停止しました。再接続してください。"</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"新しいプレビューはその後の接続でも保存されます。"</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"オンライン"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"プロトコルとパスを除いて、ホスト名または IPv4 アドレスを入力します。"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"サウンド"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"リモートテキストをコピーする"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"帯域幅が不十分な場合は、常に高い解像度を維持し、フレーム レートを下げて画像の鮮明さを確保します。"</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"メディアの暗号化"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"英数字"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"安全なストレージまたは証明書の初期化に失敗しました。再試行するか、アプリケーション データをクリアしてください。"</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"完了"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"プライバシー認証を完了すると、アップデートを確認できます"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"プライバシー認証完了後、ローカル ID を登録します"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"パスワードの保存に失敗しました。この接続はまだ使用できます"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"パスワードは暗号化されてネイティブ Android キーストアに保存されます"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"リモートID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"ピアID "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"ピアID{0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"本機に保存されているリモートプレビューはすべて削除され、接続記録とパスワードは保持されます。"</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"画面の向き"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"リモコンメニューを展開する"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"通知によりリモート接続が切断されました"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"パスワード保存済み"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"保存したパスワードが読み取れないため、再入力してください。"</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"保存したパスワードは保持できませんので、再度入力してください。"</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Delを送信しました"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"バックグラウンドキープアライブが有効になり、アプリケーションを切り替えて接続を維持し続けることができます。"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"バックグラウンドキープアライブを有効にして、接続通知を返すか接続を切断することができます"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"認められる"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"読む"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"接続されました "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"サーバーに接続しました"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"フレームレート"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"滑らかさ"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"バランス"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"アプリケーション内のライセンス ファイルが見つからないか破損しています。アプリケーションを再インストールしてください。"</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"開発バージョンには、まだリリースされていない修正が含まれています。以下のリンクは基本バージョンのソースコードです。"</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"保存を有効にする"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"プライバシー画面を有効にする"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"オープンソースコンポーネント"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"オープンソース ソフトウェアの権利の説明"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"これが現在の最新バージョンです"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"現在、リモート エンドはこのテキストの直接入力をサポートしていません。テキスト入力をサポートするバージョンのコントロール エンドを使用するか、クリップボード経由で送信してください。"</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"接続に成功すると、最も近いリモート デバイスがここに表示されます。"</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"プライバシーポリシーを読んで同意します"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"選択したファイル(読み込み不可、または容量不足)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"デバイスIDが見つかりませんでした"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"コマンドの送信に失敗しました。もう一度お試しください。"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"コマンドが送信され、コンピューターの確認を待っています..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"認可ステータス"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"受信"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"受け取る "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"ローカルデコーダ"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"撤回"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"退会後はリモート接続が停止され、画面プレビューが閉じられ、既存のプレビューが消去されます。接続記録とパスワードは保持され、再接続する前に再度認証が必要になります。"</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"ネットワーク認証の撤回"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"ネットワーク認証を取り消しますか?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"リモコンメニューを折りたたむ"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"キーボードを隠す"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"マウスを隠す"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"データ"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"ファイルの保存に失敗しました。もう一度お試しください。"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"ファイルを保存しました"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"ファイル選択がタイムアウトし、セッションが終了しました"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"テキストが128KBを超えています"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"切断"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"切断後ホームページに戻ります"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"リモート接続を切断しますか?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"切断"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"新しいバージョン"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"利用可能な新しいバージョン: v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"利用可能な新しいバージョン: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"未読のお知らせはありません"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"読み取りステータスをローカルに保存できません。もう一度試してください。"</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"このお知らせは削除できません。もう一度試してください。"</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"リモート接続を開始できません"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"オープンソースライセンスを読み取れません"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"ディスプレイ"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"パスワードを表示"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"今は同意しない"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"アナウンスはありません"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"最近の接続"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"サーバー"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"ServerTLS証明書の検証に失敗しました"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"サーバーアドレス"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"サーバーへのログインに失敗しました。サーバーのアドレスとネットワークを確認してください。"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"サーバーポート"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"サーバー接続がタイムアウトしました。サーバーのアドレスとネットワークを確認してください。"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"有効ではありません"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"リンクを開くブラウザが見つかりませんでした"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"無許可"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"未読"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"サーバーが接続されていません · クリックして再試行してください"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"このアプリケーションは、限定保証を提供し、責任を制限するCeCILL-C1.0 に基づく前方誤り訂正にOpenFECを使用します。詳細については、完全なライセンスを参照してください。"</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"ローカルID "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"ローカルID{0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"このバージョンのソース コードとビルド手順"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"このビルドではソース リビジョンが利用できません。"</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"放してリフレッシュ"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"組み立て説明書"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"ビルドレシピとプロジェクトパッチ"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"ソースコードを表示する"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"チェックに失敗しました。後でもう一度試してください。"</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"アップデートを確認する"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"プライバシー画面を閉じています…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"安全な接続を確立中…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"プライバシー画面を有効化中…"</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"アップデートを確認しています…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"クリア中..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"ローカルIDを取得中..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"選択したファイルを読み込み中..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"接続する"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"シグナリングサーバーに接続しています..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"サーバーに接続中..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"サーバーに再接続中..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"このデバイスでは使用できるファイル ピッカーがありません"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"クリア"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"プレビューを削除"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"プレビューを削除しますか?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"ソースコードとビルド手順"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"ソースコードのバージョン"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"バージョン"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"著作権およびライセンスに関する声明"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"コンピューターのキーボード"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"画質"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"ビデオ設定"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"映像遅延"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"ビデオ設定"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"ビデオ品質"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"ビデオキャプチャフレームレート"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"トラックパッド"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"相対マウス"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"分かりました"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"OK"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"確認する"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"接続を確認する"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"オフライン"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"スペース"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"ポート範囲は 1 ~ 65535 です。"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"記号・機能"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"コンピューターがプライバシー画面の状態に戻るまで待ちます。古いコンピューターではサポートされていない可能性があります。"</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"リモート画面情報待ち"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"リモート画面を待っています..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"プライバシー認証待ち"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"システムキーボード"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"直接タッチ"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"アブソリュートマウス"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"ネットワーク"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"カスタム"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"仮想マウス"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"ローカル ショートカットを使用してリモート プライバシー画面のロックが解除されました。再度ロックするとプライバシーが回復され、ローカル ショートカットのロック解除が無効になります。"</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"管理されているバージョンが低すぎるため、最新バージョンにアップグレードしてください。"</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"リモートエンコーダー"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"相手側に送信するテキスト"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"ビデオ"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"ライセンス"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"ライセンスと著作権表示"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"ライセンスと著作権表示はオフラインでも利用できます。"</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"デバイス ID を保存できませんでした。安全なストレージを確認してください。"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"デバイス ID と選択された保存されたアクセス パスワードは、Android キーストアを使用して暗号化されます。接続記録はローカルにのみ保存され、デバイスのバックアップには含まれません。クリップボードの内容はセッションが終了するとクリアされます。アプリを終了した後もピクチャーインピクチャーまたは接続された通知でセッションを維持し、アプリまたは通知内で切断します。"</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"設定"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"設定、新しいバージョンがあります"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"アクセスパスワード"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"アクセスパスワードが間違っていますので、再度入力してください。"</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"証明書の検証に失敗しました · クリックして再試行してください"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"最初にオンライン認証を完了してください"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"リモート エンドでCrossDeskシステム サービスを有効にして、再試行してください。"</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"リクエストがタイムアウトし、コンピュータのプライバシー画面の状態が確認できませんでした。"</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"現在のファイルが送信されるまでお待ちください。"</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"1 ~ 65535 の範囲でポートを入力してください"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"9桁のデバイスIDを入力"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"有効なアクセスパスワードを入力してください"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"設定を適用できませんでした。もう一度やり直してください。"</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"ソフトウェアライセンス"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"戻る"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"コントロールバーに戻る"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"まだ接続実績がありません"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"このお知らせは更新または削除されました。表示するにはお知らせリストに戻ってください。"</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"リモートタスクが終了しました"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"リモートデスクトップ"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"リモートデスクトップタッチエリア"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"リモート デスクトップ: タッチしてクリック、ドラッグして移動、右ボタンを押したまま、ピンチしてズーム、ズームインして 2 本の指で画面をドラッグ、2 本の指で上下にスライドして元のスケールでスクロール"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"リモートコンピュータ"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"リモート ウィンドウが閉じられたか、バックグラウンド キープアライブの開始に失敗し、セッションが終了しました。"</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"リモート接続"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"リモートパスワードが正しくありません"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"リモートエンドが接続を閉じました"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"リモートエンドは現在利用できません"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"リモートアクセスパスワードの有効期限が切れています。再接続してください。"</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"接続"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"接続 "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"接続に失敗しました"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"接続に失敗しました · クリックして再試行してください"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"接続が失われました"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"接続が終了しました"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"接続遅延 (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"サポートされているリモート デバイスに接続されている場合に使用できます。"</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"接続完了後、横画面をロックする"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"接続プロンプト"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"接続方法"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"サーバーに接続する"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"サーバーに接続して最新のお知らせを取得します。"</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"接続がタイムアウトしました。デバイス ID、サーバー、リモートのオンライン ステータスを確認してください。"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"リモートデスクトップに接続する"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"バックスペース"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"お知らせ"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"一部のプレビューをクリアできませんでした。もう一度お試しください。"</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"もう一度お試しください"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"読み取りステータスの保存をリトライする"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"ロック"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"キーボード"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"読んで承認する"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"プライバシー"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"プライバシーと認証"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"プライバシー画面"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"プライバシー画面:オフ"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"プライバシー画面:オン"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"プライバシー画面のロックが解除されました"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"プライバシー画面異常"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"プライバシー画面が一時的に利用できなくなります"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"プライバシー画面のステータスが期限切れになり、コンピュータの確認を待っています"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"プライバシー画面のステータスを確認する"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"プライバシーポリシー"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"一時的にプライバシーポリシーが閲覧できなくなります。インターネットへの接続を拒否し、アプリケーションを再インストールしてください。"</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"ミュート"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"オーディオ"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"プロジェクトWebサイト"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"プレビューには、リモート コンピューターからの個人情報または仕事情報が含まれる場合があります。オンにすると、接続するたびにマシンに写真が自動的に保存され、最近の接続で表示されます。いつでも閉じてクリアできます。"</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"高"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"デフォルト"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"デフォルトではオフです。オンにすると、電話が接続されているときに画面が横向きになり、横向きのままになります。オフにすると、デバイスの方向に従います。タブレットやマルチウィンドウは影響を受けません。"</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"マウスの右ボタンを押してドラッグし、放してドラッグを終了します"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"マウスの左ボタンを押してドラッグし、放してドラッグを終了します"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"マウスコントロール"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"マウスホイール、上下にスライドしてキー範囲外にスライドします"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">"、パケットロス率 "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">"、送信します "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">"、パスワードが保存されました"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">"、受け取ります "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">"、「再試行」をクリックします"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · 완료"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" 전송 실패"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" 장치"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" 기사 · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" 읽지 않은 항목"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"CrossDesk원격접속"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL 버전 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"OpenFEC공지"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"OpenFEC라이선스 및 소스"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"P2P 직접 연결"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP 활성화됨"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"TURN 릴레이"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"{0}전송 실패"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"기기: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0}읽지 않은 항목"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, 수신{1}, 송신{2}, 패킷 손실률{3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● 연결됨"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"아래로 당겨 새로고침"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"손실률"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"중간"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"영상을 부드럽게 유지하는 데 우선 순위를 두며 대역폭이나 인코딩 압력이 높을 때 해상도를 더욱 적극적으로 줄입니다."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"낮음"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"커스텀 서버 이용하기"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"비밀번호 저장"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"수신된 파일 저장"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"원격 화면 미리보기 저장"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"원격 화면 미리보기를 저장하시겠습니까?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"신호 연결이 중단되었습니다. 다시 연결하십시오."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"공지사항 로딩에 실패했습니다. 다시 시도해주세요."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"공지사항"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"합계 "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"총{0}항목 ·{1}읽지 않은 항목"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"소개"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"CrossDesk 정보"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"새 버전이 있습니다."</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"닫기"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"컨트롤 바 닫기"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"개인 정보 보호 화면 비활성화"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"선명도와 부드러움의 균형을 유지하며 일정한 압력을 가하면 해상도가 점차 감소합니다."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"해상도"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"삭제"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"공지사항 삭제"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"기록 삭제"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"이 공지를 삭제하시겠습니까?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"새로고침 안내"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"클립보드"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"로드 중…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"더 로드하기"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"업데이트 안내"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"기본 연결 초기화 실패"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"송신"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"보내기 "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"전송 후 원격 키보드에서 \"붙여넣기\"를 클릭하세요. 수신된 원격 텍스트를 로컬 시스템에 수동으로 복사할 수 있습니다."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"파일 보내기"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"문자 보내기"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"취소"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"연결 취소"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"각 구성 요소에는 나열된 오픈 소스 라이선스가 적용되며 부여된 권리에는 이 애플리케이션의 일반 사용 약관이 적용되지 않습니다. 소스 코드를 보려면 인터넷 연결이 필요합니다."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"합계"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"동의하고 계속하기"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"백그라운드에서 연결을 유지하고 원격 데스크톱으로 돌아가려면 클릭하세요."</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"백그라운드 연결 유지를 시작하지 못했습니다. 애플리케이션을 포그라운드에 유지하세요."</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"백그라운드 연결 유지를 시작하지 못했습니다. 애플리케이션을 포그라운드에 유지하고 다시 연결하세요."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"백그라운드 연결이 종료되었습니다. 다시 연결해 주세요."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"백그라운드 연결 서비스가 중지되었습니다. 다시 연결해 주세요."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"새로운 미리보기는 후속 연결에도 계속 저장됩니다."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"온라인"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"프로토콜 및 경로 없이 호스트 이름 또는 IPv4 주소를 입력하세요."</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"소리"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"원격 텍스트 복사"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"대역폭이 부족할 때 영상 선명도를 보장하려면 항상 더 높은 해상도를 유지하고 프레임 속도를 낮추십시오."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"미디어 암호화"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"문자·숫자"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"보안 저장 또는 인증서 초기화에 실패했습니다. 다시 시도하거나 애플리케이션 데이터를 삭제하세요."</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"완료"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"개인정보 동의 완료 후 업데이트 내용을 확인할 수 있습니다."</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"개인정보 인증 완료 후 로컬 신원 등록"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"비밀번호 저장에 실패했습니다. 이 연결은 계속 사용할 수 있습니다."</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"비밀번호는 기본 Android 키 저장소에 암호화되어 저장됩니다."</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"원격 ID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"피어 ID "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"피어 ID{0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"이 기기에 저장된 원격 미리보기는 모두 삭제되며, 연결 기록과 비밀번호는 유지됩니다."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"화면 방향"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"원격제어 메뉴 확장"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"알림으로 인해 원격 연결이 끊어졌습니다."</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"비밀번호 저장됨"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"저장된 비밀번호를 읽을 수 없습니다. 다시 입력해 주세요."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"저장된 비밀번호는 보관할 수 없으므로 다시 입력해 주세요."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del가 전송되었습니다."</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"백그라운드 연결 유지가 활성화되어 전환 애플리케이션이 계속 연결을 유지할 수 있습니다."</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"연결 알림에서 돌아가거나 연결을 끊기 위해 백그라운드 연결 유지가 활성화되었습니다."</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"승인됨"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"읽기"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"연결됨 "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"서버에 연결됨"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"프레임 속도"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"부드러움"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"균형"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"애플리케이션의 라이센스 파일이 없거나 손상되었습니다. 애플리케이션을 다시 설치하십시오."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"개발 버전에는 아직 출시되지 않은 수정 사항이 포함되어 있습니다. 다음 링크는 기본 버전의 소스 코드입니다."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"저장 사용"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"개인 정보 보호 화면 활성화"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"오픈 소스 구성 요소"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"오픈소스 소프트웨어 권리 설명"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"현재 최신 버전입니다."</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"현재 원격측에서는 이 텍스트를 직접 입력하는 기능을 지원하지 않습니다. 텍스트 입력을 지원하는 제어된 버전을 사용하거나 클립보드를 통해 보내주세요."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"성공적으로 연결되면 가장 가까운 원격 장치가 여기에 표시됩니다."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"개인정보 보호정책을 읽었으며 이에 동의합니다."</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"선택한 파일(읽을 수 없거나 공간이 부족함)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"장치 ID를 찾을 수 없습니다"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"명령 전송에 실패했습니다. 다시 시도해 주세요."</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"명령이 전송되었으며 컴퓨터 확인을 기다리고 있습니다..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"인증상태"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"수신"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"받다 "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"로컬 디코더"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"철회"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"탈퇴 후에는 원격접속이 중단되며 화면 미리보기가 닫히고 기존 미리보기는 지워집니다. 접속 기록과 비밀번호는 유지되며, 재접속 전 다시 인증이 필요합니다."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"네트워크 승인 철회"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"네트워크 승인을 철회하시겠습니까?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"리모컨 메뉴 접기"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"키보드 숨기기"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"마우스 숨기기"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"데이터"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"파일 저장에 실패했습니다. 다시 시도해 주세요."</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"파일이 저장되었습니다"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"파일 선택 시간이 초과되어 세션이 종료되었습니다."</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"텍스트가 128KB를 초과합니다."</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"연결 끊기"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"연결을 끊으면 홈페이지로 돌아갑니다."</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"원격 연결을 끊으시겠습니까?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"연결 해제"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"새 버전"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"새 버전 사용 가능: v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"새 버전 사용 가능: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"읽지 않은 공지사항 없음"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"읽기 상태를 로컬에 저장할 수 없습니다. 다시 시도해 주세요."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"이 공지를 삭제할 수 없습니다. 다시 시도해 주세요."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"원격 연결을 시작할 수 없습니다"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"오픈소스 라이선스를 읽을 수 없습니다."</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"디스플레이"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"비밀번호 표시"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"나중에"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"공지 없음"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"최근 연결"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"서버"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"ServerTLS인증서 확인 실패"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"서버 주소"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"서버 로그인에 실패했습니다. 서버 주소와 네트워크를 확인해주세요."</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"서버 포트"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"서버 연결 시간이 초과되었습니다. 서버 주소와 네트워크를 확인하세요."</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"활성화되지 않음"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"링크를 열 수 있는 브라우저가 없습니다."</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"무단"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"읽지 않음"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"서버가 연결되지 않았습니다. · 다시 시도하려면 클릭하세요."</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"이 애플리케이션은 제한된 보증과 책임 제한을 제공하는CeCILL-C1.0에 따라 순방향 오류 수정을 위해OpenFEC를 사용합니다. 자세한 내용은 전체 라이센스를 참조하세요."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"로컬 ID "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"로컬 ID{0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"이 버전의 소스 코드 및 빌드 지침"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"이 빌드에는 소스 개정을 사용할 수 없습니다."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"새로고침하려면 릴리즈하세요"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"빌드 지침"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"레시피 및 프로젝트 패치 빌드"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"소스코드 보기"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"확인에 실패했습니다. 나중에 다시 시도해 주세요."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"업데이트 확인"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"개인정보 보호 화면을 닫는 중…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"보안 연결 설정 중…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"개인정보 보호 화면을 켜는 중..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"업데이트 확인 중…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"지우는 중..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"로컬 ID를 얻는 중..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"선택한 파일을 읽는 중..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"연결 중"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"시그널링 서버에 연결 중..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"서버에 연결하는 중..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"서버에 재접속하는 중..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"이 장치에 사용할 수 있는 파일 선택기가 없습니다."</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"클리어"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"미리보기 삭제"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"미리보기를 삭제할까요?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"소스 코드 및 빌드 지침"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"소스코드 버전"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"버전"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"저작권 및 라이선스 정책"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"컴퓨터 키보드"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"화질"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"영상 선호도"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"영상 지연"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"영상 설정"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"비디오 품질"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"비디오 캡처 프레임 속도"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"트랙패드"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"상대 마우스"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"알겠습니다"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"알았어"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"확인"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"연결 확인"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"오프라인"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"스페이스"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"포트 범위는 1~65535입니다."</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"기호·기능"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"컴퓨터가 개인 정보 보호 화면 상태로 돌아갈 때까지 기다립니다. 오래된 컴퓨터에서는 이를 지원하지 않을 수 있습니다."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"원격 화면 정보를 기다리는 중"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"원격 화면을 기다리는 중..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"개인정보 인증 대기 중"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"시스템 키보드"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"직접 터치"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"앱솔루트 마우스"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"네트워크"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"커스텀"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"가상마우스"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"로컬 단축키를 사용하여 원격 개인정보 보호 화면을 잠금 해제했습니다. 개인정보 보호를 복원하고 로컬 바로가기 잠금 해제를 비활성화하려면 다시 잠그세요."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"제어 버전이 너무 낮습니다. 최신 버전으로 업그레이드하세요."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"리모트 엔코더"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"원격지로 보낼 텍스트"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"영상"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"라이센스"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"라이센스 및 저작권 고지"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"라이센스 및 저작권 고지는 오프라인에서 확인할 수 있습니다."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"기기 ID를 저장하지 못했습니다. 보안 저장소를 확인하세요."</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"기기 ID와 선택된 저장된 액세스 비밀번호는 Android Keystore를 사용하여 암호화됩니다. 연결 기록은 로컬에만 저장되며 장치 백업에는 포함되지 않습니다. 세션이 종료되면 클립보드 내용이 지워집니다. 앱을 종료한 후에도 PIP(picture-in-picture) 또는 연결된 알림으로 세션을 유지하고 앱 또는 알림 내에서 연결을 끊습니다."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"설정"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"설정, 새 버전이 있습니다"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"접속 비밀번호"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"접속비밀번호가 잘못되었습니다. 다시 입력해 주세요."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"인증서 확인 실패 · 다시 시도하려면 클릭하세요."</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"먼저 온라인 인증을 완료해주세요."</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"원격 측에서CrossDesk시스템 서비스를 활성화하고 다시 시도하십시오."</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"요청 시간이 초과되어 컴퓨터의 개인 정보 보호 화면 상태가 확인되지 않았습니다."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"현재 파일이 전송될 때까지 기다려 주십시오."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"1~65535 범위의 포트를 입력하세요."</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"기기 ID 9자리를 입력하세요."</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"유효한 접속 비밀번호를 입력해주세요"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"설정을 적용할 수 없습니다. 다시 시도해 보세요."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"소프트웨어 라이센스"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"뒤로"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"컨트롤 바로 돌아가기"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"아직 접속기록이 없습니다"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"본 공지사항은 업데이트 또는 삭제되었습니다. 공지사항 목록으로 돌아가서 확인하시기 바랍니다."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"원격 작업이 종료되었습니다."</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"원격 데스크톱"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"원격 데스크톱 터치 영역"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"원격 데스크톱: 터치하고 클릭하고, 드래그하여 이동하고, 오른쪽 버튼을 길게 누르고, 핀치하여 확대/축소하고, 두 손가락으로 화면을 확대하고 드래그하고, 두 손가락으로 위아래로 밀어 원래 크기로 스크롤합니다."</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"원격 컴퓨터"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"원격 창이 닫혔거나 백그라운드 Keepalive 시작에 실패하여 세션이 종료되었습니다."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"원격 접속"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"원격 비밀번호가 올바르지 않습니다."</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"원격 측에서 연결을 종료했습니다."</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"현재 원격 끝을 사용할 수 없습니다."</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"원격 접속 비밀번호가 만료되었습니다. 다시 접속해 주세요."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"연결"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"연결 "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"연결 실패"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"연결 실패 · 다시 시도하려면 클릭하세요."</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"연결 끊김"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"연결이 종료되었습니다"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"연결 지연(RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"지원되는 원격 장치에 연결된 경우 사용할 수 있습니다."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"연결 완료 후 가로 화면 잠금"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"연결 프롬프트"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"연결 방법"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"서버에 접속"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"서버에 접속하여 최신 공지사항을 받아보세요."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"연결 시간이 초과되었습니다. 장치 ID, 서버 및 원격 온라인 상태를 확인하십시오."</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"원격 데스크톱에 연결"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"백스페이스"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"공지사항"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"일부 미리보기를 삭제할 수 없습니다. 다시 시도해 주세요."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"다시 시도해 보세요"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"읽은 상태 저장 다시 시도"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"잠금"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"키보드"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"읽기 및 승인"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"개인 정보 보호"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"개인 정보 보호 및 승인"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"개인정보 보호 화면"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"화면 보호 꺼짐"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"화면 보호 켜짐"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"개인정보 보호 화면 잠금 해제됨"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"개인정보 화면 이상"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"개인 정보 보호 화면을 일시적으로 사용할 수 없습니다."</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"개인 정보 보호 화면 상태가 만료되어 컴퓨터 확인을 기다리는 중입니다."</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"개인정보 보호 화면 상태 확인 예정"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"개인정보 보호정책"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"개인정보취급방침은 일시적으로 열람할 수 없습니다. 인터넷 연결을 거부하고 애플리케이션을 다시 설치하십시오."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"음소거"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"오디오"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"프로젝트 홈페이지"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"미리보기에는 원격 컴퓨터의 개인 정보나 업무 정보가 포함될 수 있습니다. 전원을 켜면 각 연결이 자동으로 기기에 사진을 저장하고 최근 연결에 표시됩니다. 언제든지 닫고 지울 수 있습니다."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"높음"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"기본값"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"기본적으로 꺼져 있습니다. 전원을 켜면 휴대폰이 연결되면 화면이 가로 방향으로 전환되고 유지됩니다. 꺼지면 장치의 방향을 따릅니다. 태블릿과 멀티 윈도우는 영향을 받지 않습니다."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"마우스 오른쪽 버튼을 누르고 드래그한 후 떼면 드래그가 종료됩니다."</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"마우스 왼쪽 버튼을 누르고 드래그한 후 떼면 드래그가 종료됩니다."</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"마우스 제어"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"마우스 휠, 위아래로 밀어 키 범위 밖으로 슬라이드"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", 패킷 손실률 "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", 보내다 "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", 비밀번호가 저장되었습니다"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", 수신 "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">", 재시도를 클릭하세요."</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · Concluído"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" Falha na transferência"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" dispositivos"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" Artigo · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" itens não lidos"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"Conexão remota CrossDesk"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL versão 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"Aviso OpenFEC"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"Licença e fonte OpenFEC"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"Conexão direta P2P"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRT habilitado"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"GIRAR relé"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"Falha na transmissão {0}"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"Dispositivos: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"Itens não lidos {0}"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, recebendo {1}, enviando {2}, taxa de perda de pacotes {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● Conectado"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"Puxe para baixo para atualizar"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"Taxa de perda"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"Média"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"Prioriza manter a imagem suave e reduzirá a resolução de forma mais agressiva quando a largura de banda ou a pressão de codificação forem altas."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"Baixa"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"Usando um servidor personalizado"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"Salvar senha"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"Salvar arquivo recebido"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"Salvar visualizações de tela remotas"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"Salvar visualização remota da tela?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"A conexão de sinalização foi interrompida, reconecte."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"Falha ao carregar o anúncio. Tente novamente."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"Detalhes do anúncio"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"Total "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"Total de itens {0}· Itens não lidos {1}"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"Sobre"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"Sobre o CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"Sobre, há uma nova versão"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"Fechar"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"Fechar barra de controle"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"Desativar tela de privacidade"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"Equilibra clareza e suavidade, reduzindo apenas gradualmente a resolução sob pressão constante."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"Resolução"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"Excluir"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"Excluir anúncio"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"Excluir registro"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"Excluir este anúncio?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"Anúncio de atualização"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"Área de transferência"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"Carregando…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"Carregue mais"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"Atualização recomendada"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"Falha na inicialização da conexão nativa"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"Enviar"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"Enviar "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"Após o envio, clique em “Colar” no teclado remoto. O texto remoto recebido pode ser copiado manualmente para a máquina local."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"Enviar arquivo"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"Enviar texto"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"Cancelar"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"Cancelar conexão"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"Cada componente está sujeito à licença de código aberto listada e os direitos concedidos não estão sujeitos aos termos gerais de uso deste aplicativo. A visualização do código-fonte requer uma conexão com a Internet."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"Total"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"Concorde e continue"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"Mantenha-se conectado em segundo plano, clique para retornar à área de trabalho remota"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"Falha ao iniciar a manutenção de atividade em segundo plano, mantenha o aplicativo em primeiro plano"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"Falha ao iniciar a manutenção de atividade em segundo plano. Mantenha o aplicativo em primeiro plano e reconecte."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"A conexão em segundo plano foi encerrada. Reconecte."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"O serviço de conexão em segundo plano foi interrompido, reconecte."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"Novas visualizações ainda serão salvas nas conexões subsequentes."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"On-line"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"Preencha o nome do host ou endereço IPv4, sem protocolo e caminho"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"Som"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"Copiar texto remoto"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"Sempre mantenha uma resolução mais alta e reduza a taxa de quadros para garantir a clareza da imagem quando a largura de banda for insuficiente."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"Criptografia de mídia"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"Letras e números"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"Falha no armazenamento seguro ou na inicialização do certificado. Tente novamente ou limpe os dados do aplicativo"</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"Feito"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"Você pode verificar se há atualizações após concluir a autorização de privacidade"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"Registre a identidade local após concluir a autorização de privacidade"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"Falha ao salvar a senha, esta conexão ainda pode ser usada"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"As senhas são salvas criptografadas no Android Keystore nativo"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"ID remoto"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"ID do par "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"ID de par {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"Todas as visualizações remotas salvas nesta máquina serão excluídas e os registros de conexão e senhas serão mantidos."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"Orientação da tela"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"Expandir menu de controle remoto"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"Conexão remota desconectada da notificação"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"Senha salva"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"A senha salva não pode ser lida, digite-a novamente."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"A senha salva não pode ser mantida, digite-a novamente."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del enviado"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"O keep-alive em segundo plano está habilitado, a troca de aplicativos pode continuar a manter a conexão"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"Manutenção de atividade em segundo plano habilitada para retornar ou desconectar das notificações de conexão"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"Concedido"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"Leia"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"Conectado "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"Conectado ao servidor"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"Taxa de quadros"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"Fluidez"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"Equilibrado"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"O arquivo de licença do aplicativo está ausente ou danificado. Reinstale o aplicativo."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"A versão de desenvolvimento contém modificações que ainda não foram lançadas. O link a seguir é o código fonte da versão básica."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"Ativar salvamento"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"Habilitar tela de privacidade"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"Componentes de código aberto"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"Descrição dos direitos de software de código aberto"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"Esta é atualmente a versão mais recente"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"Atualmente, a extremidade remota não suporta a entrada direta deste texto. Use uma versão do final controlado que suporte entrada de texto ou envie-o através da área de transferência."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"Após a conexão bem-sucedida, o dispositivo remoto mais próximo será exibido aqui."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"Li e concordo com a Política de Privacidade"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"Arquivo selecionado (não pode ser lido ou espaço insuficiente)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"O ID do dispositivo não foi encontrado"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"Falha no envio do comando, tente novamente"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"O comando foi enviado, aguardando confirmação do computador..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"Status de autorização"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"Receber"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"receber "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"Decodificador local"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"Revogar"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"Após a retirada, a conexão remota será interrompida, a visualização da tela será fechada e a visualização existente será apagada. O registro de conexão e a senha serão mantidos e será necessária autorização novamente antes de reconectar."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"Retirar autorização de rede"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"Retirar a autorização da rede?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"Recolher menu de controle remoto"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"Ocultar teclado"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"Ocultar mouse"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"Dados"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"Falha ao salvar o arquivo, tente novamente"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"Arquivo salvo"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"A seleção do arquivo expirou e a sessão foi encerrada"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"O texto excede 128 KB"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"Desconectar"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"Retornará à página inicial após desconectar"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"Desconectar a conexão remota?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"Desconectar"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"Nova versão"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"Nova versão disponível: v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"Nova versão disponível: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"Nenhum anúncio não lido"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"Não é possível salvar o status de leitura localmente. Por favor, tente novamente."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"Não é possível excluir este anúncio. Por favor, tente novamente."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"Não é possível iniciar a conexão remota"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"Não é possível ler a licença de código aberto"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"Tela"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"Mostrar senha"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"Agora não"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"Sem anúncios"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"Conexões recentes"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"Servidor"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"Falha na verificação do certificado Server TLS"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"Endereço do servidor"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"Falha no login do servidor, verifique o endereço do servidor e a rede"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"Porta do servidor"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"A conexão do servidor expirou, verifique o endereço do servidor e a rede"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"Não habilitado"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"Nenhum navegador encontrado para abrir o link"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"Não autorizado"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"Não lido"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"Servidor não conectado · Clique para tentar novamente"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"Este aplicativo usa OpenFEC para correção direta de erros noCeCILL-C1.0, que fornece garantias limitadas e limita a responsabilidade. Veja a licença completa para detalhes."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"ID local "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"ID local {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"Código-fonte e instruções de construção para esta versão"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"A revisão fonte não está disponível para esta compilação."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"Solte para atualizar"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"Instruções de construção"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"Crie receitas e patches de projeto"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"Ver código-fonte"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"Falha na verificação. Por favor, tente novamente mais tarde."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"Verifique se há atualizações"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"Fechando a tela de privacidade…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"Estabelecendo conexão segura…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"Ativando a tela de privacidade..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"Verificando atualizações…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"Limpando..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"Obtendo ID local..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"Lendo arquivo selecionado..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"Conectando"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"Conectando ao servidor de sinalização..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"Conectando ao servidor..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"Reconectando ao servidor..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"Nenhum seletor de arquivos disponível para este dispositivo"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"Limpar"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"Limpar prévias"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"Limpar prévias?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"Código-fonte e instruções de construção"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"Versão do código-fonte"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"Versão"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"Declaração de direitos autorais e licença"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"Teclado de computador"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"Qualidade"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"Preferência de vídeo"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"Latência do vídeo"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"Configurações de vídeo"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"Qualidade de vídeo"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"Taxa de quadros de captura de vídeo"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"Touchpad"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"Rato relativo"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"Entendi"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"OK"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"Confirmar"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"Confirme a conexão"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"Off-line"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"Espaço"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"O intervalo de portas é 1–65535"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"Símbolos e teclas"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"Aguarde o computador retornar ao estado da tela de privacidade; computadores mais antigos podem não suportá-lo."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"Aguardando informações da tela remota"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"Aguardando a tela remota..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"Aguardando autorização de privacidade"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"Teclado do sistema"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"Toque direto"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"Rato Absoluto"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"Rede"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"Personalizado"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"Rato virtual"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"A tela de privacidade remota foi desbloqueada usando o atalho local. Bloqueie-o novamente para restaurar a privacidade e desativar o desbloqueio de atalhos locais."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"A versão controlada é muito baixa, atualize para a versão mais recente."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"Codificador remoto"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"Texto a ser enviado para o terminal remoto"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"Vídeo"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"Licença"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"Licenças e avisos de direitos autorais"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"A licença e o aviso de direitos autorais estão disponíveis offline."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"Falha ao salvar a identidade do dispositivo. Verifique o armazenamento seguro"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"A identidade do dispositivo e as senhas de acesso salvas selecionadas são criptografadas usando o Android Keystore. Os registros de conexão são salvos apenas localmente e não são incluídos no backup do dispositivo. O conteúdo da área de transferência é apagado quando a sessão é encerrada. Mantenha a sessão com notificações picture-in-picture ou conectadas depois de sair do aplicativo e desconecte-se do aplicativo ou das notificações."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"Configurações"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"Configurações, há uma nova versão"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"Senha de acesso"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"A senha de acesso está errada, digite-a novamente."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"Falha na verificação do certificado · Clique para tentar novamente"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"Por favor, preencha primeiro a autorização online"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"Ative o serviço do sistema CrossDesk na extremidade remota e tente novamente"</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"A solicitação expirou e o status da tela de privacidade do computador não foi confirmado."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"Aguarde o envio do arquivo atual."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"Insira uma porta no intervalo 1–65535"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"Insira um ID de dispositivo de 9 dígitos"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"Por favor, insira uma senha de acesso válida"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"Não foi possível aplicar as configurações. Tente novamente."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"Licença de software"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"Voltar"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"Retornar à barra de controle"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"Ainda não há registro de conexão"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"Este anúncio foi atualizado ou excluído. Volte à lista de anúncios para visualizá-lo."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"A tarefa remota está fechada"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"Área de Trabalho Remota"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"Área de toque da área de trabalho remota"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"Área de trabalho remota: toque e clique, arraste para mover, pressione e segure o botão direito, aperte para ampliar, amplie e arraste a tela com dois dedos, deslize para cima e para baixo com dois dedos para rolar na escala original"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"Computador remoto"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"A janela remota foi fechada ou a manutenção de atividade em segundo plano falhou ao iniciar e a sessão foi encerrada."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"Conexão remota"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"Senha remota incorreta"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"A extremidade remota fechou a conexão"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"A extremidade remota está indisponível no momento"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"A senha de acesso remoto expirou, reconecte."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"Conectar"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"Conexão "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"Falha na conexão"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"Falha na conexão · Clique para tentar novamente"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"Conexão perdida"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"Conexão encerrada"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"Atraso de conexão (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"Disponível quando conectado a um dispositivo remoto compatível."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"Bloqueie a tela paisagem após a conexão ser estabelecida"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"Solicitação de conexão"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"Método de conexão"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"Conecte-se ao servidor"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"Conecte-se ao servidor para obter os anúncios mais recentes."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"A conexão expirou, verifique o ID do dispositivo, o servidor e o status on-line remoto"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"Conecte-se à área de trabalho remota"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"Retrocesso"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"Avisos"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"Não foi possível limpar algumas visualizações. Tente novamente."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"Tente novamente"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"Tente salvar novamente o status de leitura"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"Bloqueio"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"Teclado"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"Ler e autorizar"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"Privacidade"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"Privacidade e Autorização"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"Tela de privacidade"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"Privacidade desativada"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"Privacidade ativada"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"Tela de privacidade desbloqueada"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"Anormalidade na tela de privacidade"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"A tela de privacidade está temporariamente indisponível"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"O status da tela de privacidade expirou, aguardando confirmação do computador"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"Status da tela de privacidade a ser confirmado"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"Política de Privacidade"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"A política de privacidade não pode ser lida temporariamente. Recuse-se a se conectar à Internet e reinstale o aplicativo."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"Mudo"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"Áudio"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"Site do projeto"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"As visualizações podem conter informações pessoais ou de trabalho do computador remoto. Após ligá-lo, cada conexão salvará automaticamente uma imagem na máquina e a exibirá nas conexões recentes. Você pode fechar e limpar a qualquer momento."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"Alta"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"Padrão"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"Desligado por padrão. Quando ligado, a tela entrará e permanecerá na orientação paisagem quando o telefone estiver conectado; quando desligado, seguirá a direção do dispositivo. Tablets e janelas múltiplas não são afetados."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"Botão direito do mouse, pressione e arraste, solte para finalizar o arrasto"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"Botão esquerdo do mouse, pressione e arraste, solte para finalizar o arrasto"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"Controle do mouse"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"Roda do mouse, deslize para cima e para baixo para sair do intervalo de teclas"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", taxa de perda de pacotes "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", enviar "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", senha salva"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", receba "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">", clique em Tentar novamente"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · Завершено"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" Передача не удалась"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" устройства"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" Статья · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" непрочитанные элементы"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"Удаленное подключение CrossDesk"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL версия 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"Уведомление OpenFEC"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"Лицензия и исходный код OpenFEC"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"Прямое соединение P2P"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP включен"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"Реле ПОВОРОТА"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"Передача {0} не удалась."</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"Устройства: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0} непрочитанные элементы"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, прием {1}, отправка {2}, скорость потери пакетов {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● Подключено"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"Потяните вниз, чтобы обновить"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"Потери пакетов"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"Среднее"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"Отдает приоритет сохранению плавности изображения и более агрессивно снижает разрешение при высокой пропускной способности или нагрузке на кодирование."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"Низкое"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"Использование пользовательского сервера"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"Сохранить пароль"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"Сохранить полученный файл"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"Сохранять превью удалённого экрана"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"Сохранить предварительный просмотр удаленного экрана?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"Сигнальное соединение прервано, пожалуйста, переподключитесь."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"Не удалось загрузить объявление. Повторите попытку."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"Подробности объявления"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"Итого "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"Всего элементов {0}· Непрочитанные элементы {1}"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"О программе"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"О CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"О, вышла новая версия"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"Закрыть"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"Закрыть панель управления"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"Выключить приватный экран"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"Балансирует четкость и плавность изображения, постепенно снижая разрешение под постоянным давлением."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"Разрешение"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"Удалить"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"Удалить объявление"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"Удалить запись"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"Удалить это объявление?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"Обновить объявление"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"Буфер обмена"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"Загрузка…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"Загрузить еще"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"Рекомендуется обновление"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"Не удалось инициализировать собственное соединение."</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"Отправка"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"Отправить "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"После отправки нажмите «Вставить» на удаленной клавиатуре. Полученный удаленный текст можно вручную скопировать на локальный компьютер."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"Отправить файл"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"Отправить текст"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"Отмена"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"Отмена соединения"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"На каждый компонент распространяется действие указанной лицензии с открытым исходным кодом, а предоставленные права не подпадают под действие общих условий использования этого приложения. Для просмотра исходного кода требуется подключение к Интернету."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"Итого"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"Согласитесь и продолжайте."</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"Оставайтесь на связи в фоновом режиме, нажмите, чтобы вернуться к удаленному рабочему столу."</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"Не удалось запустить фоновую поддержку активности, оставьте приложение на переднем плане."</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"Не удалось запустить фоновую поддержку активности. Оставьте приложение в активном режиме и переподключитесь."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"Фоновое соединение прервано, подключитесь заново."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"Служба фонового подключения остановлена, подключитесь повторно."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"Новые превью по-прежнему будут сохраняться при последующих подключениях."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"В сети"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"Введите имя хоста или IPv4-адрес без протокола и пути."</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"Звук"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"Копирование удаленного текста"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"Всегда поддерживайте более высокое разрешение и уменьшайте частоту кадров, чтобы обеспечить четкость изображения при недостаточной полосе пропускания."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"Шифрование мультимедиа"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"Буквы и цифры"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"Не удалось инициализировать безопасное хранилище или сертификат. Повторите попытку или очистите данные приложения."</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"Готово"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"Вы можете проверить наличие обновлений после завершения авторизации конфиденциальности."</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"Зарегистрируйте локальную личность после завершения авторизации конфиденциальности."</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"Не удалось сохранить пароль, это соединение все еще можно использовать."</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"Пароли сохраняются в зашифрованном виде в собственном хранилище ключей Android."</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"Удаленный ID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"Идентификатор узла "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"Идентификатор узла {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"Все удаленные просмотры, сохраненные на этом компьютере, будут удалены, а записи подключений и пароли сохранятся."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"Ориентация экрана"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"Развернуть меню пульта дистанционного управления"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"Удаленное соединение отключено из-за уведомления"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"Пароль сохранён"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"Сохраненный пароль невозможно прочитать, введите его еще раз."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"Сохраненный пароль невозможно сохранить, введите его еще раз."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del отправлено"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"Фоновая поддержка активности включена, переключающиеся приложения могут продолжать поддерживать соединение."</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"Фоновая поддержка активности включена для возврата или отключения от уведомлений о соединении."</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"Разрешено"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"Прочитано"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"Подключено "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"Подключено к серверу"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"Частота кадров"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"Плавность"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"Баланс"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"Файл лицензии в приложении отсутствует или поврежден, переустановите приложение."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"Версия для разработки содержит модификации, которые еще не были выпущены. По следующей ссылке приведен исходный код базовой версии."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"Включить сохранение"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"Включить приватный экран"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"Компоненты с открытым кодом"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"Описание прав на программное обеспечение с открытым исходным кодом."</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"На данный момент это последняя версия."</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"В настоящее время удаленный конец не поддерживает прямой ввод этого текста. Пожалуйста, используйте версию управляемого конца, поддерживающую ввод текста, или отправьте его через буфер обмена."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"При успешном подключении здесь будет отображено ближайшее удаленное устройство."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"Я прочитал и согласен с Политикой конфиденциальности."</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"Выбранный файл (невозможно прочитать или недостаточно места)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"Идентификатор устройства не найден."</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"Не удалось отправить команду. Повторите попытку."</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"Команда отправлена, ждём подтверждения от компьютера..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"Статус авторизации"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"Приём"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"получить "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"Локальный декодер"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"Отозвать"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"После выхода удаленное соединение будет остановлено, предварительный просмотр экрана закроется, а существующий предварительный просмотр будет очищен. Запись о подключении и пароль будут сохранены, и перед повторным подключением потребуется повторная авторизация."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"Отозвать сетевую авторизацию"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"Отозвать сетевую авторизацию?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"Свернуть меню пульта дистанционного управления"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"Скрыть клавиатуру"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"Скрыть мышь"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"Данные"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"Не удалось сохранить файл. Повторите попытку."</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"Файл сохранен."</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"Тайм-аут выбора файла истек, сеанс завершен."</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"Размер текста превышает 128 КБ."</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"Отключить"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"Вернется на домашнюю страницу после отключения."</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"Отключить удаленное соединение?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"Отключить"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"Новая версия"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"Доступна новая версия: v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"Доступна новая версия: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"Нет непрочитанных объявлений"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"Не удалось сохранить статус чтения локально. Повторите попытку."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"Не удалось удалить объявление. Повторите попытку."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"Невозможно инициировать удаленное соединение."</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"Невозможно прочитать лицензию с открытым исходным кодом."</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"Экран"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"Показать пароль"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"Не сейчас"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"Нет объявлений"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"Недавние связи"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"Сервер"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"Проверка Server TLS certificate не удалась."</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"Адрес сервера"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"Не удалось войти на сервер, проверьте адрес сервера и сеть."</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"Порт сервера"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"Тайм-аут подключения к серверу истек, проверьте адрес сервера и сеть."</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"Не включено"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"Не найден браузер, в котором можно было бы открыть ссылку."</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"Несанкционированное"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"Не прочитано"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"Сервер не подключен · Нажмите, чтобы повторить попытку."</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"Приложение использует OpenFEC для прямого исправления ошибок по лицензии CeCILL-C 1.0 с ограниченной гарантией и ответственностью. Подробности — в тексте лицензии."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"Местный идентификатор "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"Локальный идентификатор {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"Исходный код и инструкции по сборке этой версии."</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"Ревизия исходного кода этой сборки недоступна."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"Отпустите, чтобы обновить"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"Инструкции по сборке"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"Рецепты сборки и исправления проектов."</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"Открыть исходный код"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"Не удалось проверить обновления. Повторите позже."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"Проверить обновления"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"Закрытие экрана конфиденциальности…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"Установление безопасного соединения…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"Включение экрана конфиденциальности..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"Проверка обновлений…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"Очистка..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"Получение локального идентификатора..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"Чтение выбранного файла..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"Подключение"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"Подключение к серверу сигнализации..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"Подключение к серверу..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"Повторное подключение к серверу..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"Для этого устройства средство выбора файлов недоступно."</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"Очистить"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"Удалить превью"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"Удалить превью?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"Исходный код и сборка"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"Версия исходного кода"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"Версия"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"Положение об авторских правах и лицензии."</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"Компьютерная клавиатура"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"Качество"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"Режим видео"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"Задержка видео"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"Настройки видео"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"Качество видео"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"Частота захвата видео"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"Тачпад"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"Родственная мышь"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"Понятно"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"ОК"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"Подтвердить"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"Подтвердите соединение"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"Не в сети"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"Пробел"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"Диапазон портов: 1–65535."</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"Символы и клавиши"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"Подождите, пока компьютер вернется в состояние экрана конфиденциальности; старые компьютеры могут его не поддерживать."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"Ожидание информации об удаленном экране."</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"Ожидание удаленного экрана..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"Ожидание разрешения на конфиденциальность."</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"Системная клавиатура"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"Касание"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"Абсолютная мышь"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"Сеть"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"Пользовательский"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"Виртуальная мышь"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"Приватный экран отключён локальным сочетанием клавиш. Заблокируйте его снова, чтобы восстановить защиту и запретить локальную разблокировку."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"Управляемая версия слишком низкая, обновите ее до последней версии."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"Кодер удалённой стороны"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"Текст для отправки на удаленный конец"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"Видео"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"Лицензия"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"Лицензии и авторские права"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"Лицензия и уведомление об авторских правах доступны в автономном режиме."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"Не удалось сохранить идентификационные данные устройства. Проверьте безопасное хранилище."</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"Идентификатор устройства и выбранные сохраненные пароли доступа шифруются с помощью хранилища ключей Android. Записи подключений сохраняются только локально и не включаются в резервную копию устройства. Содержимое буфера обмена очищается после завершения сеанса. Оставайтесь в сеансе с помощью «картинка в картинке» или подключенных уведомлений после выхода из приложения и отключайтесь внутри приложения или уведомлений."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"Настройки"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"Настройки, есть новая версия"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"Пароль доступа"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"Неверный пароль доступа, пожалуйста, введите его еще раз."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"Не удалось проверить сертификат · Нажмите, чтобы повторить попытку."</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"Сначала пройдите онлайн-авторизацию."</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"Пожалуйста, включите системную службу CrossDesk на удаленном конце и повторите попытку."</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"Время ожидания запроса истекло, а состояние экрана конфиденциальности компьютера не подтверждено."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"Пожалуйста, дождитесь отправки текущего файла."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"Введите порт в диапазоне 1–65535."</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"Введите ID из 9 цифр"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"Пожалуйста, введите действительный пароль доступа."</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"Не удалось применить настройки. Повторите попытку."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"Лицензия программы"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"Назад"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"Возврат к панели управления."</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"Пока нет записей о подключении"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"Это объявление было обновлено или удалено. Чтобы просмотреть его, вернитесь к списку объявлений."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"Удаленная задача закрыта."</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"Удаленный рабочий стол"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"Сенсорная область удаленного рабочего стола"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"Удаленный рабочий стол: коснитесь и щелкните, перетащите для перемещения, нажмите и удерживайте правую кнопку, сведите пальцы для масштабирования, увеличьте масштаб и перетащите экран двумя пальцами, проведите двумя пальцами вверх и вниз для прокрутки в исходном масштабе."</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"Удаленный компьютер"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"Удаленное окно было закрыто, или не удалось запустить фоновую проверку активности, и сеанс завершился."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"Удаленное подключение"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"Неверный удаленный пароль."</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"Удаленная сторона закрыла соединение."</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"Удаленный конец в настоящее время недоступен."</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"Срок действия пароля удаленного доступа истек, подключитесь повторно."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"Подключиться"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"Подключение "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"Соединение не удалось"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"Не удалось подключиться · Нажмите, чтобы повторить попытку."</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"Соединение потеряно"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"Соединение завершено"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"Задержка соединения (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"Доступно при подключении к поддерживаемому устройству."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"Блокировка горизонтального экрана после установления соединения."</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"Запрос на подключение"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"Способ подключения"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"Подключиться к серверу"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"Подключитесь к серверу, чтобы получать последние объявления."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"Тайм-аут соединения истек, проверьте идентификатор устройства, сервер и удаленный онлайн-статус."</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"Подключение к удаленному рабочему столу"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"Возврат"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"Объявления"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"Некоторые предварительные просмотры не удалось удалить. Повторите попытку."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"Попробуйте еще раз"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"Повторить попытку сохранения статуса чтения."</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"Заблокировать"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"Клавиатура"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"Прочитать и авторизоваться"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"Конфиденциальность"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"Конфиденциальность и авторизация"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"Приватный экран"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"Приватный экран выкл."</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"Приватный экран вкл."</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"Приватный экран разблокирован"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"Неисправность экрана конфиденциальности."</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"Экран конфиденциальности временно недоступен."</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"Срок действия статуса экрана конфиденциальности истек, ожидание подтверждения от компьютера."</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"Статус экрана конфиденциальности подлежит подтверждению."</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"Политика конфиденциальности"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"Политику конфиденциальности временно невозможно прочитать. Пожалуйста, откажитесь от подключения к Интернету и переустановите приложение."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"Без звука"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"Аудио"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"Сайт проекта"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"Предварительный просмотр может содержать личную или рабочую информацию с удаленного компьютера. После его включения каждое подключение будет автоматически сохранять изображение на устройстве и отображать его при последних подключениях. Вы можете закрыть и очистить в любое время."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"Высокое"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"По умолчанию"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"По умолчанию выключено. При включении экран войдет и останется в альбомной ориентации при подключении телефона; в выключенном состоянии он будет следовать направлению устройства. Планшеты и мультиокна не затронуты."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"Правая кнопка мыши, нажмите и перетащите, отпустите, чтобы завершить перетаскивание."</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"Левую кнопку мыши нажмите и перетащите, отпустите, чтобы завершить перетаскивание."</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"Управление мышью"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"Колесо мыши: проведите вверх и вниз, чтобы выйти за пределы диапазона клавиш."</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", коэффициент потери пакетов "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", отправить "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", пароль сохранен"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", получить "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">"нажмите «Повторить»."</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,308 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by localization/generate.py. -->
|
||||
<resources>
|
||||
<string name="text_58b6df1059088699" formatted="false">" · Completed"</string>
|
||||
<string name="text_53af552fb3af7f6e" formatted="false">" Transfer failed"</string>
|
||||
<string name="text_23ebae95d26dfe5e" formatted="false">" devices"</string>
|
||||
<string name="text_a7f4dfd234a43b16" formatted="false">" Article · "</string>
|
||||
<string name="text_2c894057fc526576" formatted="false">" unread items"</string>
|
||||
<string name="text_d0ac753c6681e578" formatted="false">"CrossDesk remote connection"</string>
|
||||
<string name="text_0991c6fc1de4ffd6" formatted="false">"GNU GPL version 3"</string>
|
||||
<string name="text_5ae7dc4079223967" formatted="false">"OpenFEC notice"</string>
|
||||
<string name="text_69ad29820a259526" formatted="false">"OpenFEC license and source"</string>
|
||||
<string name="text_63ae4a0c7a448d21" formatted="false">"P2P direct connection"</string>
|
||||
<string name="text_70cae3c6aad8e152" formatted="false">"SRTP enabled"</string>
|
||||
<string name="text_ef813fec7d6d8a3b" formatted="false">"TURN relay"</string>
|
||||
<string name="text_ebede975edd52aee" formatted="false">"{0} transmission failed"</string>
|
||||
<string name="text_b3ffb6e982ecf5df" formatted="false">"Devices: {0}"</string>
|
||||
<string name="text_fc66a78b9e27fecf" formatted="false">"{0} unread items"</string>
|
||||
<string name="text_dcb59cdcf66150a1" formatted="false">"{0}, receiving {1}, sending {2}, packet loss rate {3}"</string>
|
||||
<string name="text_01965b3249458d01" formatted="false">"● Connected"</string>
|
||||
<string name="text_eb4236fe6a926a5a" formatted="false">"Pull down to refresh"</string>
|
||||
<string name="text_37772187ab854191" formatted="false">"Loss Rate"</string>
|
||||
<string name="text_a567bdaa11367f26" formatted="false">"Medium"</string>
|
||||
<string name="text_e6ff95a9fe6acc7f" formatted="false">"Prioritizes keeping the picture smooth, and will reduce resolution more aggressively when bandwidth or encoding pressure is high."</string>
|
||||
<string name="text_aa9e366f68d3d097" formatted="false">"Low"</string>
|
||||
<string name="text_f03d03eb35e3330c" formatted="false">"Using a custom server"</string>
|
||||
<string name="text_2e9f4819e064ce12" formatted="false">"Save password"</string>
|
||||
<string name="text_10019cef593819ed" formatted="false">"Save received file"</string>
|
||||
<string name="text_3bcdcd330bb3044c" formatted="false">"Save remote screen previews"</string>
|
||||
<string name="text_0f197074373e38fd" formatted="false">"Save remote screen preview?"</string>
|
||||
<string name="text_71ca53f95a8484da" formatted="false">"The signaling connection is interrupted, please reconnect."</string>
|
||||
<string name="text_789f22a8b1b34797" formatted="false">"Announcement loading failed, please try again."</string>
|
||||
<string name="text_00554b17c432937e" formatted="false">"Announcement details"</string>
|
||||
<string name="text_2428420e407fe2e8" formatted="false">"Total "</string>
|
||||
<string name="text_d977c7568cc5201e" formatted="false">"Total {0} items ·{1} unread items"</string>
|
||||
<string name="text_52d25a9e30ba94f1" formatted="false">"About"</string>
|
||||
<string name="text_459c1e980c1c8ce2" formatted="false">"About CrossDesk"</string>
|
||||
<string name="text_390a95b65eb33052" formatted="false">"About, there is a new version"</string>
|
||||
<string name="text_3fd47edce45b3603" formatted="false">"Close"</string>
|
||||
<string name="text_56a18c93e12fdc4e" formatted="false">"Close control bar"</string>
|
||||
<string name="text_4eb0565cc4658d7f" formatted="false">"Disable privacy screen"</string>
|
||||
<string name="text_7d466b69be42fc43" formatted="false">"Balances clarity and smoothness, only gradually reducing resolution under constant pressure."</string>
|
||||
<string name="text_b92421822c213c2b" formatted="false">"Resolution"</string>
|
||||
<string name="text_2f9daa828907b93f" formatted="false">"Delete"</string>
|
||||
<string name="text_0e46cdb3442edfba" formatted="false">"Delete announcement"</string>
|
||||
<string name="text_f2cf9101bb2d8816" formatted="false">"Delete record"</string>
|
||||
<string name="text_d087671ca7df7f0f" formatted="false">"Delete this announcement?"</string>
|
||||
<string name="text_33e7584cd5c252d7" formatted="false">"Refresh announcement"</string>
|
||||
<string name="text_8c10e2c1728ddf8e" formatted="false">"Clipboard"</string>
|
||||
<string name="text_4927a53bcc886afb" formatted="false">"Loading…"</string>
|
||||
<string name="text_af90a08fec8ee28d" formatted="false">"Load more"</string>
|
||||
<string name="text_a739f24a9fd8a5d9" formatted="false">"Update recommended"</string>
|
||||
<string name="text_861521c9e107c648" formatted="false">"Native connection initialization failed"</string>
|
||||
<string name="text_edecf0ae6e5144f9" formatted="false">"Send"</string>
|
||||
<string name="text_43b7b60867ca51e7" formatted="false">"Send "</string>
|
||||
<string name="text_da7df5a5543c4ba1" formatted="false">"After sending, click \"Paste\" on the remote keyboard. The received remote text can be manually copied to the local machine."</string>
|
||||
<string name="text_e7281fbca7af369c" formatted="false">"Send file"</string>
|
||||
<string name="text_ce6c02d19022e016" formatted="false">"Send text"</string>
|
||||
<string name="text_2cd0f3be8738a86c" formatted="false">"Cancel"</string>
|
||||
<string name="text_3687f953cfe85fb8" formatted="false">"Cancel connection"</string>
|
||||
<string name="text_72814b15a6fa152c" formatted="false">"Each component is subject to the listed open source license, and the rights granted are not subject to the general terms of use of this application. Viewing source code requires an internet connection."</string>
|
||||
<string name="text_e009c148d1890dac" formatted="false">"Total"</string>
|
||||
<string name="text_81c92013219dd115" formatted="false">"Agree and continue"</string>
|
||||
<string name="text_409696774c9998ce" formatted="false">"Keep connected in the background, click to return to the remote desktop"</string>
|
||||
<string name="text_dcb8d4e2e6973912" formatted="false">"Background keepalive failed to start, please keep the application in the foreground"</string>
|
||||
<string name="text_d96a21463557171d" formatted="false">"Background keepalive failed to start, please keep the application in the foreground and reconnect."</string>
|
||||
<string name="text_952f7300de0a7bd9" formatted="false">"The background connection has ended, please reconnect."</string>
|
||||
<string name="text_da904f715e1cff87" formatted="false">"The background connection service has stopped, please reconnect."</string>
|
||||
<string name="text_0812359e18082071" formatted="false">"New previews will still be saved on subsequent connections."</string>
|
||||
<string name="text_b9086662b1df1f20" formatted="false">"Online"</string>
|
||||
<string name="text_39f2f444a042643e" formatted="false">"Fill in the host name or IPv4 address, without protocol and path"</string>
|
||||
<string name="text_473400815c0f687d" formatted="false">"Sound"</string>
|
||||
<string name="text_ab5ca1fab212f4ca" formatted="false">"Copy remote text"</string>
|
||||
<string name="text_c92269b2039e7aa3" formatted="false">"Always maintain a higher resolution and reduce the frame rate to ensure picture clarity when bandwidth is insufficient."</string>
|
||||
<string name="text_7ff73de85ec16d15" formatted="false">"Media encryption"</string>
|
||||
<string name="text_22dc6217af77bc37" formatted="false">"Letters & numbers"</string>
|
||||
<string name="text_9ca1cdaf831182e3" formatted="false">"Secure storage or certificate initialization failed, please try again or clear application data"</string>
|
||||
<string name="text_c0b3fbff51ccc40b" formatted="false">"Done"</string>
|
||||
<string name="text_678328f6f02881c7" formatted="false">"You can check for updates after completing privacy authorization"</string>
|
||||
<string name="text_c4361f9abdba4882" formatted="false">"Register the local identity after completing privacy authorization"</string>
|
||||
<string name="text_c29b888e357d727d" formatted="false">"Password saving failed, this connection can still be used"</string>
|
||||
<string name="text_3f31300a23afb390" formatted="false">"Passwords are saved encrypted in the native Android Keystore"</string>
|
||||
<string name="text_8d3b052c4c67acda" formatted="false">"Remote ID"</string>
|
||||
<string name="text_0512084e130984bc" formatted="false">"Peer ID "</string>
|
||||
<string name="text_c5147d65960196b0" formatted="false">"Peer ID {0}"</string>
|
||||
<string name="text_882b705d31ac7e2a" formatted="false">"All remote previews saved on this machine will be deleted, and the connection records and passwords will be retained."</string>
|
||||
<string name="text_60cc99cc0013972e" formatted="false">"Screen orientation"</string>
|
||||
<string name="text_9e7294d264afecb7" formatted="false">"Expand remote control menu"</string>
|
||||
<string name="text_9343612ee473edd6" formatted="false">"Remote connection disconnected from notification"</string>
|
||||
<string name="text_9d32476fe83aef60" formatted="false">"Password saved"</string>
|
||||
<string name="text_1cbd391383283924" formatted="false">"The saved password cannot be read, please re-enter it."</string>
|
||||
<string name="text_6792b9f341380c68" formatted="false">"The saved password cannot be retained, please re-enter it."</string>
|
||||
<string name="text_74d3f0927d465047" formatted="false">"Ctrl+Alt+Del sent"</string>
|
||||
<string name="text_c28d86acbedba3b6" formatted="false">"Background keep-alive is enabled, switching applications can continue to maintain the connection"</string>
|
||||
<string name="text_506409ec08cb2a79" formatted="false">"Background keepalive enabled to return or disconnect from connection notifications"</string>
|
||||
<string name="text_521e65ffc7d0ff2d" formatted="false">"Granted"</string>
|
||||
<string name="text_efdd1a47e4beb2f9" formatted="false">"Read"</string>
|
||||
<string name="text_1f672b9b18d18e87" formatted="false">"Connected "</string>
|
||||
<string name="text_9c05d9d7fa1b0abd" formatted="false">"Connected to server"</string>
|
||||
<string name="text_f9404bf335bc14cf" formatted="false">"Frame rate"</string>
|
||||
<string name="text_9ae0c69e26db1d6a" formatted="false">"Smooth"</string>
|
||||
<string name="text_df544e46aa4d5c17" formatted="false">"Balanced"</string>
|
||||
<string name="text_3362e8fa7f65cc2f" formatted="false">"The license file in the application is missing or damaged, please reinstall the application."</string>
|
||||
<string name="text_3f2904435942cc0f" formatted="false">"The development version contains modifications that have not yet been released. The following link is the source code of the basic version."</string>
|
||||
<string name="text_4338afaad52ea445" formatted="false">"Enable saving"</string>
|
||||
<string name="text_c8bb8255390df3b6" formatted="false">"Enable privacy screen"</string>
|
||||
<string name="text_c6ecaca362ae1c80" formatted="false">"Open-source components"</string>
|
||||
<string name="text_6099db946bdb5ee4" formatted="false">"Open source software rights description"</string>
|
||||
<string name="text_6e86570183c3cdd0" formatted="false">"This is currently the latest version"</string>
|
||||
<string name="text_24a52788cf474bf6" formatted="false">"Currently, the remote end does not support direct input of this text. Please use a version of the controlled end that supports text input, or send it through the clipboard."</string>
|
||||
<string name="text_33f25b500697a7d9" formatted="false">"Upon successful connection, the nearest remote device will be displayed here."</string>
|
||||
<string name="text_70341256d7317e7d" formatted="false">"I have read and agreed to the Privacy Policy"</string>
|
||||
<string name="text_85c4c8fda0461f35" formatted="false">"Selected file (cannot be read or insufficient space)"</string>
|
||||
<string name="text_9b58c9ff5f6a856f" formatted="false">"The device ID was not found"</string>
|
||||
<string name="text_97b7b18df07b9787" formatted="false">"Command sending failed, please try again"</string>
|
||||
<string name="text_cb782027ca5986c7" formatted="false">"The command has been sent, waiting for computer confirmation..."</string>
|
||||
<string name="text_3ca40a586103f6b7" formatted="false">"Authorization status"</string>
|
||||
<string name="text_6e684586884ebee6" formatted="false">"Receive"</string>
|
||||
<string name="text_84ae92e54e3078aa" formatted="false">"receive "</string>
|
||||
<string name="text_d16488b987723161" formatted="false">"Local decoder"</string>
|
||||
<string name="text_6b58198955259066" formatted="false">"Withdraw"</string>
|
||||
<string name="text_c52e42ee856825db" formatted="false">"After withdrawing, the remote connection will be stopped, the screen preview will be closed and the existing preview will be cleared. The connection record and password will be retained, and authorization is required again before reconnecting."</string>
|
||||
<string name="text_61f4788032ec596b" formatted="false">"Withdraw network authorization"</string>
|
||||
<string name="text_e736e419008ac10a" formatted="false">"Withdraw network authorization?"</string>
|
||||
<string name="text_73431dcf0afb7a65" formatted="false">"Collapse remote control menu"</string>
|
||||
<string name="text_d826c4ece4dd4031" formatted="false">"Hide keyboard"</string>
|
||||
<string name="text_6e9a457c0c3c34ed" formatted="false">"Hide mouse"</string>
|
||||
<string name="text_5440f7424f2865bf" formatted="false">"Data"</string>
|
||||
<string name="text_94244f41d0a41378" formatted="false">"File saving failed, please try again"</string>
|
||||
<string name="text_2553a785fef2a405" formatted="false">"File saved"</string>
|
||||
<string name="text_d573b3f9a810ea04" formatted="false">"File selection timed out and session ended"</string>
|
||||
<string name="text_985a5c380be49f4f" formatted="false">"Text exceeds 128 KB"</string>
|
||||
<string name="text_f33ac04eece6a6a6" formatted="false">"Disconnect"</string>
|
||||
<string name="text_09b1395f50589327" formatted="false">"Will return to the homepage after disconnecting"</string>
|
||||
<string name="text_a73c0680db0d2e06" formatted="false">"Disconnect remote connection?"</string>
|
||||
<string name="text_a33f3804768e68ee" formatted="false">"Disconnect"</string>
|
||||
<string name="text_47ceaf502c5a05d6" formatted="false">"New version"</string>
|
||||
<string name="text_78823e6cf1a8afe5" formatted="false">"New version available: v"</string>
|
||||
<string name="text_b7e58b67fa8f7e78" formatted="false">"New version available: v{0}"</string>
|
||||
<string name="text_ef23787ec32c5588" formatted="false">"No unread announcements"</string>
|
||||
<string name="text_dca2c418ac967366" formatted="false">"Unable to save reading status locally. Please try again."</string>
|
||||
<string name="text_5c92da76360056cc" formatted="false">"Unable to delete this announcement. Please try again."</string>
|
||||
<string name="text_615709dd3129313c" formatted="false">"Unable to initiate remote connection"</string>
|
||||
<string name="text_a5c4f4ad79a640ae" formatted="false">"Unable to read open source license"</string>
|
||||
<string name="text_1a13ea5910a0c5e6" formatted="false">"Display"</string>
|
||||
<string name="text_ffe21a671190323e" formatted="false">"Show password"</string>
|
||||
<string name="text_a94460e8a04f04c5" formatted="false">"Not now"</string>
|
||||
<string name="text_6d9a7758610a2432" formatted="false">"No announcements"</string>
|
||||
<string name="text_0b290c6600d92f43" formatted="false">"Recent connections"</string>
|
||||
<string name="text_2388cbc6a486af9f" formatted="false">"Server"</string>
|
||||
<string name="text_dfe408e686175095" formatted="false">"Server TLS certificate verification failed"</string>
|
||||
<string name="text_d3716cc5a2f5a810" formatted="false">"Server Address"</string>
|
||||
<string name="text_2a9608d4465d0f70" formatted="false">"Server login failed, please check the server address and network"</string>
|
||||
<string name="text_d270868960e68629" formatted="false">"Server port"</string>
|
||||
<string name="text_7a4e4a6a1b479a52" formatted="false">"Server connection timed out, please check the server address and network"</string>
|
||||
<string name="text_f95ea7f4c063eea7" formatted="false">"Not enabled"</string>
|
||||
<string name="text_c8f7ed6f2186fb98" formatted="false">"No browser found to open the link"</string>
|
||||
<string name="text_94bc3d40defef3e9" formatted="false">"Unauthorized"</string>
|
||||
<string name="text_4d0680f9efaef147" formatted="false">"Unread"</string>
|
||||
<string name="text_d1fcb66a323a16f5" formatted="false">"Server not connected · Click to try again"</string>
|
||||
<string name="text_7eea7966c0b746c5" formatted="false">"This application uses OpenFEC for forward error correction under CeCILL-C 1.0, which provides limited warranties and limits liability. See the full license for details."</string>
|
||||
<string name="text_c611b4e50b0dbc08" formatted="false">"Local ID "</string>
|
||||
<string name="text_97d10220f08dd7fc" formatted="false">"Local ID {0}"</string>
|
||||
<string name="text_91f15d505770d7a0" formatted="false">"Source code and build instructions for this version"</string>
|
||||
<string name="text_5b2c8ca2c5a87241" formatted="false">"Source revision is unavailable for this build."</string>
|
||||
<string name="text_f0e274aa130e08a2" formatted="false">"Release to refresh"</string>
|
||||
<string name="text_47077144715b7074" formatted="false">"Build instructions"</string>
|
||||
<string name="text_837ab2d0b2f4cb98" formatted="false">"Build recipes and project patches"</string>
|
||||
<string name="text_921c3b62f1f7d6f4" formatted="false">"View source code"</string>
|
||||
<string name="text_fdd1059f10c4b979" formatted="false">"Check failed. Please try again later."</string>
|
||||
<string name="text_7f68ebad19ba6bcd" formatted="false">"Check for updates"</string>
|
||||
<string name="text_4f4d1e704e38c161" formatted="false">"Closing privacy screen…"</string>
|
||||
<string name="text_2a05f6fbd3f4436e" formatted="false">"Establishing secure connection…"</string>
|
||||
<string name="text_0da575e3c92de73a" formatted="false">"Turning on privacy screen..."</string>
|
||||
<string name="text_91834aebecabcbc3" formatted="false">"Checking for updates…"</string>
|
||||
<string name="text_0571a5f5f8ec4c83" formatted="false">"Clearing..."</string>
|
||||
<string name="text_0e6336c8fe918730" formatted="false">"Obtaining local ID..."</string>
|
||||
<string name="text_faebd20c2801fd0f" formatted="false">"Reading selected file..."</string>
|
||||
<string name="text_694186911ae3e4da" formatted="false">"Connecting"</string>
|
||||
<string name="text_c3ccc7f7d37533e3" formatted="false">"Connecting to signaling server..."</string>
|
||||
<string name="text_4024d3c0f467a4f6" formatted="false">"Connecting to server..."</string>
|
||||
<string name="text_46fe1ef52ec3a10f" formatted="false">"Reconnecting to server..."</string>
|
||||
<string name="text_4e4fce649fc00238" formatted="false">"No file picker available for this device"</string>
|
||||
<string name="text_bce2377283c2455a" formatted="false">"Clear"</string>
|
||||
<string name="text_ee0e76b8507212de" formatted="false">"Clear previews"</string>
|
||||
<string name="text_ecc809e44637de18" formatted="false">"Clear previews?"</string>
|
||||
<string name="text_2782e4485ad23d34" formatted="false">"Source code and build instructions"</string>
|
||||
<string name="text_1f566fe8f8b718f3" formatted="false">"Source code version"</string>
|
||||
<string name="text_5f76b2bf82dd2c5d" formatted="false">"Version"</string>
|
||||
<string name="text_16410ee9663fad10" formatted="false">"Copyright and License Statement"</string>
|
||||
<string name="text_11de77e1e41e680f" formatted="false">"Computer keyboard"</string>
|
||||
<string name="text_20f57900234debd7" formatted="false">"Quality"</string>
|
||||
<string name="text_43b04aba53a54cf5" formatted="false">"Video Preference"</string>
|
||||
<string name="text_82a5600c470179fa" formatted="false">"Video latency"</string>
|
||||
<string name="text_33b458faaf967e15" formatted="false">"Video settings"</string>
|
||||
<string name="text_afb5acdf8f07fadb" formatted="false">"Video Quality"</string>
|
||||
<string name="text_12b41b7a3fbcf8ca" formatted="false">"Video Capture Frame Rate"</string>
|
||||
<string name="text_66062971234f422d" formatted="false">"Trackpad"</string>
|
||||
<string name="text_c421dc051bea3f11" formatted="false">"Relative mouse"</string>
|
||||
<string name="text_de32e20193ad1042" formatted="false">"Got it"</string>
|
||||
<string name="text_fac2a67ad87807c4" formatted="false">"OK"</string>
|
||||
<string name="text_36f33adaf0942634" formatted="false">"Confirm"</string>
|
||||
<string name="text_a5b7fba4dbd8f622" formatted="false">"Confirm connection"</string>
|
||||
<string name="text_be1b4f3c6c1cda1e" formatted="false">"Offline"</string>
|
||||
<string name="text_bfa4a6e0fe42ac28" formatted="false">"Space"</string>
|
||||
<string name="text_471a579e8d87faac" formatted="false">"Port range is 1–65535"</string>
|
||||
<string name="text_c7d0c77862e32b52" formatted="false">"Symbols & keys"</string>
|
||||
<string name="text_a562f07bd38c1a49" formatted="false">"Wait for the computer to return to the privacy screen state; older computers may not support it."</string>
|
||||
<string name="text_5ee880e2524a983a" formatted="false">"Waiting for remote screen information"</string>
|
||||
<string name="text_674c723a85ee7f57" formatted="false">"Waiting for the remote screen..."</string>
|
||||
<string name="text_119f0483da4756c4" formatted="false">"Waiting for privacy authorization"</string>
|
||||
<string name="text_7916071c1a8ee458" formatted="false">"System keyboard"</string>
|
||||
<string name="text_02e9ba6fda719e2e" formatted="false">"Direct touch"</string>
|
||||
<string name="text_8527c8c6d1ccc1ca" formatted="false">"Absolute Mouse"</string>
|
||||
<string name="text_872428a7210d5e0c" formatted="false">"Network"</string>
|
||||
<string name="text_4eafa9e925b30bcd" formatted="false">"Custom"</string>
|
||||
<string name="text_dffd0c12b8f27247" formatted="false">"Virtual mouse"</string>
|
||||
<string name="text_62c199f37bf95869" formatted="false">"The remote privacy screen was unlocked using the local shortcut. Lock it again to restore privacy and disable local shortcut unlocking."</string>
|
||||
<string name="text_a569ce729c1d6e19" formatted="false">"The controlled version is too low, please upgrade to the latest version."</string>
|
||||
<string name="text_d814476826827b78" formatted="false">"Remote encoder"</string>
|
||||
<string name="text_488402883e27f6f0" formatted="false">"Text to be sent to the remote end"</string>
|
||||
<string name="text_c20f7618d330a854" formatted="false">"Video"</string>
|
||||
<string name="text_310c625425f014d8" formatted="false">"License"</string>
|
||||
<string name="text_5c34f7d0e2124dca" formatted="false">"Licenses and copyright notices"</string>
|
||||
<string name="text_e24134b643a17975" formatted="false">"The license and copyright notice are available offline."</string>
|
||||
<string name="text_5e7628f043ce71d0" formatted="false">"Device identity failed to save, please check secure storage"</string>
|
||||
<string name="text_5d35ee0d7945fcbc" formatted="false">"Device identity and selected saved access passwords are encrypted using Android Keystore. Connection records are only saved locally and are not included in device backup. The clipboard contents are cleared when the session is ended. Stay in session with picture-in-picture or connected notifications after leaving the app, and disconnect within the app or notifications."</string>
|
||||
<string name="text_df3d58c7d84b85f2" formatted="false">"Settings"</string>
|
||||
<string name="text_4f8771952222a011" formatted="false">"Settings, there is a new version"</string>
|
||||
<string name="text_f4f712d5be91721a" formatted="false">"Access password"</string>
|
||||
<string name="text_dd6c465847af1b60" formatted="false">"The access password is wrong, please re-enter it."</string>
|
||||
<string name="text_defba1d2989bfab4" formatted="false">"Certificate verification failed · Click to try again"</string>
|
||||
<string name="text_44f3d4d52347095a" formatted="false">"Please complete the online authorization first"</string>
|
||||
<string name="text_2a07ca002bc5093d" formatted="false">"Please enable the CrossDesk system service on the remote end and try again"</string>
|
||||
<string name="text_990fd23c62de5ccf" formatted="false">"The request timed out and the computer\'s privacy screen status has not been confirmed."</string>
|
||||
<string name="text_e5c8fa6dcf7b2a8b" formatted="false">"Please wait for the current file to be sent."</string>
|
||||
<string name="text_9305d9d72b06110d" formatted="false">"Please enter a port in the range 1–65535"</string>
|
||||
<string name="text_3c7223dfb8dde5cf" formatted="false">"Enter a 9-digit device ID"</string>
|
||||
<string name="text_285a0714894d2899" formatted="false">"Please enter a valid access password"</string>
|
||||
<string name="text_c808bb77ffd2b4cd" formatted="false">"Could not apply settings. Try again."</string>
|
||||
<string name="text_a762e7e650e285fa" formatted="false">"Software license"</string>
|
||||
<string name="text_572cf45ba43634b3" formatted="false">"Back"</string>
|
||||
<string name="text_7565f94031582c7c" formatted="false">"Return to control bar"</string>
|
||||
<string name="text_55113b6925c362fd" formatted="false">"No connection record yet"</string>
|
||||
<string name="text_3aa527b2106cc987" formatted="false">"This announcement has been updated or deleted, please return to the announcement list to view it."</string>
|
||||
<string name="text_e357aa9a0fb28c16" formatted="false">"Remote task is closed"</string>
|
||||
<string name="text_c255841f68096d3b" formatted="false">"Remote Desktop"</string>
|
||||
<string name="text_133f4c7dabd88b97" formatted="false">"Remote desktop touch area"</string>
|
||||
<string name="text_5931aeb3778f28f1" formatted="false">"Remote desktop: Touch and click, drag to move, press and hold the right button, pinch to zoom, zoom in and drag the screen with two fingers, slide up and down with two fingers to scroll at the original scale"</string>
|
||||
<string name="text_3535087994d050de" formatted="false">"Remote computer"</string>
|
||||
<string name="text_ed2e03072ce131de" formatted="false">"The remote window has been closed, or the background keepalive failed to start, and the session has ended."</string>
|
||||
<string name="text_678b783fb578172b" formatted="false">"Remote connection"</string>
|
||||
<string name="text_deccbd3eb9ba45f2" formatted="false">"Incorrect remote password"</string>
|
||||
<string name="text_a2ce499295e358ae" formatted="false">"The remote end has closed the connection"</string>
|
||||
<string name="text_176e117df3ace734" formatted="false">"The remote end is currently unavailable"</string>
|
||||
<string name="text_d2c0694243f93249" formatted="false">"The remote access password has expired, please reconnect."</string>
|
||||
<string name="text_a5574109f0208e89" formatted="false">"Connect"</string>
|
||||
<string name="text_1d54bfbae5f8ac08" formatted="false">"Connection "</string>
|
||||
<string name="text_89c4766afc85125f" formatted="false">"Connection failed"</string>
|
||||
<string name="text_04bf1a61ba1369f6" formatted="false">"Connection failed · Click to try again"</string>
|
||||
<string name="text_2ab7d89557e779ab" formatted="false">"Connection lost"</string>
|
||||
<string name="text_c1d20bf4c3e471c1" formatted="false">"Connection ended"</string>
|
||||
<string name="text_e57d2cbc0e9908e6" formatted="false">"Connection delay (RTT)"</string>
|
||||
<string name="text_db4d4f0fab78498a" formatted="false">"Available when connected to a supported remote device."</string>
|
||||
<string name="text_2123a9ed718f2c40" formatted="false">"Lock landscape screen after connection is established"</string>
|
||||
<string name="text_f741e0536210eafa" formatted="false">"Connection prompt"</string>
|
||||
<string name="text_485a26050ce57431" formatted="false">"Connection method"</string>
|
||||
<string name="text_5a048351a77fe88d" formatted="false">"Connect to server"</string>
|
||||
<string name="text_5854c4add7ab0f7a" formatted="false">"Connect to the server to get the latest announcements."</string>
|
||||
<string name="text_32cd27b5a72515f3" formatted="false">"Connection timed out, please check the device ID, server and remote online status"</string>
|
||||
<string name="text_6edc4b54cc1a9838" formatted="false">"Connect to remote desktop"</string>
|
||||
<string name="text_d5407b4f3c666681" formatted="false">"Backspace"</string>
|
||||
<string name="text_e02926784fcedb91" formatted="false">"Announcements"</string>
|
||||
<string name="text_465a092680d132b3" formatted="false">"Some previews could not be cleared, please try again."</string>
|
||||
<string name="text_b8784c8dd5636ff2" formatted="false">"Try again"</string>
|
||||
<string name="text_a504fb6f162bdb20" formatted="false">"Retry saving reading status"</string>
|
||||
<string name="text_ff9872a023e3a8aa" formatted="false">"Lock"</string>
|
||||
<string name="text_864200bbb241e067" formatted="false">"Keyboard"</string>
|
||||
<string name="text_1f0f014d4177a0cc" formatted="false">"Read and authorize"</string>
|
||||
<string name="text_86651d17a401c55b" formatted="false">"Privacy"</string>
|
||||
<string name="text_26c6e011e343708c" formatted="false">"Privacy and Authorization"</string>
|
||||
<string name="text_6cad42fd3da85585" formatted="false">"Privacy screen"</string>
|
||||
<string name="text_e157fafb76aad954" formatted="false">"Privacy screen off"</string>
|
||||
<string name="text_bbef0eac0c04f214" formatted="false">"Privacy screen on"</string>
|
||||
<string name="text_0c7535f7c5e43b6f" formatted="false">"Privacy screen unlocked"</string>
|
||||
<string name="text_b62ac3dfb606c00b" formatted="false">"Privacy screen abnormality"</string>
|
||||
<string name="text_598a2747dbd21dd5" formatted="false">"Privacy screen is temporarily unavailable"</string>
|
||||
<string name="text_4d59be3c75189032" formatted="false">"Privacy screen status has expired, waiting for computer confirmation"</string>
|
||||
<string name="text_1674532c70ed1db2" formatted="false">"Privacy screen status to be confirmed"</string>
|
||||
<string name="text_e464b41fd5c45ba5" formatted="false">"Privacy Policy"</string>
|
||||
<string name="text_554e957e8e3c00af" formatted="false">"The privacy policy cannot be read temporarily. Please refuse to connect to the Internet and reinstall the application."</string>
|
||||
<string name="text_7db2e189ecb01065" formatted="false">"Mute"</string>
|
||||
<string name="text_296c632ec857a0ba" formatted="false">"Audio"</string>
|
||||
<string name="text_0cadb49096a382e0" formatted="false">"Project website"</string>
|
||||
<string name="text_3beca60bd0c654f7" formatted="false">"Previews may contain personal or work information from the remote computer. After turning it on, each connection will automatically save a picture on the machine and display it in recent connections. You can close and clear at any time."</string>
|
||||
<string name="text_b1c27820fec23edb" formatted="false">"High"</string>
|
||||
<string name="text_844b8cc8dff7c1d8" formatted="false">"Default"</string>
|
||||
<string name="text_4db8737054f3b0cc" formatted="false">"Off by default. When turned on, the screen will enter and remain in landscape orientation when the phone is connected; when turned off, it will follow the direction of the device. Tablets and multi-windows are not affected."</string>
|
||||
<string name="text_1184a97edd8e1646" formatted="false">"Right mouse button, press and drag, release to end dragging"</string>
|
||||
<string name="text_926eaa3af0b5e48e" formatted="false">"Left mouse button, press and drag, release to end dragging"</string>
|
||||
<string name="text_269f39a07b4a4e01" formatted="false">"Mouse control"</string>
|
||||
<string name="text_cba62aeeb4a63a23" formatted="false">"Mouse wheel, slide up and down to slide out of the key range"</string>
|
||||
<string name="text_9aa238fc6121dede" formatted="false">", packet loss rate "</string>
|
||||
<string name="text_0b5d16c21095fe10" formatted="false">", send "</string>
|
||||
<string name="text_ba9a792d063a8c86" formatted="false">", password saved"</string>
|
||||
<string name="text_f4c1f9d7896760a7" formatted="false">", receive "</string>
|
||||
<string name="text_42237b5ec70c26f3" formatted="false">", click Retry"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="zh-Hans" />
|
||||
<locale android:name="zh-Hant" />
|
||||
<locale android:name="ja" />
|
||||
<locale android:name="ko" />
|
||||
<locale android:name="de" />
|
||||
<locale android:name="fr" />
|
||||
<locale android:name="es" />
|
||||
<locale android:name="pt-BR" />
|
||||
<locale android:name="ru" />
|
||||
</locale-config>
|
||||
@@ -7,35 +7,12 @@
|
||||
#ifndef _CONTROLLER_PROTOCOL_H_
|
||||
#define _CONTROLLER_PROTOCOL_H_
|
||||
|
||||
#include <minirtc.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "../../../libs/controller/include/mobile_controller_protocol.h"
|
||||
namespace crossdesk::android_controller {
|
||||
inline std::string SecureAttentionMessage() {
|
||||
RemoteAction action{};
|
||||
action.type = service_command;
|
||||
action.c.flag = send_sas;
|
||||
return action.to_json();
|
||||
using mobile_controller::SecureAttentionMessage;
|
||||
using mobile_controller::PrivacyMessage;
|
||||
using mobile_controller::VideoSettingsMessage;
|
||||
using mobile_controller::NetworkReport;
|
||||
}
|
||||
inline std::string VideoSettingsMessage(int quality, int frame_rate, int preference,
|
||||
uint32_t request_id) {
|
||||
RemoteAction action{};
|
||||
action.type = video_settings;
|
||||
action.vs = {quality, frame_rate, preference, request_id, false};
|
||||
return action.to_json();
|
||||
}
|
||||
|
||||
inline nlohmann::json NetworkReport(const MiniRtcNetTrafficStats& stats, TraversalMode mode) {
|
||||
const auto traffic = [](const MiniRtcInboundStats& in, const MiniRtcOutboundStats& out) {
|
||||
return nlohmann::json{{"inbound", in.bitrate}, {"outbound", out.bitrate}, {"loss", in.loss_rate}};
|
||||
};
|
||||
return {{"rtt", stats.rtt_ms}, {"mode", mode}, {"srtp", stats.srtp_active},
|
||||
{"video", traffic(stats.video_inbound_stats, stats.video_outbound_stats)},
|
||||
{"audio", traffic(stats.audio_inbound_stats, stats.audio_outbound_stats)},
|
||||
{"data", traffic(stats.data_inbound_stats, stats.data_outbound_stats)},
|
||||
{"total", traffic(stats.total_inbound_stats, stats.total_outbound_stats)}};
|
||||
}
|
||||
} // namespace crossdesk::android_controller
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,265 +7,9 @@
|
||||
#ifndef _FILE_TRANSFER_SESSION_H_
|
||||
#define _FILE_TRANSFER_SESSION_H_
|
||||
|
||||
#include <file_transfer_format.h>
|
||||
#include <stream_names.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "../../../libs/controller/include/mobile_file_transfer_session.h"
|
||||
namespace crossdesk::android_controller {
|
||||
using mobile_controller::FileTransferSession;
|
||||
}
|
||||
|
||||
// File I/O is bounded to one chunk per pump. Feedback is delivered by MiniRTC
|
||||
// callback threads; no callback waits for the RTC executor or for a remote ACK.
|
||||
class FileTransferSession {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Send = std::function<int(const char*, const char*, size_t)>;
|
||||
using Progress = std::function<void(const std::string&, double, bool,
|
||||
const std::string&)>;
|
||||
FileTransferSession(Send send, Progress progress)
|
||||
: send_(std::move(send)), progress_(std::move(progress)) {}
|
||||
~FileTransferSession() {
|
||||
if (outgoing_) {
|
||||
outgoing_->input.close();
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(outgoing_->path, ec);
|
||||
}
|
||||
for (auto& entry : incoming_) Remove(entry.second);
|
||||
}
|
||||
|
||||
void SetDirectory(std::filesystem::path directory) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
directory_ = std::move(directory);
|
||||
}
|
||||
|
||||
bool Start(const std::filesystem::path& path, const std::string& name,
|
||||
Clock::time_point now = Clock::now()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (outgoing_ || name.empty() || name.size() > UINT16_MAX) return false;
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(path, ec)) return false;
|
||||
const auto size = std::filesystem::file_size(path, ec);
|
||||
if (ec) return false;
|
||||
Outgoing file;
|
||||
file.input.open(path, std::ios::binary);
|
||||
if (!file.input) return false;
|
||||
file.id = ++next_id_;
|
||||
file.name = name;
|
||||
file.path = path;
|
||||
file.total = size;
|
||||
file.updated = now;
|
||||
outgoing_ = std::move(file);
|
||||
progress_(name, 0, true, "");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Pump(Clock::time_point now = Clock::now()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto it = incoming_.begin(); it != incoming_.end();) {
|
||||
if (now - it->second.updated < timeout_) { ++it; continue; }
|
||||
Reject(it->first, it->second.total, it->second.received);
|
||||
progress_(it->second.name, -1, false, "");
|
||||
Remove(it->second);
|
||||
it = incoming_.erase(it);
|
||||
}
|
||||
if (!outgoing_) return;
|
||||
auto& file = *outgoing_;
|
||||
if (now - file.updated >= timeout_) { FinishSend(false); return; }
|
||||
if (file.last || file.sent - file.acked >= 1024 * 1024) return;
|
||||
std::array<char, kFileChunkSize> payload{};
|
||||
const auto size = static_cast<uint32_t>(std::min<uint64_t>(payload.size(), file.total - file.sent));
|
||||
if (size) {
|
||||
file.input.read(payload.data(), size);
|
||||
if (file.input.gcount() != size) { FinishSend(false); return; }
|
||||
}
|
||||
const bool first = file.sent == 0;
|
||||
const bool last = file.sent + size == file.total;
|
||||
const auto chunk = EncodeFileChunk(file.id, file.sent, file.total,
|
||||
payload.data(), size, first ? &file.name : nullptr, first, last);
|
||||
if (chunk.empty() || send_(kFileStream, chunk.data(), chunk.size()) != 0) {
|
||||
FinishSend(false); return;
|
||||
}
|
||||
file.sent += size;
|
||||
file.last = last;
|
||||
}
|
||||
|
||||
void Ack(const char* data, size_t size, Clock::time_point now = Clock::now()) {
|
||||
FileTransferAck ack{};
|
||||
if (!DecodeFileTransferAck(data, size, &ack)) return;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!outgoing_ || ack.file_id != outgoing_->id) return;
|
||||
auto& file = *outgoing_;
|
||||
if (ack.total_size != file.total || ack.acked_offset > file.sent ||
|
||||
(ack.flags & ~3u)) return;
|
||||
if (ack.flags & 2) { FinishSend(false); return; }
|
||||
if (ack.flags & 1) {
|
||||
if (file.last && ack.acked_offset == file.total) FinishSend(true);
|
||||
return;
|
||||
}
|
||||
if (ack.acked_offset <= file.acked) return;
|
||||
file.acked = ack.acked_offset;
|
||||
file.updated = now;
|
||||
Report(file.name, file.acked, file.total, true, file.percent);
|
||||
}
|
||||
|
||||
void Receive(const char* data, size_t size, Clock::time_point now = Clock::now()) {
|
||||
FileChunkView chunk;
|
||||
if (!DecodeFileChunk(data, size, &chunk)) return;
|
||||
const auto& h = chunk.header;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = incoming_.find(h.file_id);
|
||||
const bool first = h.flags & 1, last = h.flags & 2;
|
||||
const bool invalid = (h.flags & ~3u) || h.total_size > INT64_MAX ||
|
||||
(first && h.offset != 0) || chunk.payload_size > kFileChunkSize ||
|
||||
size != sizeof(FileChunkHeader) + h.name_len + chunk.payload_size ||
|
||||
(!first && h.name_len) || (!chunk.payload_size && !last) ||
|
||||
last != (h.offset + chunk.payload_size == h.total_size);
|
||||
if (invalid || (it == incoming_.end() && incoming_.size() >= 4)) {
|
||||
Reject(h.file_id, h.total_size, h.offset);
|
||||
if (it != incoming_.end()) { progress_(it->second.name, -1, false, ""); Remove(it->second); incoming_.erase(it); }
|
||||
return;
|
||||
}
|
||||
if (it == incoming_.end()) {
|
||||
Incoming file;
|
||||
file.name = SafeName(chunk.file_name);
|
||||
file.total = h.total_size;
|
||||
std::error_code ec;
|
||||
if (!directory_.empty()) std::filesystem::create_directories(directory_, ec);
|
||||
// A per-transfer directory avoids overwrites, even for duplicate names.
|
||||
auto folder = directory_ / (std::to_string(h.file_id) + "-" + std::to_string(++receive_id_));
|
||||
if (directory_.empty() || ec || !std::filesystem::create_directory(folder, ec)) {
|
||||
Reject(h.file_id, h.total_size, 0); progress_(file.name, -1, false, ""); return;
|
||||
}
|
||||
file.path = folder / file.name;
|
||||
file.partial = folder / ".partial";
|
||||
file.output.open(file.partial, std::ios::binary | std::ios::trunc);
|
||||
if (!file.output) {
|
||||
Reject(h.file_id, h.total_size, 0); progress_(file.name, -1, false, ""); Remove(file); return;
|
||||
}
|
||||
it = incoming_.emplace(h.file_id, std::move(file)).first;
|
||||
}
|
||||
auto& file = it->second;
|
||||
if (h.total_size != file.total || (first && file.first) ||
|
||||
!AddRange(file, h.offset, h.offset + chunk.payload_size)) {
|
||||
Reject(h.file_id, h.total_size, h.offset);
|
||||
progress_(file.name, -1, false, ""); Remove(file); incoming_.erase(it); return;
|
||||
}
|
||||
if (first) {
|
||||
file.first = true;
|
||||
file.name = SafeName(chunk.file_name);
|
||||
file.path = file.path.parent_path() / file.name;
|
||||
}
|
||||
file.last |= last;
|
||||
// KCP removes messages in order, but delivers batches outside its lock on
|
||||
// both RTP and timer threads. Track coverage instead of assuming callbacks
|
||||
// arrive in order; ACK only the contiguous prefix and never expose holes.
|
||||
file.received = !file.ranges.empty() && file.ranges.begin()->first == 0
|
||||
? file.ranges.begin()->second : 0;
|
||||
const bool complete = file.first && file.last && file.received == file.total;
|
||||
file.output.seekp(static_cast<std::streamoff>(h.offset));
|
||||
file.output.write(chunk.payload, chunk.payload_size);
|
||||
if (complete) { file.output.flush(); file.output.close(); }
|
||||
std::error_code ec;
|
||||
if (complete && !file.output.fail()) std::filesystem::rename(file.partial, file.path, ec);
|
||||
if (file.output.fail() || ec) {
|
||||
Reject(h.file_id, h.total_size, h.offset);
|
||||
progress_(file.name, -1, false, ""); Remove(file); incoming_.erase(it); return;
|
||||
}
|
||||
file.updated = now;
|
||||
const FileTransferAck ack{kFileAckMagic, h.file_id, file.received, file.total, complete ? 1u : 0u};
|
||||
const auto packet = EncodeFileTransferAck(ack);
|
||||
send_(kFileFeedbackStream, packet.data(), packet.size());
|
||||
if (complete) {
|
||||
progress_(file.name, 1, false, file.path.string());
|
||||
incoming_.erase(it);
|
||||
} else Report(file.name, file.received, file.total, false, file.percent);
|
||||
}
|
||||
|
||||
bool Sending() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return outgoing_.has_value();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Outgoing {
|
||||
uint32_t id = 0;
|
||||
std::string name;
|
||||
std::filesystem::path path;
|
||||
std::ifstream input;
|
||||
uint64_t total = 0, sent = 0, acked = 0;
|
||||
bool last = false;
|
||||
int percent = -1;
|
||||
Clock::time_point updated;
|
||||
};
|
||||
struct Incoming {
|
||||
std::string name;
|
||||
std::filesystem::path path, partial;
|
||||
std::ofstream output;
|
||||
uint64_t total = 0, received = 0;
|
||||
bool first = false, last = false;
|
||||
std::map<uint64_t, uint64_t> ranges;
|
||||
int percent = -1;
|
||||
Clock::time_point updated;
|
||||
};
|
||||
static bool AddRange(Incoming& file, uint64_t start, uint64_t end) {
|
||||
if (start == end) return true;
|
||||
auto next = file.ranges.lower_bound(start);
|
||||
if (next != file.ranges.end() && next->first < end) return false;
|
||||
if (next != file.ranges.begin()) {
|
||||
auto previous = std::prev(next);
|
||||
if (previous->second > start) return false;
|
||||
if (previous->second == start) { start = previous->first; file.ranges.erase(previous); }
|
||||
}
|
||||
if (next != file.ranges.end() && next->first == end) { end = next->second; file.ranges.erase(next); }
|
||||
file.ranges.emplace(start, end);
|
||||
return file.ranges.size() <= 1024;
|
||||
}
|
||||
static std::string SafeName(std::string name) {
|
||||
const auto slash = name.find_last_of("/\\");
|
||||
if (slash != std::string::npos) name.erase(0, slash + 1);
|
||||
for (auto& ch : name) if (static_cast<unsigned char>(ch) < 32 || ch == ':') ch = '_';
|
||||
if (name.empty() || name == "." || name == ".." || name == ".partial" || name.size() > 240) return "received_file";
|
||||
return name;
|
||||
}
|
||||
static void Remove(Incoming& file) {
|
||||
file.output.close();
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(file.partial, ec);
|
||||
std::filesystem::remove(file.path.parent_path(), ec);
|
||||
}
|
||||
void Reject(uint32_t id, uint64_t total, uint64_t offset) {
|
||||
const auto packet = EncodeFileTransferAck({kFileAckMagic, id, offset, total, 2});
|
||||
send_(kFileFeedbackStream, packet.data(), packet.size());
|
||||
}
|
||||
void Report(const std::string& name, uint64_t offset, uint64_t total, bool sending, int& previous) {
|
||||
const double progress = total ? static_cast<double>(offset) / total : 0;
|
||||
const int percent = static_cast<int>(progress * 100);
|
||||
if (percent != previous) { previous = percent; progress_(name, std::min(progress, .99), sending, ""); }
|
||||
}
|
||||
void FinishSend(bool success) {
|
||||
progress_(outgoing_->name, success ? 1 : -1, true, "");
|
||||
outgoing_->input.close();
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(outgoing_->path, ec);
|
||||
outgoing_.reset();
|
||||
}
|
||||
const std::chrono::seconds timeout_{30};
|
||||
std::mutex mutex_;
|
||||
Send send_;
|
||||
Progress progress_;
|
||||
std::filesystem::path directory_;
|
||||
std::optional<Outgoing> outgoing_;
|
||||
std::map<uint32_t, Incoming> incoming_;
|
||||
uint32_t next_id_ = 0;
|
||||
uint64_t receive_id_ = 0;
|
||||
};
|
||||
} // namespace crossdesk::android_controller
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -370,7 +370,8 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nConnect)(
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv*, jclass, jlong handle) {
|
||||
auto* s = From(handle); if (!s || !s->controller) return;
|
||||
auto action = MakeHostInformation("CrossDesk Android", {}, false, "1.6.0", HostPlatform::Android);
|
||||
auto action = MakeHostInformation("CrossDesk Android", {}, true, "1.6.0", HostPlatform::Android);
|
||||
action.i.supports_video_codec_status = true;
|
||||
s->Send(kControlStream, action.to_json()); FreeRemoteAction(action);
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nPointer)(
|
||||
@@ -392,6 +393,17 @@ extern "C" JNIEXPORT void JNICALL JNI_METHOD(nKey)(JNIEnv*, jclass, jlong handle
|
||||
if (down) s->pressed_keys.insert(key); else s->pressed_keys.erase(key);
|
||||
s->Send(kKeyboardStream, action.to_json());
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nText)(JNIEnv* env, jclass, jlong handle, jbyteArray bytes) {
|
||||
auto* s = From(handle); if (!s || !bytes) return;
|
||||
const auto size = env->GetArrayLength(bytes);
|
||||
if (size <= 0 || static_cast<size_t>(size) > kMaxTextInputBytes) return;
|
||||
RemoteAction action{}; action.type = text_input;
|
||||
action.text.resize(size);
|
||||
env->GetByteArrayRegion(bytes, 0, size, reinterpret_cast<jbyte*>(action.text.data()));
|
||||
if (env->ExceptionCheck() || action.text.find('\0') != std::string::npos) return;
|
||||
// Java supplies standard UTF-8, including supplementary Unicode characters.
|
||||
s->Send(kKeyboardStream, action.to_json());
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nControl)(
|
||||
JNIEnv*, jclass, jlong handle, jint type, jint value) {
|
||||
auto* s = From(handle); if (!s) return;
|
||||
@@ -418,6 +430,14 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nSecureAttention)(JNIEnv*, jcla
|
||||
const auto data = android_controller::SecureAttentionMessage();
|
||||
return SendReliableDataFrame(s->controller, data.data(), data.size(), kControlStream) == 0;
|
||||
}
|
||||
extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nPrivacy)(
|
||||
JNIEnv*, jclass, jlong handle, jint flag, jboolean block_input) {
|
||||
auto* s = From(handle);
|
||||
if (!s || !s->controller || s->stopping) return false;
|
||||
const auto data = android_controller::PrivacyMessage(flag, block_input);
|
||||
return !data.empty() &&
|
||||
SendReliableDataFrame(s->controller, data.data(), data.size(), kControlStream) == 0;
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nFileDirectory)(JNIEnv* env, jclass, jlong handle, jstring directory) {
|
||||
auto* s = From(handle); if (s) s->files.SetDirectory(String(env, directory));
|
||||
}
|
||||
|
||||
@@ -7,21 +7,9 @@
|
||||
#ifndef _VIDEO_TIMING_H_
|
||||
#define _VIDEO_TIMING_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../../libs/controller/include/mobile_video_timing.h"
|
||||
namespace crossdesk::android_controller {
|
||||
inline double VideoLatencyMilliseconds(uint64_t captured_us,
|
||||
uint64_t received_us,
|
||||
int64_t submitted_us) {
|
||||
// MiniRTC calibrates capture time when the frame is reassembled. Validate
|
||||
// that mapping at reception, not after decoding/Surface waits: a real local
|
||||
// backlog over five seconds must remain visible in the latency measurement.
|
||||
if (!captured_us || received_us < captured_us || submitted_us <= 0 ||
|
||||
received_us > static_cast<uint64_t>(submitted_us) ||
|
||||
received_us - captured_us > 5'000'000)
|
||||
return -1;
|
||||
return (static_cast<uint64_t>(submitted_us) - captured_us) / 1000.0;
|
||||
using mobile_controller::VideoLatencyMilliseconds;
|
||||
}
|
||||
} // namespace crossdesk::android_controller
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,6 @@ ANDROID = Path(__file__).resolve().parents[1]
|
||||
ROOT = ANDROID.parents[1]
|
||||
SOURCE = ROOT / 'deps/submodules/minirtc'
|
||||
CACHE = ANDROID / '.native'
|
||||
PACKAGE_REVISION = 'eda39de3fb99b420c168f1ab9ab2d1791e11b662'
|
||||
XMAKE_MIN_VERSION = (3, 1, 1)
|
||||
|
||||
|
||||
@@ -38,18 +37,12 @@ def main():
|
||||
raise SystemExit('Initialize the MiniRTC submodule before building.')
|
||||
env = os.environ.copy()
|
||||
env['XMAKE_PKG_INSTALLDIR'] = str(CACHE / 'packages')
|
||||
env['XMAKE_GLOBALDIR'] = str(CACHE / 'global')
|
||||
env['ANDROID_NDK_HOME'] = str(ndk)
|
||||
env['ANDROID_NDK_ROOT'] = str(ndk)
|
||||
env['NO_COLOR'] = '1'
|
||||
work = ANDROID / 'native'
|
||||
repository = CACHE / 'global/.xmake/repositories/xmake-repo'
|
||||
if not (repository / '.git').is_dir():
|
||||
subprocess.run([xmake, 'repo', '--update'], cwd=work, env=env, check=True)
|
||||
revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repository, text=True).strip()
|
||||
if revision != PACKAGE_REVISION:
|
||||
subprocess.run(['git', 'fetch', 'origin', PACKAGE_REVISION], cwd=repository, check=True)
|
||||
subprocess.run(['git', 'checkout', '--detach', PACKAGE_REVISION], cwd=repository, check=True)
|
||||
# Use the desktop repository policy; Xmake manages recipe initialization
|
||||
# and updates. Only installed Android packages/build products are isolated.
|
||||
subprocess.run([xmake, 'f', '-P', str(work), '-y', '-p', 'android', '-a', 'arm64-v8a',
|
||||
'-m', 'release', '--ndk=' + str(ndk), '--ndk_sdkver=26',
|
||||
'--runtimes=c++_static', '--policies=package.precompiled:n',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Package existing reviewed notices for the Android runtime dependency set."""
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
@@ -70,8 +69,7 @@ def write_pages(output, catalog, additional, revision, minirtc):
|
||||
target = output / asset
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(document['text'], encoding='utf-8')
|
||||
documents.append({'title': document['title'], 'asset': asset,
|
||||
'sha256': hashlib.sha256(document['text'].encode('utf-8')).hexdigest()})
|
||||
documents.append({'title': document['title'], 'asset': asset})
|
||||
component['documents'] = documents
|
||||
(output / 'ThirdPartyLicenses.json').write_text(json.dumps(
|
||||
{'schemaVersion': 1, 'components': components}, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"""Check the shipped native ABI, ELF load alignment and APK zip alignment."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
@@ -40,33 +39,15 @@ def verify(path):
|
||||
name, extra = struct.unpack('<HH', raw.read(4))
|
||||
if (library.header_offset + 30 + name + extra) % 16384:
|
||||
raise ValueError('APK native library entry is not 16 KB aligned')
|
||||
if 'assets/THIRD_PARTY_NOTICES.txt' not in archive.namelist():
|
||||
raise ValueError('Missing third-party notices')
|
||||
if archive.read('assets/PRIVACY.md') != (Path(__file__).resolve().parents[3] / 'PRIVACY.md').read_bytes():
|
||||
raise ValueError('Missing or outdated bundled privacy policy')
|
||||
catalog = json.loads(archive.read('assets/ThirdPartyLicenses.json'))
|
||||
source = json.loads(archive.read('assets/SourceMetadata.json'))
|
||||
components = catalog['components']
|
||||
ids = [item['id'] for item in components]
|
||||
if catalog['schemaVersion'] != 1 or len(ids) != len(set(ids)) or not {
|
||||
'crossdesk', 'minirtc', 'openfec', 'openh264', 'libiconv', 'android-ndk', 'svt-av1'}.issubset(ids):
|
||||
raise ValueError('Incomplete About catalog')
|
||||
for component in components:
|
||||
if not component['documents']:
|
||||
raise ValueError('Missing component documents')
|
||||
for document in component['documents']:
|
||||
text = archive.read('assets/'+document['asset'])
|
||||
if not text or hashlib.sha256(text).hexdigest() != document['sha256']:
|
||||
raise ValueError('Incomplete license document: '+document['asset'])
|
||||
for document in component.get('buildDocuments', []):
|
||||
if not archive.read('assets/'+document['asset']):
|
||||
raise ValueError('Missing build document')
|
||||
if source['schemaVersion'] != 1 or len(source['revision']) != 40:
|
||||
raise ValueError('Missing source revision')
|
||||
for document in source['buildDocuments']:
|
||||
if not archive.read('assets/'+document['asset']):
|
||||
raise ValueError('Missing source build instructions')
|
||||
print(f'PASS: {Path(path).name}: arm64, 16 KB ELF/ZIP alignment, privacy policy, notices, About catalog and documents')
|
||||
print(f'PASS: {Path(path).name}: arm64, 16 KB ELF/ZIP alignment, privacy policy and source metadata')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" name="CrossDesk.SessionHelper" type="win32" />
|
||||
<description>CrossDesk Session Helper</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -4,13 +4,10 @@ IDI_ICON1 ICON "crossdesk.ico"
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_PORTABLE
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
#elif defined(CROSSDESK_DEBUG)
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
// The service supplies either the user's token (desktop observer) or a SYSTEM
|
||||
// token (secure capture/input). Requiring elevation here prevents the observer
|
||||
// from starting with CreateProcessAsUserW in installed release builds (740).
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_session_helper.manifest"
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" name="CrossDesk.Updater" type="win32"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
@@ -0,0 +1,5 @@
|
||||
1 24 "crossdesk_updater.manifest"
|
||||
IDI_ICON1 ICON "crossdesk.ico"
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Updater"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_updater.exe"
|
||||
#include "version_info.rcinc"
|
||||
@@ -111,6 +111,7 @@ DESKTOP_DIR="$DEB_DIR/usr/share/applications"
|
||||
|
||||
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
|
||||
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
|
||||
install -m 0755 "$(dirname "$BUILD_BINARY")/crossdesk_updater" "$PRIVATE_DIR/crossdesk_updater"
|
||||
|
||||
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
|
||||
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
|
||||
@@ -156,7 +157,7 @@ for size in 16 24 32 48 64 96 128 256; do
|
||||
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
|
||||
done
|
||||
|
||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
|
||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS, policykit-1"
|
||||
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
|
||||
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
|
||||
fi
|
||||
|
||||
@@ -51,6 +51,8 @@ mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
|
||||
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
cp "$(dirname "${EXECUTABLE_PATH}")/crossdesk_updater" "${MACOS_DIR}/crossdesk_updater"
|
||||
chmod +x "${MACOS_DIR}/crossdesk_updater"
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
|
||||
@@ -51,6 +51,8 @@ mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
|
||||
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
cp "$(dirname "${EXECUTABLE_PATH}")/crossdesk_updater" "${MACOS_DIR}/crossdesk_updater"
|
||||
chmod +x "${MACOS_DIR}/crossdesk_updater"
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
|
||||
@@ -71,6 +71,19 @@ verify_dependencies() {
|
||||
}
|
||||
|
||||
verify_dependencies "$APP_EXECUTABLE"
|
||||
UPDATER="$APP_BUNDLE/Contents/MacOS/crossdesk_updater"
|
||||
if [[ ! -x "$UPDATER" ]] || ! lipo "$UPDATER" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "Missing updater or incorrect updater architecture" >&2
|
||||
exit 1
|
||||
fi
|
||||
# The updater runs outside the bundle during installation, so it must not rely
|
||||
# on private frameworks that are being replaced.
|
||||
while IFS= read -r dependency; do
|
||||
case "$dependency" in
|
||||
/System/Library/*|/usr/lib/*) ;;
|
||||
*) echo "Updater has a non-system dynamic dependency: $dependency" >&2; exit 1 ;;
|
||||
esac
|
||||
done < <(otool -L "$UPDATER" | tail -n +2 | awk '{ print $1 }')
|
||||
|
||||
SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
|
||||
@@ -39,6 +39,18 @@ RequestExecutionLevel admin
|
||||
|
||||
; Include LogicLib for process handling
|
||||
!include "LogicLib.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
Var UpdateMode
|
||||
|
||||
Function .onInit
|
||||
${GetParameters} $0
|
||||
ClearErrors
|
||||
${GetOptions} $0 "/CROSSDESK_UPDATE" $1
|
||||
StrCpy $UpdateMode 0
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $UpdateMode 1
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||
@@ -55,6 +67,11 @@ InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
|
||||
ShowInstDetails show
|
||||
|
||||
Section "MainSection"
|
||||
; The updater has already coordinated shutdown. Never kill unrelated
|
||||
; instances by name during an unattended upgrade.
|
||||
${If} $UpdateMode = 1
|
||||
Goto installApp
|
||||
${EndIf}
|
||||
; Check if CrossDesk is running
|
||||
StrCpy $1 "CrossDesk.exe"
|
||||
|
||||
@@ -81,10 +98,12 @@ installApp:
|
||||
Call StopInstalledService
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
SetOverwrite ifnewer
|
||||
; The signed release determines file versions, not filesystem timestamps.
|
||||
SetOverwrite on
|
||||
|
||||
; Main application executable path
|
||||
File /oname=CrossDesk.exe "..\..\..\..\build\windows\x64\release\crossdesk.exe"
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_updater.exe"
|
||||
; Bundle service-side binaries required by the Windows service flow
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_service.exe"
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
||||
@@ -116,6 +135,7 @@ installApp:
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
|
||||
@@ -167,6 +187,7 @@ uninstallApp:
|
||||
|
||||
; Delete main executable and uninstaller
|
||||
Delete "$INSTDIR\CrossDesk.exe"
|
||||
Delete "$INSTDIR\crossdesk_updater.exe"
|
||||
Delete "$INSTDIR\crossdesk_service.exe"
|
||||
Delete "$INSTDIR\crossdesk_session_helper.exe"
|
||||
Delete "$INSTDIR\crossdesk_privacy_cursor_helper.exe"
|
||||
@@ -196,9 +217,16 @@ SectionEnd
|
||||
|
||||
; ------ Functions ------
|
||||
Function LaunchApp
|
||||
${If} $UpdateMode = 1
|
||||
Return
|
||||
${EndIf}
|
||||
Exec "$INSTDIR\CrossDesk.exe"
|
||||
FunctionEnd
|
||||
|
||||
Function .onInstFailed
|
||||
SetErrorLevel 1
|
||||
FunctionEnd
|
||||
|
||||
Function StopInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc
|
||||
|
||||
@@ -20,15 +20,20 @@
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "service_host.h"
|
||||
#include "service_lifetime.h"
|
||||
#include "shutdown_order.h"
|
||||
#include "unattended_config.h"
|
||||
#endif
|
||||
|
||||
#include "config_center.h"
|
||||
#include "daemon.h"
|
||||
#include "path_manager.h"
|
||||
#include "platform/instance_activation.h"
|
||||
#include "platform/single_instance.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
#include "updater/update_platform.h"
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
#include "SimpleIni.h"
|
||||
@@ -203,8 +208,6 @@ int HandleServiceCliCommand(const std::string& command) {
|
||||
destination.SetTurnMode(source.GetTurnMode()) == 0 &&
|
||||
destination.SetVideoEncodeFormat(source.GetVideoEncodeFormat()) ==
|
||||
0 &&
|
||||
destination.SetHardwareVideoCodec(source.IsHardwareVideoCodec()) ==
|
||||
0 &&
|
||||
destination.SetScreenCaptureMethod(
|
||||
crossdesk::ScreenCaptureMethod::Auto) == 0 &&
|
||||
destination.SetPrivacyScreen(false) == 0 &&
|
||||
@@ -307,6 +310,25 @@ static crossdesk::platform::InstanceResult AcquireInstance(
|
||||
return result;
|
||||
}
|
||||
|
||||
static int ExitForExistingInstance(
|
||||
crossdesk::platform::InstanceResult result, bool interactive_launch) {
|
||||
if (interactive_launch &&
|
||||
result == crossdesk::platform::InstanceResult::kAlreadyRunning) {
|
||||
std::string error;
|
||||
if (!crossdesk::platform::RequestInstanceActivation(error)) {
|
||||
std::cerr << error << '\n';
|
||||
#ifdef _WIN32
|
||||
MessageBoxW(nullptr,
|
||||
L"CrossDesk is already running, but its window could not be "
|
||||
L"activated.\n\nIt may still be starting or be running in "
|
||||
L"another Windows session. Please check the system tray.",
|
||||
L"CrossDesk", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return result == crossdesk::platform::InstanceResult::kAlreadyRunning ? 0 : 1;
|
||||
}
|
||||
|
||||
static int RunApplication(
|
||||
int argc, char* argv[],
|
||||
crossdesk::platform::SingleInstanceGuard& client_instance) {
|
||||
@@ -314,6 +336,12 @@ static int RunApplication(
|
||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||
crossdesk::InitLogger(path_manager->GetLogPath().string());
|
||||
|
||||
#ifdef _WIN32
|
||||
crossdesk::get_logger()->info(
|
||||
"CrossDesk executable: {}",
|
||||
std::filesystem::path(GetCurrentExecutablePathW()).u8string());
|
||||
#endif
|
||||
|
||||
if (IsDaemonChild(argc, argv)) {
|
||||
// child process: run render directly
|
||||
crossdesk::Render render;
|
||||
@@ -377,6 +405,16 @@ static int RunApplication(
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
// Apply in every process, including the daemon and --child. Windows does
|
||||
// not inherit shutdown priority from a parent process.
|
||||
const DWORD shutdown_error = crossdesk::ConfigureWindowsShutdownOrder(
|
||||
crossdesk::WindowsShutdownRole::host);
|
||||
if (shutdown_error != ERROR_SUCCESS) {
|
||||
std::cerr << "Could not configure Windows host shutdown order, error="
|
||||
<< shutdown_error << '\n';
|
||||
}
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
// Run before GUI, configuration, logging or display initialization.
|
||||
if (argc == 2 &&
|
||||
@@ -389,7 +427,7 @@ int main(int argc, char* argv[]) {
|
||||
CSimpleIniA config;
|
||||
config.SetUnicode(true);
|
||||
config.LoadFile((paths.GetCachePath() / "config.ini").string().c_str());
|
||||
console.SetLanguage(static_cast<int>(config.GetLongValue("Settings", "language", 0)));
|
||||
console.SetLanguage(static_cast<int>(config.GetLongValue("Settings", "language", 10)));
|
||||
std::cout << console.Text("console_cli_help");
|
||||
return 0;
|
||||
}
|
||||
@@ -446,17 +484,28 @@ int main(int argc, char* argv[]) {
|
||||
#endif
|
||||
|
||||
using crossdesk::platform::InstanceResult;
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
// Installer CLI commands above stay available during maintenance. Ordinary
|
||||
// launches (including daemon restarts) must not reopen the installation.
|
||||
try {
|
||||
if (crossdesk::updater::MaintenanceActive(crossdesk::updater::ExecutablePath()))
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Cannot check update maintenance: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
using crossdesk::platform::InstanceRole;
|
||||
crossdesk::platform::SingleInstanceGuard launcher_instance;
|
||||
crossdesk::platform::SingleInstanceGuard client_instance;
|
||||
if (!IsDaemonChild(argc, argv)) {
|
||||
const auto result = AcquireInstance(launcher_instance, InstanceRole::kLauncher);
|
||||
if (result != InstanceResult::kAcquired)
|
||||
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
|
||||
return ExitForExistingInstance(result, true);
|
||||
}
|
||||
const auto result = AcquireInstance(client_instance, InstanceRole::kClient);
|
||||
if (result != InstanceResult::kAcquired)
|
||||
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
|
||||
return ExitForExistingInstance(result, !IsDaemonChild(argc, argv));
|
||||
|
||||
#ifdef __linux__
|
||||
const int exit_code = crossdesk::RunWithLinuxDisplay(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-25
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_LOCK_H_
|
||||
#define _SCREEN_LOCK_H_
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Called on the application thread. True means the OS accepted the request,
|
||||
// not that the asynchronous transition to the lock screen has finished.
|
||||
bool LockScreen();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -41,6 +41,8 @@ int ConfigCenter::Load() {
|
||||
}
|
||||
|
||||
bool persist_config_migration = false;
|
||||
// Hardware codecs are always preferred, with software fallback in MiniRTC.
|
||||
persist_config_migration |= ini_->Delete(section_, "hardware_video_codec");
|
||||
// These options now belong only to the active remote connection. Discard
|
||||
// legacy local defaults so they cannot affect this host or new connections.
|
||||
persist_config_migration |= ini_->Delete(section_, "video_quality");
|
||||
@@ -52,16 +54,27 @@ int ConfigCenter::Load() {
|
||||
ini_->Delete(section_, "enable_desktop_quality_optimization");
|
||||
persist_config_migration |=
|
||||
ini_->Delete(section_, "enable_minimize_to_tray");
|
||||
// The old global flag has no version information. Prompt once for the
|
||||
// current release, then remember its version when the user dismisses it.
|
||||
persist_config_migration |=
|
||||
ini_->Delete(section_, "sponsorship_prompt_dismissed");
|
||||
|
||||
const long language_value =
|
||||
ini_->GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
|
||||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
|
||||
language_value > static_cast<long>(LANGUAGE::SYSTEM)) {
|
||||
language_ = LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
language_ = static_cast<LANGUAGE>(language_value);
|
||||
}
|
||||
|
||||
enable_ip_direct_ = ini_->GetBoolValue(section_, "enable_ip_direct", true);
|
||||
connection_mode_ =
|
||||
enable_ip_direct_ &&
|
||||
std::strcmp(ini_->GetValue(section_, "connection_mode", "id"), "ip") == 0
|
||||
? CONNECTION_MODE::DIRECT_IP
|
||||
: CONNECTION_MODE::DEVICE_ID;
|
||||
|
||||
const long screen_capture_method_value = ini_->GetLongValue(
|
||||
section_, "screen_capture_method",
|
||||
static_cast<long>(ScreenCaptureMethod::Auto));
|
||||
@@ -74,8 +87,6 @@ int ConfigCenter::Load() {
|
||||
ini_->GetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_)));
|
||||
|
||||
hardware_video_codec_ = ini_->GetBoolValue(section_, "hardware_video_codec",
|
||||
hardware_video_codec_);
|
||||
const char* turn_mode_value = ini_->GetValue(section_, "turn_mode", nullptr);
|
||||
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
|
||||
const long parsed_turn_mode = ini_->GetLongValue(
|
||||
@@ -126,11 +137,17 @@ int ConfigCenter::Load() {
|
||||
enable_daemon_ = ini_->GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_privacy_screen_.store(
|
||||
ini_->GetBoolValue(section_, "enable_privacy_screen", true));
|
||||
privacy_local_unlock_allowed_.store(
|
||||
ini_->GetBoolValue(section_, "privacy_local_unlock_allowed", true));
|
||||
lock_on_disconnect_.store(
|
||||
ini_->GetBoolValue(section_, "lock_on_disconnect", true));
|
||||
save_remote_previews_.store(
|
||||
ini_->GetBoolValue(section_, "save_remote_previews", true));
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_->GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
sponsorship_prompt_dismissed_version_ =
|
||||
ini_->GetValue(section_, "sponsorship_prompt_dismissed_version", "");
|
||||
|
||||
const char* file_transfer_save_path_value =
|
||||
ini_->GetValue(section_, "file_transfer_save_path", nullptr);
|
||||
@@ -195,11 +212,13 @@ int ConfigCenter::StoreValues(
|
||||
|
||||
int ConfigCenter::Save() {
|
||||
ini_->SetLongValue(section_, "language", static_cast<long>(language_));
|
||||
ini_->SetValue(section_, "connection_mode",
|
||||
connection_mode_ == CONNECTION_MODE::DIRECT_IP ? "ip" : "id");
|
||||
ini_->SetBoolValue(section_, "enable_ip_direct", enable_ip_direct_);
|
||||
ini_->SetLongValue(section_, "screen_capture_method",
|
||||
static_cast<long>(screen_capture_method_));
|
||||
ini_->SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_->SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
ini_->SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_->SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
@@ -217,10 +236,15 @@ int ConfigCenter::Save() {
|
||||
ini_->SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_->SetBoolValue(section_, "enable_privacy_screen",
|
||||
enable_privacy_screen_.load());
|
||||
ini_->SetBoolValue(section_, "privacy_local_unlock_allowed",
|
||||
privacy_local_unlock_allowed_.load());
|
||||
ini_->SetBoolValue(section_, "lock_on_disconnect", lock_on_disconnect_.load());
|
||||
ini_->SetBoolValue(section_, "save_remote_previews",
|
||||
save_remote_previews_.load());
|
||||
ini_->SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
ini_->SetValue(section_, "sponsorship_prompt_dismissed_version",
|
||||
sponsorship_prompt_dismissed_version_.c_str());
|
||||
|
||||
ini_->SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
@@ -237,11 +261,39 @@ int ConfigCenter::Save() {
|
||||
|
||||
int ConfigCenter::SetLanguage(LANGUAGE language) {
|
||||
const int value = static_cast<int>(language);
|
||||
if (value < 0 || value > 2 || StoreValues({{"language", std::to_string(value)}}) != 0) return -1;
|
||||
if (value < 0 || value > static_cast<int>(LANGUAGE::SYSTEM) || StoreValues({{"language", std::to_string(value)}}) != 0) return -1;
|
||||
language_ = language;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetConnectionMode(CONNECTION_MODE mode) {
|
||||
if (mode != CONNECTION_MODE::DEVICE_ID && mode != CONNECTION_MODE::DIRECT_IP) {
|
||||
return -1;
|
||||
}
|
||||
if (mode == CONNECTION_MODE::DIRECT_IP && !enable_ip_direct_) return -1;
|
||||
if (mode == connection_mode_) return 0;
|
||||
if (StoreValues({{"connection_mode",
|
||||
mode == CONNECTION_MODE::DIRECT_IP ? "ip" : "id"}}) != 0) {
|
||||
return -1;
|
||||
}
|
||||
connection_mode_ = mode;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetIpDirectEnabled(bool enabled) {
|
||||
if (enabled == enable_ip_direct_) return 0;
|
||||
const auto mode = enabled ? connection_mode_ : CONNECTION_MODE::DEVICE_ID;
|
||||
// Persist availability and the fallback mode together, so a failed save
|
||||
// leaves both the current runtime and the next launch unchanged.
|
||||
if (StoreValues({{"enable_ip_direct", enabled ? "true" : "false"},
|
||||
{"connection_mode", mode == CONNECTION_MODE::DIRECT_IP
|
||||
? "ip" : "id"}}) != 0)
|
||||
return -1;
|
||||
enable_ip_direct_ = enabled;
|
||||
connection_mode_ = mode;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
|
||||
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
|
||||
return -1;
|
||||
@@ -263,13 +315,6 @@ int ConfigCenter::SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT format) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetHardwareVideoCodec(bool enabled) {
|
||||
if (enabled && !IsHardwareVideoCodecAvailable()) return -1;
|
||||
if (StoreValues({{"hardware_video_codec", enabled ? "true" : "false"}}) != 0) return -1;
|
||||
hardware_video_codec_ = enabled;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurnMode(TURN_MODE mode) {
|
||||
if (!IsValidTurnModeValue(static_cast<long>(mode))) return -1;
|
||||
if (StoreValues({{"turn_mode", std::to_string(static_cast<int>(mode))},
|
||||
@@ -278,10 +323,7 @@ int ConfigCenter::SetTurnMode(TURN_MODE mode) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurn(bool enable_turn, bool force_relay) {
|
||||
if (!enable_turn) {
|
||||
return SetTurnMode(TURN_MODE::DISABLED);
|
||||
}
|
||||
int ConfigCenter::SetForceRelay(bool force_relay) {
|
||||
if (!force_relay) {
|
||||
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
|
||||
}
|
||||
@@ -371,6 +413,29 @@ int ConfigCenter::SetPrivacyScreen(bool enable_privacy_screen) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPrivacyLocalUnlock(bool allowed) {
|
||||
if (privacy_local_unlock_allowed_.load() == allowed) return 0;
|
||||
if (StoreValues({{"privacy_local_unlock_allowed", allowed ? "true" : "false"}}) != 0)
|
||||
return -1;
|
||||
privacy_local_unlock_allowed_.store(allowed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsPrivacyLocalUnlockAllowed() const {
|
||||
return privacy_local_unlock_allowed_.load();
|
||||
}
|
||||
|
||||
int ConfigCenter::SetLockOnDisconnect(bool enabled) {
|
||||
if (StoreValues({{"lock_on_disconnect", enabled ? "true" : "false"}}) != 0)
|
||||
return -1;
|
||||
lock_on_disconnect_.store(enabled);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsLockOnDisconnect() const {
|
||||
return lock_on_disconnect_.load();
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSaveRemotePreviews(bool enabled) {
|
||||
if (StoreValues({{"save_remote_previews", enabled ? "true" : "false"}}) != 0) {
|
||||
return -1;
|
||||
@@ -395,10 +460,26 @@ int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSponsorshipPromptDismissedVersion(
|
||||
const std::string& version) {
|
||||
if (version.empty() ||
|
||||
StoreValues({{"sponsorship_prompt_dismissed_version", version}}) != 0) {
|
||||
return -1;
|
||||
}
|
||||
sponsorship_prompt_dismissed_version_ = version;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// getters
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
|
||||
ConfigCenter::CONNECTION_MODE ConfigCenter::GetConnectionMode() const {
|
||||
return connection_mode_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsIpDirectEnabled() const { return enable_ip_direct_; }
|
||||
|
||||
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
|
||||
return screen_capture_method_;
|
||||
}
|
||||
@@ -407,28 +488,10 @@ ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsHardwareVideoCodecAvailable() {
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
!defined(__arm__) && USE_CUDA) || \
|
||||
defined(__APPLE__))
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsHardwareVideoCodec() const {
|
||||
return IsHardwareVideoCodecAvailable() && hardware_video_codec_;
|
||||
}
|
||||
|
||||
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
|
||||
return turn_mode_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableTurn() const {
|
||||
return turn_mode_ != TURN_MODE::DISABLED;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsForceRelay() const {
|
||||
return turn_mode_ == TURN_MODE::FORCE_UDP || turn_mode_ == TURN_MODE::FORCE_TCP;
|
||||
}
|
||||
@@ -463,6 +526,10 @@ bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
std::string ConfigCenter::GetSponsorshipPromptDismissedVersion() const {
|
||||
return sponsorship_prompt_dismissed_version_;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
if (path.size() >= 512 || std::any_of(path.begin(), path.end(), [](unsigned char c) { return c < 32 || c == 127; })) return -1;
|
||||
std::lock_guard<std::mutex> lock(file_path_mutex_);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CONFIG_CENTER_H_
|
||||
@@ -20,7 +20,12 @@ namespace crossdesk {
|
||||
|
||||
class ConfigCenter {
|
||||
public:
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||
enum class LANGUAGE {
|
||||
CHINESE = 0, ENGLISH = 1, RUSSIAN = 2, TRADITIONAL_CHINESE = 3,
|
||||
JAPANESE = 4, KOREAN = 5, GERMAN = 6, FRENCH = 7, SPANISH = 8,
|
||||
BRAZILIAN_PORTUGUESE = 9, SYSTEM = 10
|
||||
};
|
||||
enum class CONNECTION_MODE { DEVICE_ID = 0, DIRECT_IP = 1 };
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
@@ -35,11 +40,12 @@ class ConfigCenter {
|
||||
|
||||
// write config
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetConnectionMode(CONNECTION_MODE mode);
|
||||
int SetIpDirectEnabled(bool enabled);
|
||||
int SetScreenCaptureMethod(ScreenCaptureMethod method);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
int SetTurn(bool enable_turn, bool force_relay);
|
||||
int SetForceRelay(bool force_relay);
|
||||
int SetServerHost(const std::string& signal_server_host);
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetServerEndpoint(const std::string& address, int port);
|
||||
@@ -47,20 +53,21 @@ class ConfigCenter {
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPrivacyScreen(bool enable_privacy_screen);
|
||||
int SetPrivacyLocalUnlock(bool allowed);
|
||||
int SetLockOnDisconnect(bool enabled);
|
||||
int SetSaveRemotePreviews(bool enabled);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetSponsorshipPromptDismissedVersion(const std::string& version);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
// read config
|
||||
|
||||
LANGUAGE GetLanguage() const;
|
||||
CONNECTION_MODE GetConnectionMode() const;
|
||||
bool IsIpDirectEnabled() const;
|
||||
ScreenCaptureMethod GetScreenCaptureMethod() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
// Whether this build includes a hardware video codec backend.
|
||||
static bool IsHardwareVideoCodecAvailable();
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
bool IsEnableTurn() const;
|
||||
bool IsForceRelay() const;
|
||||
bool IsEnableSrtp() const;
|
||||
std::string GetSignalServerHost() const;
|
||||
@@ -71,8 +78,11 @@ class ConfigCenter {
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsEnablePrivacyScreen() const;
|
||||
bool IsPrivacyLocalUnlockAllowed() const;
|
||||
bool IsLockOnDisconnect() const;
|
||||
bool IsSaveRemotePreviews() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetSponsorshipPromptDismissedVersion() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
int Load();
|
||||
@@ -87,10 +97,11 @@ class ConfigCenter {
|
||||
std::unique_ptr<IniStorage> ini_;
|
||||
const char* section_ = "Settings";
|
||||
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
LANGUAGE language_ = LANGUAGE::SYSTEM;
|
||||
CONNECTION_MODE connection_mode_ = CONNECTION_MODE::DEVICE_ID;
|
||||
bool enable_ip_direct_ = true;
|
||||
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
std::string signal_server_host_ = "";
|
||||
std::string signal_server_host_default_ = "api.crossdesk.cn";
|
||||
@@ -100,8 +111,11 @@ class ConfigCenter {
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
std::atomic<bool> enable_privacy_screen_{true};
|
||||
std::atomic<bool> privacy_local_unlock_allowed_{true};
|
||||
std::atomic<bool> lock_on_disconnect_{true};
|
||||
std::atomic<bool> save_remote_previews_{true};
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string sponsorship_prompt_dismissed_version_;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2023-12-14
|
||||
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_CONTROLLER_H_
|
||||
@@ -32,6 +32,9 @@ class MouseController : public DeviceController {
|
||||
virtual int Destroy() = 0;
|
||||
virtual int SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) = 0;
|
||||
// Bits 0/1/2 are left/right/middle. Return the subset confirmed released,
|
||||
// including buttons already up. Must not require valid display geometry.
|
||||
virtual unsigned ReleasePressedButtons(unsigned mask = 7) = 0;
|
||||
virtual void UpdateDisplayInfoList(
|
||||
const std::vector<DisplayInfo>&) {}
|
||||
};
|
||||
@@ -45,6 +48,11 @@ class KeyboardCapturer : public DeviceController {
|
||||
// Backends with an asynchronous capture loop override this so the UI can
|
||||
// fall back to window events if that loop stops unexpectedly.
|
||||
virtual bool IsHookActive() const { return true; }
|
||||
// Permission-denied backends can wait for an OS grant notification rather
|
||||
// than repeatedly releasing held fallback keys to retry an impossible hook.
|
||||
virtual bool ShouldRetryHook() const { return true; }
|
||||
virtual bool SupportsTextInput() const { return false; }
|
||||
virtual int SendTextCommand(const std::string&) { return -1; }
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false) = 0;
|
||||
|
||||
@@ -32,7 +32,7 @@ struct InteractionState {
|
||||
bool audio_capture_ = false;
|
||||
int screen_width_ = 1280;
|
||||
int screen_height_ = 720;
|
||||
int selected_display_ = 0;
|
||||
std::atomic<int> selected_display_{0};
|
||||
char remote_id_display_[12] = "";
|
||||
unsigned char audio_buffer_[720]{};
|
||||
int audio_len_ = 0;
|
||||
@@ -77,7 +77,7 @@ struct UiState {
|
||||
bool show_connection_status_window_ = false;
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
std::atomic<bool> is_server_mode_{false};
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_offline_warning_window_ = false;
|
||||
std::string offline_warning_text_;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
#define _GUI_APPLICATION_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -21,6 +22,7 @@
|
||||
namespace crossdesk {
|
||||
|
||||
class UpdateChecker;
|
||||
namespace updater { class Controller; }
|
||||
|
||||
// Slint application shell. GuiRuntime remains the owner of transport, media,
|
||||
// device, clipboard, transfer and settings behavior.
|
||||
@@ -49,6 +51,7 @@ private:
|
||||
void InitializeModules();
|
||||
void InitializeUi();
|
||||
void InitializeSystemTray();
|
||||
void ShowMainWindow(const std::string& activation_token = {});
|
||||
bool MinimizeMainWindowToTray();
|
||||
void BindMainCallbacks(bool gallery_window = false);
|
||||
void BindStreamCallbacks();
|
||||
@@ -89,9 +92,11 @@ private:
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
void SyncFileTransferNotifications();
|
||||
void CancelFileTransfer(uint32_t file_id);
|
||||
void SetServerPanelExpanded(bool expanded);
|
||||
void PositionServerWindows();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
void SyncXWaylandWindowActivation();
|
||||
#endif
|
||||
void UpdateLocalization();
|
||||
@@ -116,18 +121,22 @@ private:
|
||||
void Cleanup();
|
||||
|
||||
std::shared_ptr<RemoteSession> SelectedSession();
|
||||
std::string OpenFileDialog(const std::string &title);
|
||||
void OpenFileDialog(const std::string& title,
|
||||
std::function<void(std::string)> completion);
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
std::unique_ptr<UpdateChecker> update_checker_;
|
||||
#if CROSSDESK_HAS_UPDATER
|
||||
std::unique_ptr<updater::Controller> updater_;
|
||||
#endif
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
uint32_t cursor_state_sequence_ = 0;
|
||||
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||
SignalConnectionIndicator signal_connection_indicator_;
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
bool use_xwayland_gui_ = false;
|
||||
bool use_x11_custom_titlebar_ = false;
|
||||
#endif
|
||||
|
||||
@@ -1,116 +1,91 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "localization_data.h"
|
||||
#include "system_language.h"
|
||||
|
||||
#if _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
struct LanguageOption {
|
||||
std::string code;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
class LocalizedString {
|
||||
public:
|
||||
constexpr explicit LocalizedString(const char* key) : key_(key) {}
|
||||
const std::string& operator[](int language_index) const;
|
||||
|
||||
private:
|
||||
const char* key_;
|
||||
};
|
||||
|
||||
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
|
||||
static const std::vector<LanguageOption> kSupportedLanguages = {
|
||||
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
|
||||
{"en-US", "English"},
|
||||
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
|
||||
return kSupportedLanguages;
|
||||
}
|
||||
|
||||
public:
|
||||
constexpr explicit LocalizedString(const char* key) : key_(key) {}
|
||||
const std::string& operator[](int language_index) const;
|
||||
|
||||
private:
|
||||
const char* key_;
|
||||
};
|
||||
|
||||
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
|
||||
static const std::vector<LanguageOption> kSupportedLanguages = {
|
||||
{"zh-CN", "简体中文"}, {"en-US", "English"}, {"ru-RU", "Русский"},
|
||||
{"zh-Hant", "繁體中文"}, {"ja", "日本語"}, {"ko", "한국어"},
|
||||
{"de", "Deutsch"}, {"fr", "Français"}, {"es", "Español"},
|
||||
{"pt-BR", "Português (Brasil)"}};
|
||||
return kSupportedLanguages;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline int ClampLanguageIndex(int language_index) {
|
||||
if (language_index >= 0 &&
|
||||
language_index < static_cast<int>(GetSupportedLanguages().size())) {
|
||||
return language_index;
|
||||
}
|
||||
return 0;
|
||||
if (language_index == kSystemLanguage) return SystemLanguage();
|
||||
return language_index >= 0 && language_index < kSystemLanguage
|
||||
? language_index : kEnglishLanguage;
|
||||
}
|
||||
|
||||
using TranslationTable =
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, std::string>>;
|
||||
|
||||
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
|
||||
const TranslationRow& row) {
|
||||
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
|
||||
{"en-US", reinterpret_cast<const char*>(row.en)},
|
||||
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
|
||||
}
|
||||
|
||||
inline TranslationTable BuildTranslationTable() {
|
||||
TranslationTable table;
|
||||
for (const auto& row : kTranslationRows) {
|
||||
table[row.key] = MakeLocalizedValues(row);
|
||||
}
|
||||
|
||||
using TranslationTable = std::unordered_map<std::string, std::vector<std::string>>;
|
||||
inline const TranslationTable& LiteralTable() {
|
||||
static const TranslationTable table = [] {
|
||||
TranslationTable result;
|
||||
for (const auto& row : kTranslationRows)
|
||||
result.emplace(row.key, std::vector<std::string>(std::begin(row.text), std::end(row.text)));
|
||||
return result;
|
||||
}();
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const TranslationTable& GetTranslationTable() {
|
||||
static const TranslationTable table = BuildTranslationTable();
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const std::string& GetTranslatedText(const std::string& key,
|
||||
int language_index) {
|
||||
static const std::string kEmptyText = "";
|
||||
|
||||
const auto& table = GetTranslationTable();
|
||||
const auto key_it = table.find(key);
|
||||
if (key_it == table.end()) {
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
const auto& localized_values = key_it->second;
|
||||
const std::string& language_code =
|
||||
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
|
||||
|
||||
const auto exact_it = localized_values.find(language_code);
|
||||
if (exact_it != localized_values.end()) {
|
||||
return exact_it->second;
|
||||
}
|
||||
|
||||
const auto english_it = localized_values.find("en-US");
|
||||
if (english_it != localized_values.end()) {
|
||||
return english_it->second;
|
||||
}
|
||||
|
||||
const auto chinese_it = localized_values.find("zh-CN");
|
||||
if (chinese_it != localized_values.end()) {
|
||||
return chinese_it->second;
|
||||
}
|
||||
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& GetLiteralText(const std::string& key, int language) {
|
||||
static const std::string empty;
|
||||
const auto it = LiteralTable().find(key);
|
||||
if (it == LiteralTable().end()) return empty;
|
||||
const auto& values = it->second;
|
||||
return values[ClampLanguageIndex(language)];
|
||||
}
|
||||
inline const std::string& GetTranslatedText(const std::string& key, int language) {
|
||||
static const std::unordered_map<std::string, std::string> names = [] {
|
||||
std::unordered_map<std::string, std::string> result;
|
||||
for (const auto& row : kNamedTranslations) result.emplace(row.name, row.literal);
|
||||
return result;
|
||||
}();
|
||||
static const std::string empty;
|
||||
const auto it = names.find(key);
|
||||
return it == names.end() ? empty : GetLiteralText(it->second, language);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& Literal(const std::string& key, int language) {
|
||||
return detail::GetLiteralText(key, language);
|
||||
}
|
||||
|
||||
inline const std::string& LocalizedString::operator[](
|
||||
int language_index) const {
|
||||
return detail::GetTranslatedText(key_, language_index);
|
||||
@@ -130,11 +105,11 @@ inline std::string FormatDisplayLabel(size_t display_index,
|
||||
return label;
|
||||
}
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#if _WIN32
|
||||
inline const wchar_t* GetWindowsLocalizedLabel(
|
||||
int language_index, const char* key, const wchar_t* fallback,
|
||||
@@ -187,8 +162,8 @@ inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||
cache);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-27
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SYSTEM_LANGUAGE_H_
|
||||
#define _SYSTEM_LANGUAGE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <Windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
inline constexpr int kSystemLanguage = 10;
|
||||
inline constexpr int kEnglishLanguage = 1;
|
||||
|
||||
// Keep 0/1/2 compatible with existing Chinese/English/Russian config files.
|
||||
inline int MatchLanguage(std::string tag) {
|
||||
for (char& c : tag) {
|
||||
if (c == '_') c = '-';
|
||||
else if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
||||
}
|
||||
tag = tag.substr(0, tag.find_first_of(".@"));
|
||||
const auto base = tag.substr(0, tag.find('-'));
|
||||
if (base == "zh") {
|
||||
const auto parts = "-" + tag + "-";
|
||||
if (parts.find("-hans-") != std::string::npos) return 0;
|
||||
return parts.find("-hant-") != std::string::npos ||
|
||||
parts.find("-tw-") != std::string::npos ||
|
||||
parts.find("-hk-") != std::string::npos ||
|
||||
parts.find("-mo-") != std::string::npos ? 3 : 0;
|
||||
}
|
||||
const char* codes[] = {"zh", "en", "ru", "zh", "ja", "ko", "de", "fr", "es", "pt"};
|
||||
for (int i = 1; i < kSystemLanguage; ++i)
|
||||
if (i != 3 && base == codes[i]) return i;
|
||||
if (base == "c" || base == "posix") return kEnglishLanguage;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int MatchPreferredLanguages(const std::vector<std::string>& tags) {
|
||||
for (const auto& tag : tags) {
|
||||
const int match = MatchLanguage(tag);
|
||||
if (match >= 0) return match;
|
||||
}
|
||||
return kEnglishLanguage;
|
||||
}
|
||||
|
||||
inline int DetectSystemLanguage() {
|
||||
std::vector<std::string> tags;
|
||||
#if defined(_WIN32)
|
||||
ULONG count = 0, length = 0;
|
||||
GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, nullptr, &length);
|
||||
std::vector<wchar_t> names(length);
|
||||
if (length && GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, names.data(), &length)) {
|
||||
for (const wchar_t* name = names.data(); *name; name += wcslen(name) + 1) {
|
||||
std::string tag;
|
||||
for (const wchar_t* c = name; *c; ++c) tag += static_cast<char>(*c);
|
||||
tags.push_back(tag);
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
if (CFArrayRef names = CFLocaleCopyPreferredLanguages()) {
|
||||
for (CFIndex i = 0; i < CFArrayGetCount(names); ++i) {
|
||||
char tag[128] = {};
|
||||
auto name = static_cast<CFStringRef>(CFArrayGetValueAtIndex(names, i));
|
||||
if (CFStringGetCString(name, tag, sizeof(tag), kCFStringEncodingUTF8)) tags.emplace_back(tag);
|
||||
}
|
||||
CFRelease(names);
|
||||
}
|
||||
#else
|
||||
// The Harmony desktop host supplies LANGUAGE from the system i18n API.
|
||||
const char* locale = std::getenv("LC_ALL");
|
||||
if (!locale || !*locale) locale = std::getenv("LC_MESSAGES");
|
||||
if (!locale || !*locale) locale = std::getenv("LANG");
|
||||
if (locale && (std::string(locale) == "C" || std::string(locale) == "POSIX")) return kEnglishLanguage;
|
||||
if (const char* preferred = std::getenv("LANGUAGE")) {
|
||||
std::string list(preferred);
|
||||
size_t start = 0;
|
||||
do {
|
||||
const auto end = list.find(':', start);
|
||||
tags.push_back(list.substr(start, end - start));
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
} while (start < list.size());
|
||||
}
|
||||
if (locale) tags.emplace_back(locale);
|
||||
#endif
|
||||
return MatchPreferredLanguages(tags);
|
||||
}
|
||||
|
||||
inline int SystemLanguage() {
|
||||
// Language changes take effect on the next application launch; explicit
|
||||
// application preferences always win over this process-wide default.
|
||||
static const int language = DetectSystemLanguage();
|
||||
return language;
|
||||
}
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "about_catalog.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "about_resources.h"
|
||||
|
||||
namespace crossdesk::about {
|
||||
namespace {
|
||||
|
||||
slint::SharedString Text(const nlohmann::json& object, const char* key) {
|
||||
return slint::SharedString(object.value(key, std::string{}));
|
||||
}
|
||||
|
||||
std::string_view TrimLayoutWhitespace(std::string_view line) {
|
||||
const auto first = line.find_first_not_of(" \t\r");
|
||||
if (first == std::string_view::npos) return {};
|
||||
return line.substr(first, line.find_last_not_of(" \t\r") - first + 1);
|
||||
}
|
||||
|
||||
slint::SharedString DocumentText(const nlohmann::json& document) {
|
||||
const auto title = document.value("title", std::string{});
|
||||
const auto text = document.value("text", std::string{});
|
||||
if (title != "GNU GPL v3" && title != "GNU LGPL v3") {
|
||||
return slint::SharedString(text);
|
||||
}
|
||||
|
||||
// GNU's plain-text copies use fixed-width wrapping and space-aligned headings.
|
||||
// Reflow prose for the reader, retaining paragraph boundaries, notice examples,
|
||||
// and every non-whitespace character. The embedded source stays untouched.
|
||||
std::string formatted;
|
||||
std::vector<std::string_view> paragraph;
|
||||
auto append_paragraph = [&] {
|
||||
if (paragraph.empty()) return;
|
||||
if (!formatted.empty()) formatted += "\n\n";
|
||||
const bool preserve_lines = paragraph.front().starts_with("GNU ") ||
|
||||
paragraph.front().starts_with("<");
|
||||
for (size_t index = 0; index < paragraph.size(); ++index) {
|
||||
if (index > 0) {
|
||||
const bool after_copyright =
|
||||
paragraph[index - 1].starts_with("Copyright (C)");
|
||||
formatted += preserve_lines || after_copyright ? '\n' : ' ';
|
||||
}
|
||||
formatted += paragraph[index];
|
||||
}
|
||||
paragraph.clear();
|
||||
};
|
||||
std::string_view remaining(text);
|
||||
while (!remaining.empty()) {
|
||||
const auto end = remaining.find('\n');
|
||||
const auto line = TrimLayoutWhitespace(remaining.substr(0, end));
|
||||
if (line.empty()) {
|
||||
append_paragraph();
|
||||
} else {
|
||||
paragraph.push_back(line);
|
||||
}
|
||||
if (end == std::string_view::npos) break;
|
||||
remaining.remove_prefix(end + 1);
|
||||
}
|
||||
append_paragraph();
|
||||
return slint::SharedString(formatted);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Populate(const slint::ComponentHandle<ui::MainWindow>& window) {
|
||||
// Both the main window and DeskHub share the immutable, embedded catalog.
|
||||
static const auto catalog = nlohmann::json::parse(
|
||||
std::begin(kAboutResources), std::end(kAboutResources));
|
||||
static const auto model = [] {
|
||||
std::vector<ui::AboutComponent> components;
|
||||
for (const auto& item : catalog.at("components")) {
|
||||
ui::AboutComponent component;
|
||||
component.id = Text(item, "id");
|
||||
component.name = Text(item, "name");
|
||||
component.version = Text(item, "version");
|
||||
component.license = Text(item, "license");
|
||||
component.source_url = Text(item, "sourceURL");
|
||||
std::vector<ui::AboutDocument> documents;
|
||||
for (const auto& document : item.at("documents")) {
|
||||
ui::AboutDocument entry;
|
||||
entry.title = Text(document, "title");
|
||||
entry.text = DocumentText(document);
|
||||
documents.push_back(std::move(entry));
|
||||
}
|
||||
component.documents =
|
||||
std::make_shared<slint::VectorModel<ui::AboutDocument>>(documents);
|
||||
components.push_back(std::move(component));
|
||||
}
|
||||
return std::make_shared<slint::VectorModel<ui::AboutComponent>>(components);
|
||||
}();
|
||||
window->set_about_components(model);
|
||||
for (size_t index = 0; index < model->row_count(); ++index) {
|
||||
if (model->row_data(index)->id == "openfec") {
|
||||
window->set_about_openfec_index(static_cast<int>(index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
window->set_about_revision(Text(catalog, "revision"));
|
||||
window->set_about_source_url(Text(catalog, "sourceURL"));
|
||||
window->set_about_build_url(Text(catalog, "buildURL"));
|
||||
}
|
||||
|
||||
} // namespace crossdesk::about
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _ABOUT_CATALOG_H_
|
||||
#define _ABOUT_CATALOG_H_
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
|
||||
namespace crossdesk::about {
|
||||
|
||||
void Populate(const slint::ComponentHandle<ui::MainWindow>& window);
|
||||
|
||||
} // namespace crossdesk::about
|
||||
|
||||
#endif
|
||||
@@ -30,6 +30,7 @@ uint32_t ClipboardController::event_type() const { return event_type_; }
|
||||
|
||||
void ClipboardController::Initialize() {
|
||||
last_text_.clear();
|
||||
deferred_local_session_.reset();
|
||||
|
||||
char *clipboard_text = SDL_GetClipboardText();
|
||||
if (clipboard_text) {
|
||||
@@ -42,6 +43,7 @@ void ClipboardController::Initialize() {
|
||||
|
||||
void ClipboardController::Shutdown() {
|
||||
events_enabled_.store(false, std::memory_order_release);
|
||||
deferred_local_session_.reset();
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
pending_remote_text_.reset();
|
||||
}
|
||||
@@ -126,6 +128,7 @@ void ClipboardController::ApplyPendingRemoteText() {
|
||||
// SDL will normally emit SDL_EVENT_CLIPBOARD_UPDATE for this write. Update
|
||||
// the baseline first so that event cannot echo the text back to the sender.
|
||||
last_text_ = std::move(*pending_text);
|
||||
deferred_local_session_.reset();
|
||||
}
|
||||
|
||||
void ClipboardController::HandleLocalUpdate() {
|
||||
@@ -134,6 +137,7 @@ void ClipboardController::HandleLocalUpdate() {
|
||||
}
|
||||
if (!SDL_HasClipboardText()) {
|
||||
last_text_.clear();
|
||||
deferred_local_session_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,12 +150,14 @@ void ClipboardController::HandleLocalUpdate() {
|
||||
std::string text(clipboard_text);
|
||||
SDL_free(clipboard_text);
|
||||
if (text == last_text_) {
|
||||
SendDeferredLocalText();
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the value before sending. Duplicate SDL notifications and a
|
||||
// remote write of the same value must not create a clipboard feedback loop.
|
||||
last_text_ = text;
|
||||
deferred_local_session_.reset();
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -163,11 +169,50 @@ void ClipboardController::HandleLocalUpdate() {
|
||||
SendToPeers(text);
|
||||
}
|
||||
|
||||
void ClipboardController::SendDeferredLocalText() {
|
||||
const auto session = deferred_local_session_.lock();
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
const auto current = owner_.remote_sessions_.find(session->remote_id_);
|
||||
if (current == owner_.remote_sessions_.end() || current->second != session ||
|
||||
session->closing_ || !session->peer_ || !session->connection_established_ ||
|
||||
!session->tab_opened_ || !session->tab_selected_ ||
|
||||
!session->control_mouse_) {
|
||||
deferred_local_session_.reset();
|
||||
return;
|
||||
}
|
||||
if (!session->clipboard_allowed_.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int ret = SendReliableDataFrame(
|
||||
session->peer_, last_text_.data(), last_text_.size(),
|
||||
session->clipboard_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Failed to send deferred clipboard data to peer [{}], ret={}",
|
||||
session->remote_id_, ret);
|
||||
return;
|
||||
}
|
||||
deferred_local_session_.reset();
|
||||
}
|
||||
|
||||
int ClipboardController::SendToPeers(const std::string &text) {
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
|
||||
if (!props || !props->peer_ || !props->connection_established_ ||
|
||||
!props->clipboard_allowed_.load()) {
|
||||
if (!props || props->closing_ || !props->peer_ ||
|
||||
!props->connection_established_) {
|
||||
continue;
|
||||
}
|
||||
if (!props->clipboard_allowed_.load()) {
|
||||
// Copying in another local app temporarily releases keyboard control.
|
||||
// Keep this copy for the same selected stream, without sharing it with
|
||||
// background tabs or DeskHub previews.
|
||||
if (props->tab_opened_ && props->tab_selected_ && props->control_mouse_) {
|
||||
deferred_local_session_ = props;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -181,15 +226,11 @@ int ClipboardController::SendToPeers(const std::string &text) {
|
||||
}
|
||||
}
|
||||
|
||||
if (owner_.peer_) {
|
||||
const int ret =
|
||||
SendReliableDataFrame(owner_.peer_, text.data(), text.size(),
|
||||
owner_.clipboard_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
|
||||
owner_.remote_id_display_, ret);
|
||||
return ret;
|
||||
}
|
||||
lock.unlock();
|
||||
for (const auto& id : owner_.ConnectedControllers()) {
|
||||
const int ret = owner_.SendControllerData(id, text.data(), text.size(),
|
||||
owner_.clipboard_label_.c_str());
|
||||
if (ret != 0) return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -41,6 +41,7 @@ public:
|
||||
|
||||
private:
|
||||
int SendToPeers(const std::string &text);
|
||||
void SendDeferredLocalText();
|
||||
|
||||
GuiRuntime &owner_;
|
||||
uint32_t event_type_ = 0;
|
||||
@@ -51,8 +52,11 @@ private:
|
||||
std::weak_ptr<gui_detail::RemoteSession> pending_session_;
|
||||
uint64_t pending_generation_ = 0;
|
||||
std::string last_text_;
|
||||
// UI thread only. Retain the destination of a local copy while its stream
|
||||
// window is unfocused; last_text_ holds the latest value to send on return.
|
||||
std::weak_ptr<gui_detail::RemoteSession> deferred_local_session_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _CLIPBOARD_CONTROLLER_H_
|
||||
#endif // _CLIPBOARD_CONTROLLER_H_
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#include "features/devices/screen_sharing_power.h"
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#elif defined(__linux__) && !defined(__OHOS__)
|
||||
#include <dbus/dbus.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <SDL3/SDL.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#if defined(__linux__) && !defined(__OHOS__)
|
||||
namespace {
|
||||
|
||||
constexpr const char* kApplication = "CrossDesk";
|
||||
constexpr const char* kReason = "Sharing the desktop with a remote controller";
|
||||
constexpr int kDbusTimeoutMs = 1000;
|
||||
|
||||
struct ConnectionCloser {
|
||||
void operator()(DBusConnection* connection) const {
|
||||
dbus_connection_close(connection);
|
||||
dbus_connection_unref(connection);
|
||||
}
|
||||
};
|
||||
using BusConnection = std::unique_ptr<DBusConnection, ConnectionCloser>;
|
||||
using BusMessage = std::unique_ptr<DBusMessage, decltype(&dbus_message_unref)>;
|
||||
|
||||
BusConnection OpenBus(DBusBusType type) {
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
BusConnection connection(dbus_bus_get_private(type, &error));
|
||||
if (connection) {
|
||||
dbus_connection_set_exit_on_disconnect(connection.get(), false);
|
||||
} else {
|
||||
LOG_WARN("Cannot connect to power management bus: {}",
|
||||
error.message ? error.message : "unknown error");
|
||||
}
|
||||
dbus_error_free(&error);
|
||||
return connection;
|
||||
}
|
||||
|
||||
BusMessage Call(DBusConnection* connection, DBusMessage* message) {
|
||||
return BusMessage(dbus_connection_send_with_reply_and_block(
|
||||
connection, message, kDbusTimeoutMs, nullptr),
|
||||
dbus_message_unref);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
struct ScreenSharingPower::Impl {
|
||||
#ifdef _WIN32
|
||||
HANDLE request = INVALID_HANDLE_VALUE;
|
||||
bool display = false;
|
||||
bool system = false;
|
||||
|
||||
void Start() {
|
||||
wchar_t reason_text[] = L"CrossDesk is sharing the desktop";
|
||||
REASON_CONTEXT reason{};
|
||||
reason.Version = POWER_REQUEST_CONTEXT_VERSION;
|
||||
reason.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING;
|
||||
reason.Reason.SimpleReasonString = reason_text;
|
||||
request = PowerCreateRequest(&reason);
|
||||
if (request == INVALID_HANDLE_VALUE) {
|
||||
LOG_WARN("Cannot create screen sharing power request: {}", GetLastError());
|
||||
return;
|
||||
}
|
||||
// Handle-based requests also work in the unattended host, which has no
|
||||
// SDL video subsystem, and do not depend on the calling thread's lifetime.
|
||||
system = PowerSetRequest(request, PowerRequestSystemRequired) != 0;
|
||||
if (!system)
|
||||
LOG_WARN("Cannot prevent system idle sleep: {}", GetLastError());
|
||||
display = PowerSetRequest(request, PowerRequestDisplayRequired) != 0;
|
||||
if (!display)
|
||||
LOG_WARN("Cannot prevent display idle sleep: {}", GetLastError());
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
if (request == INVALID_HANDLE_VALUE) return;
|
||||
if (display) PowerClearRequest(request, PowerRequestDisplayRequired);
|
||||
if (system) PowerClearRequest(request, PowerRequestSystemRequired);
|
||||
CloseHandle(request);
|
||||
request = INVALID_HANDLE_VALUE;
|
||||
display = system = false;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
IOPMAssertionID display = kIOPMNullAssertionID;
|
||||
IOPMAssertionID system = kIOPMNullAssertionID;
|
||||
|
||||
void Start() {
|
||||
const auto reason = CFSTR("CrossDesk is sharing the desktop");
|
||||
auto result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypePreventUserIdleSystemSleep, kIOPMAssertionLevelOn,
|
||||
reason, &system);
|
||||
if (result != kIOReturnSuccess)
|
||||
LOG_WARN("Cannot prevent system idle sleep: {}", result);
|
||||
result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn,
|
||||
reason, &display);
|
||||
if (result != kIOReturnSuccess)
|
||||
LOG_WARN("Cannot prevent display idle sleep: {}", result);
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
if (display != kIOPMNullAssertionID) IOPMAssertionRelease(display);
|
||||
if (system != kIOPMNullAssertionID) IOPMAssertionRelease(system);
|
||||
display = system = kIOPMNullAssertionID;
|
||||
}
|
||||
#elif defined(__linux__) && !defined(__OHOS__)
|
||||
BusConnection session;
|
||||
dbus_uint32_t cookie = 0;
|
||||
bool desktop_inhibited = false;
|
||||
bool gnome = false;
|
||||
int sleep_fd = -1;
|
||||
|
||||
bool InhibitDesktop(bool use_gnome) {
|
||||
const char* service = use_gnome ? "org.gnome.SessionManager"
|
||||
: "org.freedesktop.ScreenSaver";
|
||||
const char* path = use_gnome ? "/org/gnome/SessionManager"
|
||||
: "/org/freedesktop/ScreenSaver";
|
||||
BusMessage message(dbus_message_new_method_call(service, path, service,
|
||||
"Inhibit"),
|
||||
dbus_message_unref);
|
||||
if (!message) return false;
|
||||
dbus_bool_t appended;
|
||||
if (use_gnome) {
|
||||
const dbus_uint32_t window = 0;
|
||||
const dbus_uint32_t flags = 4 | 8; // Suspend and session idle.
|
||||
appended = dbus_message_append_args(
|
||||
message.get(), DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_UINT32,
|
||||
&window, DBUS_TYPE_STRING, &kReason, DBUS_TYPE_UINT32, &flags,
|
||||
DBUS_TYPE_INVALID);
|
||||
} else {
|
||||
appended = dbus_message_append_args(
|
||||
message.get(), DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_STRING,
|
||||
&kReason, DBUS_TYPE_INVALID);
|
||||
}
|
||||
if (!appended) return false;
|
||||
auto reply = Call(session.get(), message.get());
|
||||
if (!reply || !dbus_message_get_args(reply.get(), nullptr, DBUS_TYPE_UINT32,
|
||||
&cookie, DBUS_TYPE_INVALID))
|
||||
return false;
|
||||
gnome = use_gnome;
|
||||
desktop_inhibited = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Start() {
|
||||
if (!dbus_threads_init_default()) {
|
||||
LOG_WARN("Cannot initialize D-Bus for screen sharing power management");
|
||||
return;
|
||||
}
|
||||
// These session APIs work without an SDL window, including native Wayland
|
||||
// and a Slint window hidden in the tray. GNOME's API inhibits both kinds of
|
||||
// idle sleep; other desktops use ScreenSaver plus logind below.
|
||||
session = OpenBus(DBUS_BUS_SESSION);
|
||||
if (session && InhibitDesktop(true)) return;
|
||||
if (!session || !InhibitDesktop(false))
|
||||
LOG_WARN("Cannot inhibit desktop idle while sharing the screen");
|
||||
|
||||
auto system_bus = OpenBus(DBUS_BUS_SYSTEM);
|
||||
if (!system_bus) return;
|
||||
BusMessage message(dbus_message_new_method_call(
|
||||
"org.freedesktop.login1", "/org/freedesktop/login1",
|
||||
"org.freedesktop.login1.Manager", "Inhibit"),
|
||||
dbus_message_unref);
|
||||
const char* what = "sleep:idle";
|
||||
const char* mode = "block";
|
||||
if (!message || !dbus_message_append_args(
|
||||
message.get(), DBUS_TYPE_STRING, &what,
|
||||
DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_STRING,
|
||||
&kReason, DBUS_TYPE_STRING, &mode, DBUS_TYPE_INVALID)) {
|
||||
LOG_WARN("Cannot create system sleep inhibition request");
|
||||
return;
|
||||
}
|
||||
auto reply = Call(system_bus.get(), message.get());
|
||||
// get_args duplicates the returned fd with CLOEXEC. Holding this fd keeps
|
||||
// the inhibitor alive independently of the reply or D-Bus connection.
|
||||
if (!reply || !dbus_message_get_args(reply.get(), nullptr, DBUS_TYPE_UNIX_FD,
|
||||
&sleep_fd, DBUS_TYPE_INVALID))
|
||||
LOG_WARN("Cannot inhibit system sleep while sharing the screen");
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
if (sleep_fd >= 0) {
|
||||
close(sleep_fd);
|
||||
sleep_fd = -1;
|
||||
}
|
||||
if (session && desktop_inhibited &&
|
||||
dbus_connection_get_is_connected(session.get())) {
|
||||
const char* service = gnome ? "org.gnome.SessionManager"
|
||||
: "org.freedesktop.ScreenSaver";
|
||||
const char* path = gnome ? "/org/gnome/SessionManager"
|
||||
: "/org/freedesktop/ScreenSaver";
|
||||
BusMessage message(dbus_message_new_method_call(
|
||||
service, path, service,
|
||||
gnome ? "Uninhibit" : "UnInhibit"),
|
||||
dbus_message_unref);
|
||||
if (message && dbus_message_append_args(
|
||||
message.get(), DBUS_TYPE_UINT32, &cookie,
|
||||
DBUS_TYPE_INVALID)) {
|
||||
dbus_message_set_no_reply(message.get(), true);
|
||||
dbus_connection_send(session.get(), message.get(), nullptr);
|
||||
dbus_connection_flush(session.get());
|
||||
}
|
||||
}
|
||||
// A private connection also revokes the desktop inhibitor if UnInhibit
|
||||
// could not be sent. Nothing is retained by unrelated D-Bus clients.
|
||||
session.reset();
|
||||
desktop_inhibited = false;
|
||||
gnome = false;
|
||||
cookie = 0;
|
||||
}
|
||||
#else
|
||||
void Start() {
|
||||
if (!SDL_DisableScreenSaver())
|
||||
LOG_WARN("Cannot prevent display idle sleep: {}", SDL_GetError());
|
||||
}
|
||||
void Stop() { SDL_EnableScreenSaver(); }
|
||||
#endif
|
||||
};
|
||||
|
||||
ScreenSharingPower::ScreenSharingPower() : impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
ScreenSharingPower::~ScreenSharingPower() { SetSharing(false); }
|
||||
|
||||
void ScreenSharingPower::SetSharing(bool sharing) {
|
||||
if (sharing_ == sharing) return;
|
||||
sharing_ = sharing;
|
||||
if (sharing) {
|
||||
impl_->Start();
|
||||
LOG_INFO("Requested display and system idle-sleep inhibition for screen sharing");
|
||||
} else {
|
||||
impl_->Stop();
|
||||
LOG_INFO("Released screen sharing power inhibitors");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-24
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_SHARING_POWER_H_
|
||||
#define _SCREEN_SHARING_POWER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Owns temporary display/system idle-sleep inhibitors for the shared capture
|
||||
// source. Call on the UI thread; repeated updates do not acquire extra requests.
|
||||
class ScreenSharingPower {
|
||||
public:
|
||||
ScreenSharingPower();
|
||||
~ScreenSharingPower();
|
||||
ScreenSharingPower(const ScreenSharingPower&) = delete;
|
||||
ScreenSharingPower& operator=(const ScreenSharingPower&) = delete;
|
||||
|
||||
void SetSharing(bool sharing);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
bool sharing_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -21,7 +21,6 @@
|
||||
#ifdef _WIN32
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "platform/windows/screen_capturer/screen_capturer_win.h"
|
||||
#include "platform/windows/input/mouse/mouse_controller.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
@@ -30,7 +29,7 @@ namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__OHOS__)
|
||||
constexpr auto kKeyboardCaptureRetryDelay = std::chrono::seconds(1);
|
||||
#endif
|
||||
|
||||
@@ -44,18 +43,17 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
|
||||
SpeakerCapturerFactory().Create());
|
||||
},
|
||||
[this](unsigned char *data, size_t size, const char *) {
|
||||
if (!owner_.peer_) return;
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
for (const auto& [id, status] : owner_.connection_status_) {
|
||||
if (status == ConnectionStatus::Connected)
|
||||
SendAudioFrameToPeer(owner_.peer_, &frame,
|
||||
owner_.audio_label_.c_str(), id.data(),
|
||||
id.size());
|
||||
}
|
||||
for (const auto& id : owner_.ConnectedControllers())
|
||||
owner_.WithController(
|
||||
id, [&](PeerPtr* peer, const std::string& wire) {
|
||||
frame.captured_timestamp = GetSystemTimeMicros(peer);
|
||||
return SendAudioFrameToPeer(peer, &frame,
|
||||
owner_.audio_label_.c_str(),
|
||||
wire.data(), wire.size());
|
||||
});
|
||||
}) {}
|
||||
|
||||
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
|
||||
@@ -133,6 +131,10 @@ void SessionDeviceManager::Initialize() {
|
||||
DeviceControllerFactory::Device::Keyboard));
|
||||
}
|
||||
|
||||
int SessionDeviceManager::EnsureCaptureReady() {
|
||||
return screen_capturer_ ? 0 : InitializeScreenCapturer();
|
||||
}
|
||||
|
||||
int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
||||
@@ -194,16 +196,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
.count()
|
||||
: 0;
|
||||
|
||||
std::vector<std::string> connected_remote_ids;
|
||||
{
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
connected_remote_ids.reserve(owner_.connection_status_.size());
|
||||
for (const auto &[remote_id, status] : owner_.connection_status_) {
|
||||
if (status == ConnectionStatus::Connected) {
|
||||
connected_remote_ids.push_back(remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto connected_remote_ids = owner_.ConnectedControllers();
|
||||
|
||||
// Capture can still deliver frames while ICE is gathering or after
|
||||
// the final controller disconnects. Do not broadcast those frames to
|
||||
@@ -233,28 +226,27 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
|
||||
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
||||
last_video_frame_stream_id_ != stream_id;
|
||||
if (resumed_after_gap || stream_changed) {
|
||||
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
||||
LOG_DEBUG("Request video key frame before sending captured frame, "
|
||||
"stream='{}', gap_ms={}, stream_changed={}",
|
||||
stream_id, duration_ms, stream_changed);
|
||||
}
|
||||
}
|
||||
|
||||
MiniRtcVideoFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
|
||||
frame.native_frame = native_frame;
|
||||
const auto send_started = std::chrono::steady_clock::now();
|
||||
uint64_t send_errors = 0;
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time,
|
||||
stream_id)) continue;
|
||||
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size()) != 0)
|
||||
if (owner_.WithController(
|
||||
remote_id, [&](PeerPtr* peer, const std::string& wire) {
|
||||
if (resumed_after_gap || stream_changed)
|
||||
RequestVideoKeyFrame(peer, stream_id.c_str());
|
||||
frame.captured_timestamp = GetSystemTimeMicros(peer);
|
||||
return SendVideoFrameToPeer(peer, &frame, stream_id.c_str(),
|
||||
wire.data(), wire.size());
|
||||
}) != 0)
|
||||
++send_errors;
|
||||
}
|
||||
const auto send_us =
|
||||
@@ -299,6 +291,21 @@ bool SessionDeviceManager::ShouldCaptureCursor() const {
|
||||
});
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::HasConnectedController() const {
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
return std::any_of(owner_.connection_status_.begin(),
|
||||
owner_.connection_status_.end(), [](const auto& entry) {
|
||||
return entry.second == ConnectionStatus::Connected;
|
||||
});
|
||||
}
|
||||
|
||||
void SessionDeviceManager::UpdateScreenSharingPower() {
|
||||
// Wayland keeps capture warm after the last disconnect. Only live viewers
|
||||
// justify keeping the host awake; an outgoing viewing session does not.
|
||||
screen_sharing_power_.SetSharing(owner_.screen_capturer_is_started_ &&
|
||||
HasConnectedController());
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartScreenCapturer() {
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
||||
@@ -318,8 +325,10 @@ int SessionDeviceManager::StartScreenCapturer() {
|
||||
cursor_capture_dirty_.store(false, std::memory_order_relaxed);
|
||||
const bool show_cursor = ShouldCaptureCursor();
|
||||
LOG_INFO("Start screen capturer, show cursor: {}", show_cursor);
|
||||
screen_sharing_power_.SetSharing(HasConnectedController());
|
||||
const int ret = screen_capturer_->Start(show_cursor);
|
||||
if (ret != 0) {
|
||||
screen_sharing_power_.SetSharing(false);
|
||||
LOG_ERROR("Start screen capturer failed: {}", ret);
|
||||
}
|
||||
return ret;
|
||||
@@ -330,29 +339,49 @@ int SessionDeviceManager::StopScreenCapturer() {
|
||||
LOG_INFO("Stop screen capturer");
|
||||
screen_capturer_->Stop();
|
||||
}
|
||||
owner_.screen_capturer_is_started_ = false;
|
||||
screen_sharing_power_.SetSharing(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::ReleaseRemoteMouseButtons() {
|
||||
return remote_mouse_buttons_.ReleaseAll(
|
||||
[this](unsigned mask) { return ReleaseMouseButtonMask(mask); });
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::ReleaseRemoteMouseButtons(const std::string& remote_id,
|
||||
unsigned mask) {
|
||||
return remote_mouse_buttons_.ReleaseController(
|
||||
remote_id, [this](unsigned buttons) { return ReleaseMouseButtonMask(buttons); }, mask);
|
||||
}
|
||||
|
||||
unsigned SessionDeviceManager::ReleaseMouseButtonMask(unsigned mask) {
|
||||
unsigned released = mask;
|
||||
{
|
||||
std::lock_guard lock(mouse_controller_mutex_);
|
||||
if (mouse_controller_) released &= mouse_controller_->ReleasePressedButtons(mask);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
void SessionDeviceManager::ReleaseRemoteMouseButtons() {
|
||||
if (auto mouse = dynamic_cast<PlatformMouseController*>(mouse_controller_))
|
||||
mouse->ReleasePressedButtons();
|
||||
std::lock_guard lock(service_mouse_mutex_);
|
||||
for (int button = 0; button < 3; ++button) {
|
||||
const unsigned bit = 1u << button;
|
||||
if (!(service_mouse_buttons_ & bit)) continue;
|
||||
if (!(service_mouse_buttons_ & mask & bit)) continue;
|
||||
const auto response = SendCrossDeskSecureDesktopMouseInput(
|
||||
service_mouse_x_, service_mouse_y_, 0, (button + 1) * 2, 1000);
|
||||
const auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_object() && json.value("ok", false)) {
|
||||
service_mouse_buttons_ &= ~bit;
|
||||
} else {
|
||||
released &= ~bit;
|
||||
LOG_WARN("Service mouse button release failed: button={}, response={}",
|
||||
button, response);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return released;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string SessionDeviceManager::SendServiceMouseCommand(int x, int y,
|
||||
int wheel, int flag) {
|
||||
std::lock_guard lock(service_mouse_mutex_);
|
||||
@@ -398,7 +427,10 @@ bool SessionDeviceManager::SyncDisplayInfo() {
|
||||
});
|
||||
if (changed) display_info_list_ = displays;
|
||||
}
|
||||
if (changed && mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays);
|
||||
if (changed) {
|
||||
std::lock_guard lock(mouse_controller_mutex_);
|
||||
if (mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -419,7 +451,7 @@ int SessionDeviceManager::StartMouseController() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
if (!screen_capturer_) {
|
||||
return 1;
|
||||
@@ -434,6 +466,7 @@ int SessionDeviceManager::StartMouseController() {
|
||||
#endif
|
||||
|
||||
SyncDisplayInfo();
|
||||
std::lock_guard lock(mouse_controller_mutex_);
|
||||
mouse_controller_ =
|
||||
static_cast<MouseController *>(device_controller_factory_->Create(
|
||||
DeviceControllerFactory::Device::Mouse));
|
||||
@@ -452,10 +485,16 @@ int SessionDeviceManager::StartMouseController() {
|
||||
return init_ret;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopMouseController() {
|
||||
#ifdef _WIN32
|
||||
int SessionDeviceManager::StopMouseController(bool force) {
|
||||
// Retain the native button state across transient cleanup failures. The UI
|
||||
// can keep pumping while the input worker retries, including with no peers.
|
||||
if (!force && owner_.remote_input_.HasPendingReleases()) return 1;
|
||||
// A disconnect has revoked queued input. Finish its running injection and
|
||||
// release callback before destroying the native controller.
|
||||
owner_.remote_input_.WaitIdle();
|
||||
if (!force && owner_.remote_input_.HasPendingReleases()) return 1;
|
||||
ReleaseRemoteMouseButtons();
|
||||
#endif
|
||||
std::lock_guard lock(mouse_controller_mutex_);
|
||||
if (mouse_controller_) {
|
||||
mouse_controller_->Destroy();
|
||||
delete mouse_controller_;
|
||||
@@ -467,7 +506,7 @@ int SessionDeviceManager::StopMouseController() {
|
||||
int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
captured_keyboard_inputs_.Clear();
|
||||
owner_.keyboard_capturer_uses_window_events_ = false;
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__OHOS__)
|
||||
next_keyboard_capture_retry_ =
|
||||
std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay;
|
||||
#endif
|
||||
@@ -479,7 +518,7 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
|
||||
@@ -546,7 +585,7 @@ int SessionDeviceManager::InitializeAudioOutput() {
|
||||
};
|
||||
|
||||
if (!open_device()) {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
LOG_WARN("Failed to open output stream with driver [{}]: {}",
|
||||
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER")
|
||||
: "(default)",
|
||||
@@ -620,6 +659,8 @@ void SessionDeviceManager::PushAudio(const std::string& remote_id,
|
||||
}
|
||||
|
||||
void SessionDeviceManager::UpdateInteractions() {
|
||||
// Release even when pending remote-input cleanup makes this tick return early.
|
||||
UpdateScreenSharingPower();
|
||||
#ifdef _WIN32
|
||||
if (owner_.screen_capturer_is_started_ &&
|
||||
cursor_capture_dirty_.load(std::memory_order_relaxed)) {
|
||||
@@ -629,16 +670,16 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
const bool is_wayland_session = IsWaylandSession();
|
||||
const bool stop_wayland_mouse_before_screen =
|
||||
is_wayland_session && !owner_.start_screen_capturer_ &&
|
||||
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
|
||||
owner_.mouse_controller_is_started_;
|
||||
if (stop_wayland_mouse_before_screen) {
|
||||
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
|
||||
if (StopMouseController() != 0) return;
|
||||
LOG_INFO("Stopped Wayland mouse controller before screen capturer to "
|
||||
"cleanly release the shared portal session");
|
||||
StopMouseController();
|
||||
owner_.mouse_controller_is_started_ = false;
|
||||
}
|
||||
#endif
|
||||
@@ -653,35 +694,39 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.screen_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
UpdateScreenSharingPower();
|
||||
|
||||
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
|
||||
if (StartMouseController() == 0) {
|
||||
owner_.mouse_controller_is_started_ = true;
|
||||
}
|
||||
} else if (!owner_.start_mouse_controller_ &&
|
||||
owner_.mouse_controller_is_started_) {
|
||||
StopMouseController();
|
||||
owner_.mouse_controller_is_started_ = false;
|
||||
if (StopMouseController() == 0) owner_.mouse_controller_is_started_ = false;
|
||||
}
|
||||
|
||||
if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) {
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__OHOS__)
|
||||
const int active_display = screen_capturer_->GetCurrentMonitorIndex();
|
||||
const auto displays = display_info_list();
|
||||
if (active_display >= 0 &&
|
||||
active_display < static_cast<int>(displays.size())) {
|
||||
owner_.selected_display_ = active_display;
|
||||
#ifdef _WIN32
|
||||
if (displays[active_display].width <= 0 ||
|
||||
displays[active_display].height <= 0)
|
||||
ReleaseRemoteMouseButtons();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_ &&
|
||||
!owner_.privacy_.Snapshot().input_blocked) {
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__OHOS__)
|
||||
if (owner_.keyboard_capturer_is_started_ &&
|
||||
owner_.keyboard_capturer_uses_window_events_ && keyboard_capturer_ &&
|
||||
keyboard_capturer_->ShouldRetryHook() &&
|
||||
std::chrono::steady_clock::now() >= next_keyboard_capture_retry_) {
|
||||
// A transient installation failure or stalled hook must not leave
|
||||
// system shortcuts in the window-event fallback until focus changes.
|
||||
@@ -699,7 +744,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.keyboard_.ForceReleasePressedKeys();
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
owner_.keyboard_capturer_is_started_ = true;
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__OHOS__)
|
||||
next_keyboard_capture_retry_ =
|
||||
std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay;
|
||||
#endif
|
||||
@@ -730,6 +775,14 @@ void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||
}
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::SupportsTextInput() const {
|
||||
return keyboard_capturer_ && keyboard_capturer_->SupportsTextInput();
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::SendTextCommand(const std::string& text) {
|
||||
return keyboard_capturer_ && keyboard_capturer_->SendTextCommand(text) == 0;
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
@@ -739,6 +792,7 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||
|
||||
bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
||||
int selected_display) {
|
||||
std::lock_guard lock(mouse_controller_mutex_);
|
||||
if (mouse_controller_) {
|
||||
return mouse_controller_->SendMouseCommand(action, selected_display) == 0;
|
||||
}
|
||||
@@ -752,19 +806,20 @@ int SessionDeviceManager::SwitchDisplay(int display_id) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::ResetToInitialDisplay() {
|
||||
InputOutcome SessionDeviceManager::ResetToInitialDisplay() {
|
||||
if (!screen_capturer_) {
|
||||
return;
|
||||
return InputOutcome::ignored;
|
||||
}
|
||||
const int ret = screen_capturer_->ResetToInitialMonitor();
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Reset to initial display failed, ret={}", ret);
|
||||
return;
|
||||
return InputOutcome::failed;
|
||||
}
|
||||
// Capture backends start at display 0. Reset the input mapping as well so
|
||||
// the next connection cannot show that display while controlling another.
|
||||
owner_.selected_display_ = 0;
|
||||
SyncDisplayInfo();
|
||||
return InputOutcome::applied;
|
||||
}
|
||||
|
||||
std::vector<DisplayInfo> SessionDeviceManager::display_info_list() const {
|
||||
@@ -784,8 +839,10 @@ std::vector<HostDisplay> SessionDeviceManager::host_display_list() const {
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DestroyDevices() {
|
||||
owner_.screen_capturer_is_started_ = false;
|
||||
screen_sharing_power_.SetSharing(false);
|
||||
speaker_capture_.Shutdown();
|
||||
StopMouseController();
|
||||
StopMouseController(true);
|
||||
if (screen_capturer_) {
|
||||
screen_capturer_->Destroy();
|
||||
delete screen_capturer_;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
@@ -21,6 +22,8 @@
|
||||
#include "device_controller.h"
|
||||
#include "display_info.h"
|
||||
#include "features/input/captured_keyboard_queue.h"
|
||||
#include "features/input/remote_mouse_buttons.h"
|
||||
#include "screen_sharing_power.h"
|
||||
#include "speaker_capture_controller.h"
|
||||
|
||||
struct SDL_AudioStream;
|
||||
@@ -51,10 +54,18 @@ public:
|
||||
void DestroyFactories();
|
||||
|
||||
int InitializeScreenCapturer();
|
||||
int EnsureCaptureReady();
|
||||
int StartScreenCapturer();
|
||||
int StopScreenCapturer();
|
||||
bool ReleaseRemoteMouseButtons();
|
||||
bool ReleaseRemoteMouseButtons(const std::string& remote_id,
|
||||
unsigned mask = 7);
|
||||
template <typename Send>
|
||||
InputOutcome ApplyRemoteMouse(const std::string& remote_id, MouseFlag flag,
|
||||
Send send) {
|
||||
return remote_mouse_buttons_.Apply(remote_id, flag, std::move(send));
|
||||
}
|
||||
#ifdef _WIN32
|
||||
void ReleaseRemoteMouseButtons();
|
||||
std::string SendServiceMouseCommand(int x, int y, int wheel, int flag);
|
||||
#endif
|
||||
// Requests are asynchronous; Stop drains/revokes audio delivery before
|
||||
@@ -62,7 +73,9 @@ public:
|
||||
void StartSpeakerCapturer();
|
||||
void StopSpeakerCapturer();
|
||||
int StartMouseController();
|
||||
int StopMouseController();
|
||||
// Returns 1 while background releases are pending. force is only used
|
||||
// during final destruction after the input dispatcher has been stopped.
|
||||
int StopMouseController(bool force = false);
|
||||
int StartKeyboardCapturer();
|
||||
int StopKeyboardCapturer();
|
||||
|
||||
@@ -73,11 +86,13 @@ public:
|
||||
void ClearAudio(const std::string& remote_id);
|
||||
void PushAudio(const std::string& remote_id, const char *data, size_t size);
|
||||
|
||||
bool SupportsTextInput() const;
|
||||
bool SendTextCommand(const std::string& text);
|
||||
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
bool SendMouseCommand(const RemoteAction &action, int selected_display);
|
||||
int SwitchDisplay(int display_id);
|
||||
void ResetToInitialDisplay();
|
||||
InputOutcome ResetToInitialDisplay();
|
||||
|
||||
// Snapshot of the capture displays. The UI thread refreshes the list every
|
||||
// tick while transport threads build host information and map mouse input
|
||||
@@ -87,10 +102,13 @@ public:
|
||||
std::vector<HostDisplay> host_display_list() const;
|
||||
|
||||
private:
|
||||
unsigned ReleaseMouseButtonMask(unsigned mask);
|
||||
void DrainCapturedKeyboardInput();
|
||||
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
||||
int fps);
|
||||
bool ShouldCaptureCursor() const;
|
||||
bool HasConnectedController() const;
|
||||
void UpdateScreenSharingPower();
|
||||
bool SyncDisplayInfo();
|
||||
void SetDisplayInfoList(std::vector<DisplayInfo> displays);
|
||||
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
|
||||
@@ -101,18 +119,24 @@ private:
|
||||
std::unordered_map<std::string, SDL_AudioStream*> output_streams_;
|
||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||
ScreenCapturer *screen_capturer_ = nullptr;
|
||||
ScreenSharingPower screen_sharing_power_;
|
||||
std::atomic<bool> cursor_capture_dirty_{true};
|
||||
SpeakerCaptureController speaker_capture_;
|
||||
DeviceControllerFactory *device_controller_factory_ = nullptr;
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
RemoteMouseButtons remote_mouse_buttons_;
|
||||
// Input is executed on a worker while the UI starts/stops this controller.
|
||||
std::mutex mouse_controller_mutex_;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
// Guards display_info_list_ (UI thread writes, transport threads read).
|
||||
mutable std::mutex display_info_mutex_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
size_t registered_display_stream_count_ = 0;
|
||||
CapturedKeyboardQueue captured_keyboard_inputs_;
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__OHOS__)
|
||||
std::chrono::steady_clock::time_point next_keyboard_capture_retry_{};
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
std::mutex service_mouse_mutex_;
|
||||
unsigned service_mouse_buttons_ = 0;
|
||||
int service_mouse_x_ = 0, service_mouse_y_ = 0;
|
||||
|
||||
@@ -1,370 +1,412 @@
|
||||
#include "features/file_transfer/file_transfer_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "filesystem_utf8.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
FileTransferManager::FileTransferManager(GuiRuntime& owner) : owner_(owner) {}
|
||||
FileTransferManager::~FileTransferManager() { Shutdown(); }
|
||||
|
||||
FileTransferManager::~FileTransferManager() {
|
||||
void FileTransferManager::Shutdown() {
|
||||
std::vector<std::future<void>> workers;
|
||||
{
|
||||
std::lock_guard lock(workers_mutex_);
|
||||
stopping_ = true;
|
||||
workers.swap(workers_);
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
for (auto& [id, sender] : senders_) sender.session->Cancel();
|
||||
for (auto& [handler, peers] : receivers_)
|
||||
for (auto& [id, receiver] : peers) receiver.session->Cancel();
|
||||
receivers_.clear();
|
||||
}
|
||||
wake_.notify_all();
|
||||
for (auto& worker : workers) worker.wait();
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
|
||||
return global_state_;
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::state_for(
|
||||
const std::shared_ptr<RemoteSession> &props) {
|
||||
FileTransferManager::FileTransferState& FileTransferManager::global_state() { return global_state_; }
|
||||
FileTransferManager::FileTransferState& FileTransferManager::received_state() { return received_state_; }
|
||||
FileTransferManager::FileTransferState& FileTransferManager::state_for(const std::shared_ptr<RemoteSession>& props) {
|
||||
return props ? props->file_transfer_ : global_state_;
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessSelectedFile(
|
||||
const std::string &path,
|
||||
const std::shared_ptr<RemoteSession> &props,
|
||||
const std::string &file_label, const std::string &remote_id) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState &state = state_for(props);
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
const std::filesystem::path file_path = PathFromUtf8(path);
|
||||
bool FileTransferManager::ProcessSelectedFile(const std::string& path,
|
||||
const std::shared_ptr<RemoteSession>& props, const std::string& file_label,
|
||||
const std::string& remote_id, uint32_t file_id) {
|
||||
if (path.empty() || stopping_ || (props && props->closing_)) return false;
|
||||
const auto file_path = PathFromUtf8(path);
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("Selected path is not a regular file: {}", path);
|
||||
return;
|
||||
}
|
||||
const uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) return false;
|
||||
const auto size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) return false;
|
||||
auto& state = state_for(props);
|
||||
// Allocate once at enqueue time so queued and active notifications keep the
|
||||
// same identity, including repeated sends of the same path.
|
||||
const auto id = file_id ? file_id : file_transfer::Session::NextFileId();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_transfer_list_mutex_);
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
const auto utf8_name = file_path.filename().u8string();
|
||||
info.file_name.assign(reinterpret_cast<const char *>(utf8_name.data()),
|
||||
utf8_name.size());
|
||||
info.file_path = file_path;
|
||||
info.file_size = file_size;
|
||||
const auto name = file_path.filename().u8string();
|
||||
info.file_name.assign(name.begin(), name.end());
|
||||
info.remote_id = props ? props->remote_id_ : remote_id;
|
||||
info.file_path = file_path; info.file_size = size;
|
||||
info.file_id = id;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
state.file_transfer_list_.push_back(std::move(info));
|
||||
}
|
||||
state.file_transfer_window_visible_ = true;
|
||||
|
||||
auto enqueue = [&]() {
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
|
||||
state.file_send_queue_.push(
|
||||
FileTransferState::QueuedFile{file_path, file_label, remote_id});
|
||||
queue_size = state.file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
};
|
||||
|
||||
if (state.file_sending_.load()) {
|
||||
enqueue();
|
||||
return;
|
||||
}
|
||||
|
||||
Start(props, file_path, file_label, remote_id);
|
||||
if (!state.file_sending_.load()) {
|
||||
enqueue();
|
||||
{
|
||||
std::lock_guard lock(state.file_queue_mutex_);
|
||||
state.file_send_queue_.push({file_path, file_label, props ? props->remote_id_ : remote_id, id});
|
||||
}
|
||||
ProcessQueue(props);
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileTransferManager::Start(
|
||||
std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path, const std::string &file_label,
|
||||
const std::string &remote_id) {
|
||||
if (props && props->closing_) return;
|
||||
const bool is_global = !props;
|
||||
PeerPtr* peer = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events;
|
||||
{
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
if (props && props->closing_) return;
|
||||
peer = is_global ? owner_.peer_ : props->peer_;
|
||||
peer_events = is_global ? owner_.peer_events_ : props->peer_events_;
|
||||
}
|
||||
if (!peer) {
|
||||
LOG_ERROR("StartFileTransfer: invalid peer");
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState &initial_state = state_for(props);
|
||||
bool expected = false;
|
||||
if (!initial_state.file_sending_.compare_exchange_strong(expected, true)) {
|
||||
LOG_WARN("StartFileTransfer called while another file is active: {}",
|
||||
file_path.filename().string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto props_weak = std::weak_ptr<RemoteSession>(props);
|
||||
void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
||||
std::lock_guard workers_lock(workers_mutex_);
|
||||
if (stopping_) return;
|
||||
// Reap finished senders without blocking the UI.
|
||||
auto& state = state_for(props);
|
||||
if (stopping_ || state.file_sending_) return;
|
||||
FileTransferState::QueuedFile file;
|
||||
{
|
||||
std::lock_guard lock(state.file_queue_mutex_);
|
||||
if (state.file_send_queue_.empty()) return;
|
||||
file = state.file_send_queue_.front(); state.file_send_queue_.pop();
|
||||
state.file_sending_ = true;
|
||||
}
|
||||
std::erase_if(workers_, [](auto& worker) {
|
||||
return worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
});
|
||||
workers_.push_back(std::async(std::launch::async,
|
||||
[this, peer, peer_events, file_path, file_label, props_weak, remote_id,
|
||||
is_global]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
FileTransferState *state = nullptr;
|
||||
if (props_locked) {
|
||||
state = &props_locked->file_transfer_;
|
||||
} else if (is_global) {
|
||||
state = &global_state_;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const uint64_t total_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
state->file_sending_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
state->file_sent_bytes_ = 0;
|
||||
state->file_total_bytes_ = total_size;
|
||||
state->file_send_rate_bps_ = 0;
|
||||
state->file_transfer_window_visible_ = true;
|
||||
workers_.push_back(std::async(std::launch::async, [this, props, file] {
|
||||
auto& state = state_for(props);
|
||||
const auto id = file.file_id;
|
||||
PeerPtr* peer;
|
||||
std::shared_ptr<PeerEventHandler> events;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
state->file_send_start_time_ = std::chrono::steady_clock::now();
|
||||
state->file_send_last_update_time_ = state->file_send_start_time_;
|
||||
state->file_send_last_bytes_ = 0;
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
peer = props ? props->peer_ : owner_.peer_;
|
||||
events = props ? props->peer_events_ : owner_.peer_events_;
|
||||
}
|
||||
|
||||
FileSender sender;
|
||||
const uint32_t file_id = FileSender::NextFileId();
|
||||
if (props_locked) {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
|
||||
file_id_to_props_[file_id] = props_weak;
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
|
||||
file_id_to_state_[file_id] = state;
|
||||
if (!props && file.remote_id.rfind("lan/", 0) == 0) {
|
||||
events = owner_.ControllerEvents(file.remote_id);
|
||||
peer = events ? events->DirectPeer() : nullptr;
|
||||
}
|
||||
state->current_file_id_ = file_id;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto &info : state->file_transfer_list_) {
|
||||
if (info.file_path == file_path &&
|
||||
info.status == FileTransferState::FileTransferStatus::Queued) {
|
||||
info.status = FileTransferState::FileTransferStatus::Sending;
|
||||
info.file_id = file_id;
|
||||
info.file_size = total_size;
|
||||
info.sent_bytes = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, peer_events, file_label, remote_id, props_locked](const char *buffer, size_t size) {
|
||||
// Close drains each in-flight send before detaching the peer.
|
||||
if (props_locked && props_locked->closing_) return -1;
|
||||
auto callback = peer_events->EnterCallback();
|
||||
if (!callback) return -1;
|
||||
if (remote_id.empty()) {
|
||||
return SendReliableDataFrame(peer, buffer, size,
|
||||
file_label.c_str());
|
||||
}
|
||||
auto session = std::make_shared<file_transfer::Session>(
|
||||
[peer, events, file, props](const char*, const char* data, size_t size) {
|
||||
if (!peer || !events || (props && props->closing_)) return -1;
|
||||
auto callback = events->EnterCallback();
|
||||
if (!callback || !events->DataAllowed()) return -1;
|
||||
const auto wire_id = events->WireId(file.remote_id);
|
||||
return SendReliableDataFrameToPeer(
|
||||
peer, buffer, size, file_label.c_str(), remote_id.c_str(),
|
||||
remote_id.size());
|
||||
},
|
||||
64 * 1024, file_id);
|
||||
|
||||
if (ret == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->file_sending_ = false;
|
||||
state->file_transfer_window_visible_ = false;
|
||||
state->file_sent_bytes_ = 0;
|
||||
state->file_total_bytes_ = 0;
|
||||
state->file_send_rate_bps_ = 0;
|
||||
state->current_file_id_ = 0;
|
||||
Unregister(file_id, props_locked != nullptr);
|
||||
|
||||
peer, data, size, file.file_label.c_str(),
|
||||
wire_id.data(), wire_id.size());
|
||||
}, file_transfer::Session::Progress{});
|
||||
state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0; state.file_send_rate_bps_ = 0;
|
||||
state.current_file_id_ = id;
|
||||
const auto started_at = std::chrono::steady_clock::now();
|
||||
auto next_rate_update = started_at + std::chrono::seconds(1);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto &info : state->file_transfer_list_) {
|
||||
if (info.file_id == file_id) {
|
||||
info.status = FileTransferState::FileTransferStatus::Failed;
|
||||
std::lock_guard lock(state.file_transfer_mutex_);
|
||||
state.file_send_start_time_ = state.file_send_last_update_time_ = started_at;
|
||||
state.file_send_last_bytes_ = 0;
|
||||
}
|
||||
bool cancelled = false;
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (auto& info : state.file_transfer_list_) {
|
||||
if (info.file_id == id) {
|
||||
cancelled = info.status == FileTransferState::FileTransferStatus::Cancelled;
|
||||
if (!cancelled) info.status = FileTransferState::FileTransferStatus::Sending;
|
||||
info.file_id = id; info.sent_bytes = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
if (peer_events->IsActive()) ProcessQueue(props_locked);
|
||||
}));
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessQueue(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
if (props && props->closing_) return;
|
||||
FileTransferState &state = state_for(props);
|
||||
if (state.file_sending_.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
|
||||
if (state.file_send_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
queued_file = state.file_send_queue_.front();
|
||||
state.file_send_queue_.pop();
|
||||
}
|
||||
Start(props, queued_file.file_path, queued_file.file_label,
|
||||
queued_file.remote_id);
|
||||
}
|
||||
|
||||
void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
|
||||
if (per_peer) {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
|
||||
file_id_to_props_.erase(file_id);
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
|
||||
file_id_to_state_.erase(file_id);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::HandleAck(const char *data, size_t size) {
|
||||
FileTransferAck ack{};
|
||||
if (!DecodeFileTransferAck(data, size, &ack)) {
|
||||
LOG_ERROR("FileTransferAck: invalid payload, size={}", size);
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<RemoteSession> props;
|
||||
{
|
||||
std::shared_lock lock(file_id_to_props_mutex_);
|
||||
const auto it = file_id_to_props_.find(ack.file_id);
|
||||
if (it != file_id_to_props_.end()) {
|
||||
props = it->second.lock();
|
||||
}
|
||||
}
|
||||
|
||||
FileTransferState *state = props ? &props->file_transfer_ : nullptr;
|
||||
if (!state) {
|
||||
std::shared_lock lock(file_id_to_state_mutex_);
|
||||
const auto it = file_id_to_state_.find(ack.file_id);
|
||||
if (it != file_id_to_state_.end()) {
|
||||
state = it->second;
|
||||
}
|
||||
}
|
||||
if (!state) {
|
||||
LOG_WARN("FileTransferAck: no state found for file_id={}", ack.file_id);
|
||||
return;
|
||||
}
|
||||
|
||||
state->file_sent_bytes_ = ack.acked_offset;
|
||||
state->file_total_bytes_ = ack.total_size;
|
||||
uint32_t rate_bps = 0;
|
||||
if (props) {
|
||||
const uint32_t bitrate =
|
||||
props->net_traffic_stats_.DataOutboundBitrate();
|
||||
if (bitrate > 0 && state->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
}
|
||||
} else {
|
||||
rate_bps = state->file_send_rate_bps_.load();
|
||||
}
|
||||
} else {
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
uint32_t estimated_rate = 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
uint64_t last_bytes = 0;
|
||||
std::chrono::steady_clock::time_point last_time;
|
||||
const auto name = file.file_path.filename().u8string();
|
||||
bool started = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
last_bytes = state->file_send_last_bytes_;
|
||||
last_time = state->file_send_last_update_time_;
|
||||
}
|
||||
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
|
||||
const auto delta_bytes = ack.acked_offset - last_bytes;
|
||||
const double seconds =
|
||||
std::chrono::duration<double>(now - last_time).count();
|
||||
if (seconds > 0.0 && delta_bytes > 0) {
|
||||
const double bits_per_second = delta_bytes * 8.0 / seconds;
|
||||
estimated_rate = static_cast<uint32_t>((std::min)(
|
||||
bits_per_second,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
const auto closed = disconnected_.find(events.get());
|
||||
const bool disconnected = closed != disconnected_.end() &&
|
||||
(closed->second.count("") || closed->second.count(file.remote_id));
|
||||
if (!cancelled && !stopping_ && peer && events && events->IsActive() && !disconnected &&
|
||||
(!props || !props->closing_) && !file.remote_id.empty()) {
|
||||
started = session->Start(file.file_path, std::string(name.begin(), name.end()),
|
||||
file_transfer::Session::Clock::now(), id);
|
||||
if (started) senders_.emplace(id, Sender{session, events, file.remote_id});
|
||||
}
|
||||
}
|
||||
rate_bps =
|
||||
estimated_rate > 0 && current_rate > 0
|
||||
? static_cast<uint32_t>(current_rate * 0.7 + estimated_rate * 0.3)
|
||||
: estimated_rate > 0 ? estimated_rate
|
||||
: current_rate;
|
||||
}
|
||||
|
||||
state->file_send_rate_bps_ = rate_bps;
|
||||
state->file_send_last_bytes_ = ack.acked_offset;
|
||||
state->file_send_last_update_time_ = std::chrono::steady_clock::now();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto &info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.sent_bytes = ack.acked_offset;
|
||||
info.file_size = ack.total_size;
|
||||
info.rate_bps = rate_bps;
|
||||
if ((ack.flags & 0x01) != 0) {
|
||||
info.status = FileTransferState::FileTransferStatus::Completed;
|
||||
info.sent_bytes = ack.total_size;
|
||||
uint64_t previous = UINT64_MAX;
|
||||
auto progress = file_transfer::SendState{id, 0, 0,
|
||||
cancelled ? file_transfer::Status::Cancelled : file_transfer::Status::Failed};
|
||||
if (started) {
|
||||
do {
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (const auto& info : state.file_transfer_list_) if (info.file_id == id) {
|
||||
cancelled = info.status == FileTransferState::FileTransferStatus::Cancelling;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cancelled) session->CancelSend();
|
||||
else if (stopping_ || !events->IsActive() || (props && props->closing_)) session->Cancel();
|
||||
const bool pumped = session->Pump();
|
||||
progress = session->Snapshot();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool rate_due = now >= next_rate_update;
|
||||
if (progress.acknowledged != previous || rate_due ||
|
||||
progress.status != file_transfer::Status::Sending) {
|
||||
UpdateProgress(state, progress, now);
|
||||
previous = progress.acknowledged;
|
||||
if (rate_due) next_rate_update = now + std::chrono::seconds(1);
|
||||
}
|
||||
if (progress.status != file_transfer::Status::Sending) break;
|
||||
if (!pumped) {
|
||||
std::unique_lock lock(wake_mutex_);
|
||||
wake_.wait_for(lock, std::chrono::milliseconds(5));
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
senders_.erase(id);
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (auto& info : state.file_transfer_list_) if (info.file_id == id) {
|
||||
const bool cancelled = progress.status == file_transfer::Status::Cancelled ||
|
||||
info.status == FileTransferState::FileTransferStatus::Cancelled ||
|
||||
info.status == FileTransferState::FileTransferStatus::Cancelling;
|
||||
info.status = progress.status == file_transfer::Status::Completed
|
||||
? FileTransferState::FileTransferStatus::Completed
|
||||
: cancelled ? FileTransferState::FileTransferStatus::Cancelled
|
||||
: FileTransferState::FileTransferStatus::Failed;
|
||||
info.rate_bps = 0;
|
||||
if (info.status == FileTransferState::FileTransferStatus::Completed) info.sent_bytes = info.file_size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((ack.flags & 0x01) == 0) {
|
||||
return;
|
||||
}
|
||||
LOG_INFO("File transfer completed: file_id={}, bytes={}", ack.file_id,
|
||||
ack.total_size);
|
||||
state->file_transfer_window_visible_ = true;
|
||||
state->file_sending_ = false;
|
||||
Unregister(ack.file_id, props != nullptr);
|
||||
ProcessQueue(props);
|
||||
if (progress.status != file_transfer::Status::Completed) {
|
||||
state.file_transfer_window_visible_ = false;
|
||||
state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0;
|
||||
state.file_send_rate_bps_ = 0; state.current_file_id_ = 0;
|
||||
} else state.file_transfer_window_visible_ = true;
|
||||
state.file_sending_ = false;
|
||||
// Only the worker advances the queue, after all sends for this file drained.
|
||||
ProcessQueue(props);
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
void FileTransferManager::UpdateProgress(FileTransferState& state,
|
||||
const file_transfer::SendState& progress,
|
||||
std::chrono::steady_clock::time_point now) {
|
||||
state.file_sent_bytes_ = progress.acknowledged; state.file_total_bytes_ = progress.total;
|
||||
uint32_t rate = state.file_send_rate_bps_.load();
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_mutex_);
|
||||
const auto elapsed = now - state.file_send_last_update_time_;
|
||||
// Progress follows every ACK; speed uses one full sample per second for
|
||||
// this file. Sample even without new ACKs so a stalled transfer reads zero.
|
||||
if (elapsed >= std::chrono::seconds(1)) {
|
||||
const double seconds = std::chrono::duration<double>(elapsed).count();
|
||||
const auto bytes = progress.acknowledged >= state.file_send_last_bytes_
|
||||
? progress.acknowledged - state.file_send_last_bytes_ : 0;
|
||||
const double measured = bytes * 8.0 / seconds;
|
||||
rate = static_cast<uint32_t>((std::min)(measured,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
state.file_send_rate_bps_ = rate;
|
||||
state.file_send_last_bytes_ = progress.acknowledged;
|
||||
state.file_send_last_update_time_ = now;
|
||||
}
|
||||
}
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (auto& info : state.file_transfer_list_) if (info.file_id == progress.id) {
|
||||
info.sent_bytes = progress.acknowledged; info.file_size = progress.total; info.rate_bps = rate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool FileTransferManager::CancelSend(uint32_t file_id) {
|
||||
using Status = FileTransferState::FileTransferStatus;
|
||||
const auto cancel = [file_id](FileTransferState& state) {
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (auto& info : state.file_transfer_list_) {
|
||||
if (info.file_id != file_id || info.receiving) continue;
|
||||
if (info.status != Status::Queued && info.status != Status::Sending) return false;
|
||||
info.status = info.status == Status::Queued ? Status::Cancelled : Status::Cancelling;
|
||||
info.rate_bps = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
bool cancelled = cancel(global_state_);
|
||||
if (!cancelled) {
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
for (const auto& [_, props] : owner_.remote_sessions_) {
|
||||
if (props && cancel(props->file_transfer_)) { cancelled = true; break; }
|
||||
}
|
||||
}
|
||||
if (cancelled) wake_.notify_all();
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
void FileTransferManager::HandleAck(const char* data, size_t size,
|
||||
PeerEventHandler* handler, const std::string& remote) {
|
||||
FileTransferAck ack{};
|
||||
if (!DecodeFileTransferAck(data, size, &ack)) return;
|
||||
std::shared_ptr<file_transfer::Session> session;
|
||||
{
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
const auto it = senders_.find(ack.file_id);
|
||||
if (it == senders_.end() || it->second.handler.get() != handler || it->second.remote != remote) return;
|
||||
session = it->second.session;
|
||||
}
|
||||
session->Ack(data, size);
|
||||
wake_.notify_all();
|
||||
}
|
||||
|
||||
void FileTransferManager::UpdateReceiveProgress(
|
||||
const file_transfer::ReceiveState& progress,
|
||||
std::unordered_map<uint32_t, size_t>& notifications,
|
||||
const std::string& remote_id) {
|
||||
using Status = FileTransferState::FileTransferStatus;
|
||||
std::lock_guard lock(received_state_.file_transfer_list_mutex_);
|
||||
auto& files = received_state_.file_transfer_list_;
|
||||
auto found = notifications.find(progress.id);
|
||||
if (found == notifications.end()) {
|
||||
FileTransferState::FileTransferInfo info;
|
||||
// Wire IDs can collide between peers or with outgoing transfers. Give each
|
||||
// reception a local notification identity from the same allocator as sends.
|
||||
info.file_id = file_transfer::Session::NextFileId();
|
||||
info.remote_id = remote_id;
|
||||
info.receiving = true;
|
||||
receive_rate_samples_.emplace(info.file_id,
|
||||
ReceiveRateSample{std::chrono::steady_clock::now(), 0});
|
||||
found = notifications.emplace(progress.id, files.size()).first;
|
||||
files.push_back(std::move(info));
|
||||
}
|
||||
auto& info = files[found->second];
|
||||
info.file_name = progress.name;
|
||||
info.file_size = progress.total;
|
||||
info.sent_bytes = progress.received;
|
||||
switch (progress.status) {
|
||||
case file_transfer::ReceiveStatus::Receiving: info.status = Status::Receiving; break;
|
||||
case file_transfer::ReceiveStatus::Completed: info.status = Status::Completed; break;
|
||||
case file_transfer::ReceiveStatus::Failed: info.status = Status::Failed; break;
|
||||
case file_transfer::ReceiveStatus::Cancelled: info.status = Status::Cancelled; break;
|
||||
}
|
||||
if (info.status != Status::Receiving) {
|
||||
info.rate_bps = 0;
|
||||
receive_rate_samples_.erase(info.file_id);
|
||||
notifications.erase(found);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::Receive(const char* data, size_t size,
|
||||
PeerEventHandler* handler, const std::string& remote) {
|
||||
const auto props = owner_.FindRemoteSession(remote);
|
||||
PeerPtr* peer;
|
||||
std::shared_ptr<PeerEventHandler> events;
|
||||
{
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
peer = props ? props->peer_ : owner_.peer_;
|
||||
events = props ? props->peer_events_ : owner_.peer_events_;
|
||||
}
|
||||
if (!props && remote.rfind("lan/", 0) == 0) {
|
||||
events = owner_.ControllerEvents(remote);
|
||||
peer = events ? events->DirectPeer() : nullptr;
|
||||
}
|
||||
if (!peer || !events || events.get() != handler || (props && props->closing_)) return;
|
||||
std::shared_ptr<FileReceiver> receiver;
|
||||
std::filesystem::path default_directory;
|
||||
{
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
const auto closed = disconnected_.find(handler);
|
||||
if (stopping_ || (closed != disconnected_.end() &&
|
||||
(closed->second.count("") || closed->second.count(remote)))) return;
|
||||
auto& entry = receivers_[handler][remote];
|
||||
if (!entry.session) {
|
||||
entry.session = std::make_shared<FileReceiver>(); entry.handler = events;
|
||||
entry.default_directory = entry.session->OutputDir();
|
||||
// The transfer engine serializes progress callbacks for this receiver.
|
||||
entry.session->SetOnReceiveProgress(
|
||||
[this, remote, notifications = std::unordered_map<uint32_t, size_t>{}]
|
||||
(const file_transfer::ReceiveState& progress) mutable {
|
||||
UpdateReceiveProgress(progress, notifications, remote);
|
||||
});
|
||||
entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) {
|
||||
auto callback = events->EnterCallback();
|
||||
if (!callback || !events->DataAllowed()) return -1;
|
||||
const auto bytes = EncodeFileTransferAck(ack);
|
||||
const auto wire_id = events->WireId(remote);
|
||||
return SendReliableDataFrameToPeer(
|
||||
peer, bytes.data(), bytes.size(), kFileFeedbackStream,
|
||||
wire_id.data(), wire_id.size());
|
||||
});
|
||||
}
|
||||
receiver = entry.session;
|
||||
default_directory = entry.default_directory;
|
||||
}
|
||||
const auto configured = owner_.config_center_->GetFileTransferSavePath();
|
||||
receiver->SetOutputDir(configured.empty() ? default_directory : PathFromUtf8(configured));
|
||||
receiver->OnData(data, size);
|
||||
}
|
||||
|
||||
void FileTransferManager::Connected(PeerEventHandler* handler, const std::string& remote) {
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
if (remote.empty()) disconnected_.erase(handler);
|
||||
else if (const auto it = disconnected_.find(handler); it != disconnected_.end()) {
|
||||
it->second.erase(remote);
|
||||
if (it->second.empty()) disconnected_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::Cancel(PeerEventHandler* handler, const std::string& remote) {
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
disconnected_[handler].insert(remote);
|
||||
for (auto& [id, sender] : senders_)
|
||||
if (sender.handler.get() == handler && (remote.empty() || sender.remote == remote)) sender.session->Cancel();
|
||||
const auto it = receivers_.find(handler);
|
||||
if (it != receivers_.end()) {
|
||||
for (auto entry = it->second.begin(); entry != it->second.end();) {
|
||||
if (remote.empty() || entry->first == remote) {
|
||||
entry->second.session->Cancel(); entry = it->second.erase(entry);
|
||||
} else ++entry;
|
||||
}
|
||||
if (it->second.empty()) receivers_.erase(it);
|
||||
}
|
||||
wake_.notify_all();
|
||||
}
|
||||
|
||||
void FileTransferManager::Poll() {
|
||||
std::vector<std::shared_ptr<FileReceiver>> receivers;
|
||||
{
|
||||
std::lock_guard lock(sessions_mutex_);
|
||||
for (const auto& [handler, peers] : receivers_)
|
||||
for (const auto& [remote, receiver] : peers) receivers.push_back(receiver.session);
|
||||
}
|
||||
for (const auto& receiver : receivers) receiver->Poll();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(received_state_.file_transfer_list_mutex_);
|
||||
for (auto& info : received_state_.file_transfer_list_) {
|
||||
const auto sample = receive_rate_samples_.find(info.file_id);
|
||||
if (sample == receive_rate_samples_.end()) continue;
|
||||
const auto elapsed = now - sample->second.updated;
|
||||
if (elapsed < std::chrono::seconds(1)) continue;
|
||||
const double seconds = std::chrono::duration<double>(elapsed).count();
|
||||
const auto bytes = info.sent_bytes >= sample->second.bytes
|
||||
? info.sent_bytes - sample->second.bytes : 0;
|
||||
info.rate_bps = static_cast<uint32_t>((std::min)(bytes * 8.0 / seconds,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
sample->second = {now, info.sent_bytes};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,61 +1,83 @@
|
||||
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "runtime/remote_session.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
class PeerEventHandler;
|
||||
|
||||
class FileTransferManager {
|
||||
public:
|
||||
public:
|
||||
using FileTransferState = gui_detail::FileTransferState;
|
||||
using RemoteSession = gui_detail::RemoteSession;
|
||||
|
||||
explicit FileTransferManager(GuiRuntime &owner);
|
||||
explicit FileTransferManager(GuiRuntime& owner);
|
||||
~FileTransferManager();
|
||||
FileTransferState& global_state();
|
||||
FileTransferState& received_state();
|
||||
FileTransferState& state_for(const std::shared_ptr<RemoteSession>& props);
|
||||
// A reserved ID keeps a DeskHub notification stable while waiting for a slot.
|
||||
bool ProcessSelectedFile(const std::string& path, const std::shared_ptr<RemoteSession>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "",
|
||||
uint32_t file_id = 0);
|
||||
void HandleAck(const char* data, size_t size, PeerEventHandler* handler, const std::string& remote);
|
||||
void Receive(const char* data, size_t size, PeerEventHandler* handler, const std::string& remote);
|
||||
void Connected(PeerEventHandler* handler, const std::string& remote);
|
||||
void Cancel(PeerEventHandler* handler, const std::string& remote = "");
|
||||
bool CancelSend(uint32_t file_id);
|
||||
void Poll();
|
||||
void Shutdown();
|
||||
|
||||
FileTransferState &global_state();
|
||||
FileTransferState &
|
||||
state_for(const std::shared_ptr<RemoteSession> &props);
|
||||
|
||||
void
|
||||
ProcessSelectedFile(const std::string &path,
|
||||
const std::shared_ptr<RemoteSession> &props,
|
||||
const std::string &file_label,
|
||||
const std::string &remote_id = "");
|
||||
void HandleAck(const char *data, size_t size);
|
||||
|
||||
private:
|
||||
void Start(std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path,
|
||||
const std::string &file_label, const std::string &remote_id = "");
|
||||
private:
|
||||
struct Sender {
|
||||
std::shared_ptr<file_transfer::Session> session;
|
||||
std::shared_ptr<PeerEventHandler> handler;
|
||||
std::string remote;
|
||||
};
|
||||
struct Receiver {
|
||||
std::shared_ptr<FileReceiver> session;
|
||||
std::shared_ptr<PeerEventHandler> handler;
|
||||
std::filesystem::path default_directory;
|
||||
};
|
||||
void ProcessQueue(std::shared_ptr<RemoteSession> props);
|
||||
void Unregister(uint32_t file_id, bool per_peer);
|
||||
|
||||
std::mutex workers_mutex_;
|
||||
void UpdateProgress(FileTransferState& state,
|
||||
const file_transfer::SendState& progress,
|
||||
std::chrono::steady_clock::time_point now);
|
||||
void UpdateReceiveProgress(const file_transfer::ReceiveState& progress,
|
||||
std::unordered_map<uint32_t, size_t>& notifications,
|
||||
const std::string& remote_id);
|
||||
struct ReceiveRateSample {
|
||||
std::chrono::steady_clock::time_point updated;
|
||||
uint64_t bytes = 0;
|
||||
};
|
||||
std::mutex workers_mutex_, sessions_mutex_, wake_mutex_;
|
||||
std::condition_variable wake_;
|
||||
std::vector<std::future<void>> workers_;
|
||||
bool stopping_ = false;
|
||||
GuiRuntime &owner_;
|
||||
std::atomic<bool> stopping_{false};
|
||||
std::unordered_map<uint32_t, Sender> senders_;
|
||||
std::unordered_map<PeerEventHandler*, std::unordered_map<std::string, Receiver>> receivers_;
|
||||
std::unordered_map<PeerEventHandler*, std::set<std::string>> disconnected_;
|
||||
GuiRuntime& owner_;
|
||||
FileTransferState global_state_;
|
||||
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
|
||||
file_id_to_props_;
|
||||
std::shared_mutex file_id_to_props_mutex_;
|
||||
std::unordered_map<uint32_t, FileTransferState *> file_id_to_state_;
|
||||
std::shared_mutex file_id_to_state_mutex_;
|
||||
// Keep received files separate from outgoing queues.
|
||||
FileTransferState received_state_;
|
||||
// Guarded by received_state_.file_transfer_list_mutex_.
|
||||
std::unordered_map<uint32_t, ReceiveRateSample> receive_rate_samples_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-19
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _TRANSFER_NOTIFICATION_LAYOUT_H_
|
||||
#define _TRANSFER_NOTIFICATION_LAYOUT_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
|
||||
namespace crossdesk::transfer_notification {
|
||||
|
||||
constexpr float kWidth = 360;
|
||||
constexpr float kHeight = 132;
|
||||
constexpr float kMargin = 16;
|
||||
constexpr float kGap = 10;
|
||||
|
||||
struct Layout {
|
||||
float width, height;
|
||||
size_t rows, columns;
|
||||
size_t capacity() const { return rows * columns; }
|
||||
};
|
||||
|
||||
inline Layout Fit(float width, float height) {
|
||||
const float card_width = std::min(kWidth, std::max(1.0f, width - 2 * kMargin));
|
||||
const float card_height = std::min(kHeight, std::max(1.0f, height - 2 * kMargin));
|
||||
const auto count = [](float available, float card) {
|
||||
return static_cast<size_t>(std::max(1.0f, (available - 2 * kMargin + kGap) / (card + kGap)));
|
||||
};
|
||||
return {card_width, card_height, count(height, card_height), count(width, card_width)};
|
||||
}
|
||||
|
||||
struct Position { float x, y; };
|
||||
|
||||
inline Position Place(const Layout& layout, float x, float y,
|
||||
float width, float height, size_t index) {
|
||||
// Fill upward from the bottom-right, then continue in a column to the left.
|
||||
return {x + std::max(0.0f, width - kMargin - layout.width) -
|
||||
(index / layout.rows) * (layout.width + kGap),
|
||||
y + std::max(0.0f, height - kMargin - layout.height) -
|
||||
(index % layout.rows) * (layout.height + kGap)};
|
||||
}
|
||||
|
||||
} // namespace crossdesk::transfer_notification
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _INPUT_OUTCOME_H_
|
||||
#define _INPUT_OUTCOME_H_
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Applied means the native/service API accepted the operation. It does not
|
||||
// assert that the foreground application has consumed the injected event.
|
||||
enum class InputOutcome { applied, ignored, failed };
|
||||
|
||||
inline const char* InputOutcomeName(InputOutcome outcome) {
|
||||
switch (outcome) {
|
||||
case InputOutcome::applied:
|
||||
return "applied";
|
||||
case InputOutcome::ignored:
|
||||
return "ignored";
|
||||
case InputOutcome::failed:
|
||||
return "failed";
|
||||
}
|
||||
return "failed";
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -20,6 +21,8 @@
|
||||
#include "interactive_desktop.h"
|
||||
#include "service_host.h"
|
||||
#include "unattended_config.h"
|
||||
#include "platform/windows/input/windows_input_injector.h"
|
||||
#include "platform/windows/input/windows_text_input.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -249,14 +252,14 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
extended);
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||
InputOutcome KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
const int key_code = static_cast<int>(action.k.key_value);
|
||||
const bool is_down = action.k.flag == KeyFlag::key_down;
|
||||
const bool injected =
|
||||
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
||||
if (injected) pending_key_releases_.erase(key_code);
|
||||
if (injected) pending_key_releases_.Supersede(key_code);
|
||||
|
||||
auto& state = remote_states_[remote_id];
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
@@ -266,15 +269,68 @@ void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||
} else if (!is_down && injected) {
|
||||
state.pressed_keys.erase(key_code);
|
||||
}
|
||||
return injected ? InputOutcome::applied : InputOutcome::failed;
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
InputOutcome KeyboardController::ApplyRemoteText(const std::string& remote_id,
|
||||
const std::string& text) {
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
if (!owner_.IsAuthorizedController(remote_id)) return InputOutcome::ignored;
|
||||
if (!owner_.devices_.SupportsTextInput()) return InputOutcome::failed;
|
||||
#if _WIN32
|
||||
std::vector<INPUT> inputs;
|
||||
if (!BuildWindowsTextInputs(text, inputs)) return InputOutcome::failed;
|
||||
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable())
|
||||
owner_.privacy_.SuspendForDesktop();
|
||||
static const bool process_elevated = IsAdministratorProcess();
|
||||
const bool injected = DispatchDesktopInput(
|
||||
[&] {
|
||||
return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(),
|
||||
owner_.windows_consent_ui_.load(),
|
||||
owner_.local_service_available_.load(),
|
||||
process_elevated);
|
||||
},
|
||||
[&] {
|
||||
const UINT sent = SendWindowsTextInputs(
|
||||
inputs, [](INPUT* batch, UINT count) {
|
||||
return SendInputOnUserDesktop(batch, count);
|
||||
});
|
||||
const DWORD error = GetLastError();
|
||||
const bool blocked =
|
||||
error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE;
|
||||
// A text commit is a batch: only zero insertion permits a retry.
|
||||
return DesktopInputResult{
|
||||
sent == inputs.size(),
|
||||
sent == 0 && (IsDesktopTransitionInputError(error) || blocked),
|
||||
sent == 0 && blocked};
|
||||
},
|
||||
[&]() -> DesktopInputResult {
|
||||
const auto response = SendCrossDeskSecureDesktopTextInput(text, 1000);
|
||||
const auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_object() && json.value("ok", false)) return {true, false};
|
||||
const std::string error =
|
||||
json.is_object() ? json.value("error", "") : "invalid_response";
|
||||
// No text or raw reply in diagnostics; commits may be passwords.
|
||||
LOG_WARN("Secure desktop text injection failed: error={}", error);
|
||||
return {false, IsDesktopInputSetupPending(error)};
|
||||
});
|
||||
return injected ? InputOutcome::applied : InputOutcome::failed;
|
||||
#else
|
||||
if (!owner_.devices_.SendTextCommand(text)) {
|
||||
LOG_WARN("Failed to inject committed remote text");
|
||||
return InputOutcome::failed;
|
||||
}
|
||||
return InputOutcome::applied;
|
||||
#endif
|
||||
}
|
||||
|
||||
InputOutcome KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
auto& state = remote_states_[remote_id];
|
||||
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
||||
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
||||
return;
|
||||
return InputOutcome::ignored;
|
||||
}
|
||||
|
||||
state.last_seq = action.ks.seq;
|
||||
@@ -291,56 +347,60 @@ void KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
}
|
||||
const auto changes = ReconcileKeyboardState(state.pressed_keys, desired_keys);
|
||||
bool can_press = true;
|
||||
bool applied = true;
|
||||
for (const auto& key : changes) {
|
||||
if (key.is_down && !can_press) continue;
|
||||
if (!InjectRemoteKey(key.key_code, key.is_down, key.scan_code, key.extended)) {
|
||||
// Do not replay a shortcut with a stale/missing modifier. The next
|
||||
// heartbeat will retry from the state that was actually injected.
|
||||
can_press = false;
|
||||
applied = false;
|
||||
continue;
|
||||
}
|
||||
pending_key_releases_.erase(key.key_code);
|
||||
pending_key_releases_.Supersede(key.key_code);
|
||||
if (key.is_down)
|
||||
state.pressed_keys[key.key_code] =
|
||||
{key.key_code, key.scan_code, key.extended};
|
||||
else
|
||||
state.pressed_keys.erase(key.key_code);
|
||||
}
|
||||
return applied ? InputOutcome::applied : InputOutcome::failed;
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
bool KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
const char* reason) {
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
ReleaseRemotePressedKeysLocked(remote_id, reason);
|
||||
return ReleaseRemotePressedKeysLocked(remote_id, reason);
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseRemotePressedKeysLocked(
|
||||
bool KeyboardController::ReleaseRemotePressedKeysLocked(
|
||||
std::string remote_id, const char* reason) {
|
||||
const auto state_it = remote_states_.find(remote_id);
|
||||
if (state_it == remote_states_.end()) return;
|
||||
auto keys_to_release = std::move(state_it->second.pressed_keys);
|
||||
remote_states_.erase(state_it);
|
||||
|
||||
if (!keys_to_release.empty()) {
|
||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const auto& [code, key] : keys_to_release) {
|
||||
if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
pending_key_releases_[code] = key;
|
||||
} else {
|
||||
pending_key_releases_.erase(code);
|
||||
if (state_it != remote_states_.end()) {
|
||||
const auto& keys = state_it->second.pressed_keys;
|
||||
if (!keys.empty()) {
|
||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||
keys.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
// Stage before erasing: subsequent dispatcher retries must still see the
|
||||
// keys if injection fails, even though active/heartbeat state is gone.
|
||||
pending_key_releases_.Queue(remote_id, keys);
|
||||
remote_states_.erase(state_it);
|
||||
}
|
||||
return pending_key_releases_.ReleaseController(
|
||||
remote_id, [this](const KeyboardKey& key) {
|
||||
return InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
});
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) {
|
||||
{
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
while (!remote_states_.empty())
|
||||
ReleaseRemotePressedKeysLocked(remote_states_.begin()->first, reason);
|
||||
}
|
||||
CheckRemoteTimeouts();
|
||||
std::lock_guard input_lock(remote_input_mutex_);
|
||||
auto ids = pending_key_releases_.ControllerIds();
|
||||
for (const auto& [id, _] : remote_states_) ids.push_back(id);
|
||||
std::sort(ids.begin(), ids.end());
|
||||
ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
|
||||
// A failure remains pending; shutdown must never spin until it succeeds.
|
||||
for (const auto& id : ids) ReleaseRemotePressedKeysLocked(id, reason);
|
||||
}
|
||||
|
||||
void KeyboardController::CheckRemoteTimeouts() {
|
||||
@@ -350,7 +410,7 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
// Retain failed releases in ordinary mode as well as privacy mode. Retry
|
||||
// only on the ordinary desktop, never through the security UI helper.
|
||||
if (!pending_key_releases_.empty() &&
|
||||
if (!pending_key_releases_.Empty() &&
|
||||
(last_release_retry_tick_ == 0 ||
|
||||
now - last_release_retry_tick_ >= kReleaseRetryIntervalMs)
|
||||
#ifdef _WIN32
|
||||
@@ -358,15 +418,10 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
#endif
|
||||
) {
|
||||
last_release_retry_tick_ = now;
|
||||
for (auto it = pending_key_releases_.begin();
|
||||
it != pending_key_releases_.end();) {
|
||||
const auto& key = it->second;
|
||||
if (owner_.devices_.SendKeyboardCommand(
|
||||
key.key_code, false, key.scan_code, key.extended))
|
||||
it = pending_key_releases_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
pending_key_releases_.RetryAll([this](const KeyboardKey& key) {
|
||||
return owner_.devices_.SendKeyboardCommand(
|
||||
key.key_code, false, key.scan_code, key.extended);
|
||||
});
|
||||
}
|
||||
|
||||
// Keep the timeout decision and release under the same lock, so a fresh
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "keyboard_state.h"
|
||||
#include "input_outcome.h"
|
||||
#include "pending_key_releases.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -25,11 +27,12 @@ public:
|
||||
void ForceReleasePressedKeys();
|
||||
void RecoverCapturedState(const PressedKeys& pressed);
|
||||
void SendHeartbeat(bool force);
|
||||
void ApplyRemoteEvent(const std::string &remote_id,
|
||||
InputOutcome ApplyRemoteEvent(const std::string &remote_id,
|
||||
const RemoteAction &remote_action);
|
||||
void ApplyRemoteState(const std::string &remote_id,
|
||||
InputOutcome ApplyRemoteText(const std::string& remote_id, const std::string& text);
|
||||
InputOutcome ApplyRemoteState(const std::string &remote_id,
|
||||
const RemoteAction &remote_action);
|
||||
void ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
bool ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason);
|
||||
void ReleaseAllRemotePressedKeys(const char* reason);
|
||||
void CheckRemoteTimeouts();
|
||||
@@ -47,7 +50,7 @@ private:
|
||||
bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
// Caller holds remote_input_mutex_. Copy the id because this erases state.
|
||||
void ReleaseRemotePressedKeysLocked(std::string remote_id, const char* reason);
|
||||
bool ReleaseRemotePressedKeysLocked(std::string remote_id, const char* reason);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
PressedKeys pressed_keys_;
|
||||
@@ -58,7 +61,7 @@ private:
|
||||
// Guards all remote key state and serializes injection, cleanup and retries.
|
||||
std::mutex remote_input_mutex_;
|
||||
// Successful new input supersedes cleanup of an older press of that key.
|
||||
PressedKeys pending_key_releases_;
|
||||
PendingKeyReleases pending_key_releases_;
|
||||
uint32_t last_release_retry_tick_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PENDING_KEY_RELEASES_H_
|
||||
#define _PENDING_KEY_RELEASES_H_
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "keyboard_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Caller serializes access with KeyboardController's remote-input mutex.
|
||||
// Pending releases outlive the peer's active key/heartbeat state, but retain
|
||||
// their controller identity so cleanup cannot report success prematurely.
|
||||
class PendingKeyReleases {
|
||||
public:
|
||||
void Queue(const std::string& id, const PressedKeys& keys) {
|
||||
if (keys.empty()) return;
|
||||
auto& pending = peers_[id];
|
||||
for (const auto& [code, key] : keys) pending[code] = key;
|
||||
}
|
||||
|
||||
bool Empty() const { return peers_.empty(); }
|
||||
bool Contains(const std::string& id) const { return peers_.count(id) != 0; }
|
||||
|
||||
std::vector<std::string> ControllerIds() const {
|
||||
std::vector<std::string> ids;
|
||||
ids.reserve(peers_.size());
|
||||
for (const auto& [id, _] : peers_) ids.push_back(id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// A confirmed new event supersedes an older pending release of that key,
|
||||
// including one belonging to a disconnected controller.
|
||||
void Supersede(int key_code) {
|
||||
for (auto it = peers_.begin(); it != peers_.end();) {
|
||||
it->second.erase(key_code);
|
||||
if (it->second.empty())
|
||||
it = peers_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename SendUp>
|
||||
bool ReleaseController(const std::string& id, SendUp send_up) {
|
||||
auto peer = peers_.find(id);
|
||||
if (peer == peers_.end()) return true;
|
||||
auto& keys = peer->second;
|
||||
for (auto it = keys.begin(); it != keys.end();) {
|
||||
if (send_up(it->second))
|
||||
it = keys.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
if (!keys.empty()) return false;
|
||||
peers_.erase(peer);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename SendUp>
|
||||
void RetryAll(SendUp send_up) {
|
||||
// One bounded pass, even when every native release fails.
|
||||
for (const auto& id : ControllerIds()) ReleaseController(id, send_up);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, PressedKeys> peers_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "remote_input_dispatcher.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
RemoteInputDispatcher::RemoteInputDispatcher(Apply apply, Release release,
|
||||
Report report,
|
||||
ResetDisplay reset_display,
|
||||
ReleaseMouse release_mouse)
|
||||
: apply_(std::move(apply)),
|
||||
release_(std::move(release)),
|
||||
report_(std::move(report)),
|
||||
reset_display_(std::move(reset_display)),
|
||||
release_mouse_(std::move(release_mouse)),
|
||||
worker_([this] { Run(); }) {}
|
||||
|
||||
RemoteInputDispatcher::~RemoteInputDispatcher() { Stop(); }
|
||||
|
||||
bool RemoteInputDispatcher::Accepts(const RemoteAction& action) {
|
||||
return action.type == ControlType::mouse ||
|
||||
action.type == ControlType::keyboard ||
|
||||
action.type == ControlType::keyboard_state ||
|
||||
action.type == ControlType::text_input ||
|
||||
action.type == ControlType::display_id ||
|
||||
action.type == ControlType::service_command;
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::Connect(const std::string& id) {
|
||||
if (id.empty()) return;
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_ || !connected_.insert(id).second) return;
|
||||
logs_.erase(id); // A new session must get its first diagnostic.
|
||||
// In-flight resets finish before this session's input; queued old resets
|
||||
// must not change the new session's display. Failed releases are retained.
|
||||
RemoveResetsLocked();
|
||||
ready_.notify_one();
|
||||
}
|
||||
|
||||
RemoteInputDispatcher::Result RemoteInputDispatcher::Push(
|
||||
const std::string& id, const RemoteAction& action) {
|
||||
// HostInfo owns raw arrays; it must never enter this value-copy queue.
|
||||
if (!Accepts(action)) return Result::rejected;
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_ || !connected_.count(id)) return Result::rejected;
|
||||
if (IsMove(action) && !pending_.empty()) {
|
||||
auto& last = pending_.back();
|
||||
if (last.id == id && last.IsInput() && IsMove(*last.action)) {
|
||||
last.action = action;
|
||||
last.queued_at = Clock::now();
|
||||
return Result::coalesced;
|
||||
}
|
||||
}
|
||||
if (pending_counts_[id] >= kMaxPendingPerController) {
|
||||
QueueReleaseLocked(id);
|
||||
return Result::overflow;
|
||||
}
|
||||
pending_.push_back({id, action, Clock::now(), ItemKind::input});
|
||||
++pending_counts_[id];
|
||||
ready_.notify_one();
|
||||
return Result::queued;
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::Disconnect(const std::string& id) {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_ || !connected_.erase(id)) return;
|
||||
QueueReleaseLocked(id);
|
||||
if (connected_.empty() && reset_display_) {
|
||||
RemoveResetsLocked();
|
||||
pending_.push_back(
|
||||
{{}, std::nullopt, Clock::now(), ItemKind::reset_display});
|
||||
}
|
||||
ready_.notify_one();
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::WaitIdle() {
|
||||
std::unique_lock lock(mutex_);
|
||||
idle_.wait(lock, [this] { return pending_.empty() && !executing_; });
|
||||
}
|
||||
|
||||
bool RemoteInputDispatcher::HasPendingReleases() {
|
||||
std::lock_guard lock(mutex_);
|
||||
return !blocked_.empty();
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::Stop() {
|
||||
std::call_once(stop_once_, [this] {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
stopping_ = true;
|
||||
auto ids = std::move(connected_);
|
||||
connected_.clear();
|
||||
ids.insert(blocked_.begin(), blocked_.end());
|
||||
for (const auto& item : pending_)
|
||||
if (!item.IsReset()) ids.insert(item.id);
|
||||
pending_.clear();
|
||||
pending_counts_.clear();
|
||||
retries_.clear();
|
||||
for (const auto& id : ids) {
|
||||
pending_.push_back({id, std::nullopt, Clock::now()});
|
||||
pending_counts_[id] = 1;
|
||||
}
|
||||
}
|
||||
ready_.notify_one();
|
||||
worker_.join();
|
||||
});
|
||||
}
|
||||
|
||||
bool RemoteInputDispatcher::IsMove(const RemoteAction& action) {
|
||||
return action.type == ControlType::mouse && action.m.flag == MouseFlag::move;
|
||||
}
|
||||
|
||||
unsigned RemoteInputDispatcher::MouseUpMask(const RemoteAction& action) {
|
||||
if (action.type != ControlType::mouse) return 0;
|
||||
switch (action.m.flag) {
|
||||
case MouseFlag::left_up:
|
||||
return 1;
|
||||
case MouseFlag::right_up:
|
||||
return 2;
|
||||
case MouseFlag::middle_up:
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::RemoveResetsLocked() {
|
||||
pending_.erase(
|
||||
std::remove_if(pending_.begin(), pending_.end(),
|
||||
[](const Item& item) { return item.IsReset(); }),
|
||||
pending_.end());
|
||||
if (pending_.empty() && !executing_) idle_.notify_all();
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::QueueReleaseLocked(const std::string& id) {
|
||||
pending_.erase(
|
||||
std::remove_if(pending_.begin(), pending_.end(),
|
||||
[&](const Item& item) { return item.id == id; }),
|
||||
pending_.end());
|
||||
pending_counts_.erase(id);
|
||||
blocked_.insert(id);
|
||||
// Repeated overflow/disconnect must not defeat the retry backoff.
|
||||
if (auto retry = retries_.find(id); retry != retries_.end()) {
|
||||
retry->second.mouse_mask = 0; // Escalate a button repair on disconnect.
|
||||
} else {
|
||||
pending_.push_back({id, std::nullopt, Clock::now()});
|
||||
pending_counts_[id] = 1;
|
||||
}
|
||||
ready_.notify_one();
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::QueueMouseReleaseLocked(const std::string& id,
|
||||
unsigned mask) {
|
||||
// A concurrent disconnect/overflow already requested the broader cleanup.
|
||||
if (!blocked_.insert(id).second) return;
|
||||
// Keep subsequent clicks/keys queued, but don't apply them until the old
|
||||
// button is confirmed up. Never replay a timed-out down/click operation.
|
||||
pending_.push_front(
|
||||
{id, std::nullopt, Clock::now(), ItemKind::cleanup, mask});
|
||||
++pending_counts_[id];
|
||||
ready_.notify_one();
|
||||
}
|
||||
|
||||
std::optional<RemoteInputDispatcher::Item> RemoteInputDispatcher::NextLocked() {
|
||||
const auto now = Clock::now();
|
||||
auto next = retries_.end();
|
||||
for (auto it = retries_.begin(); it != retries_.end(); ++it)
|
||||
if (next == retries_.end() || it->second.due < next->second.due) next = it;
|
||||
auto it =
|
||||
std::find_if(pending_.begin(), pending_.end(), [&](const Item& item) {
|
||||
return !item.IsInput() || !blocked_.count(item.id);
|
||||
});
|
||||
// Alternate due retries with runnable queued work. Multiple slow cleanup
|
||||
// calls must not monopolize this worker; continuous input must not starve
|
||||
// recovery either. Input for the recovering controller remains blocked.
|
||||
if (next != retries_.end() && next->second.due <= now &&
|
||||
(it == pending_.end() || !last_was_retry_)) {
|
||||
last_was_retry_ = true;
|
||||
return Item{next->first, std::nullopt, next->second.due, ItemKind::cleanup,
|
||||
next->second.mouse_mask};
|
||||
}
|
||||
if (it == pending_.end()) return std::nullopt;
|
||||
last_was_retry_ = false;
|
||||
Item item = std::move(*it);
|
||||
pending_.erase(it);
|
||||
if (!item.IsReset() && --pending_counts_.at(item.id) == 0)
|
||||
pending_counts_.erase(item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
bool RemoteInputDispatcher::ShouldReportLocked(const Item& item,
|
||||
Clock::time_point now,
|
||||
int64_t queued_ms,
|
||||
int64_t execution_ms,
|
||||
InputOutcome outcome) {
|
||||
if (!report_) return false;
|
||||
auto& state = logs_[item.id];
|
||||
auto& last = outcome == InputOutcome::failed
|
||||
? state.failure[item.IsInput() ? 0 : 1]
|
||||
: state.slow;
|
||||
if (outcome != InputOutcome::failed && queued_ms < 50 && execution_ms < 50)
|
||||
return false;
|
||||
if (last != Clock::time_point{} && now - last < std::chrono::seconds(2))
|
||||
return false;
|
||||
last = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::RemoveQueuedCleanupLocked(const std::string& id) {
|
||||
// Cleanup queued during the running call covers the same old state. Merge
|
||||
// duplicates so they cannot bypass backoff or release a later session twice.
|
||||
const size_t before = pending_.size();
|
||||
pending_.erase(std::remove_if(pending_.begin(), pending_.end(),
|
||||
[&](const Item& queued) {
|
||||
return queued.id == id && queued.IsCleanup();
|
||||
}),
|
||||
pending_.end());
|
||||
const size_t removed = before - pending_.size();
|
||||
if (removed && (pending_counts_.at(id) -= removed) == 0)
|
||||
pending_counts_.erase(id);
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::ScheduleRetryLocked(const std::string& id,
|
||||
unsigned mouse_mask,
|
||||
Clock::time_point finished) {
|
||||
auto [retry, inserted] = retries_.try_emplace(id);
|
||||
retry->second.mouse_mask = mouse_mask;
|
||||
if (!inserted)
|
||||
retry->second.delay =
|
||||
std::min(retry->second.delay * 2, std::chrono::milliseconds(1000));
|
||||
retry->second.due = finished + retry->second.delay;
|
||||
blocked_.insert(id);
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::CompleteCleanupLocked(const Item& item,
|
||||
InputOutcome outcome,
|
||||
Clock::time_point finished) {
|
||||
if (stopping_) {
|
||||
retries_.erase(item.id);
|
||||
blocked_.erase(item.id);
|
||||
return;
|
||||
}
|
||||
// A successful button repair cannot discharge a concurrent request to
|
||||
// release all keyboard/mouse state after disconnect or queue overflow.
|
||||
const bool queued_full_cleanup =
|
||||
item.mouse_mask &&
|
||||
std::any_of(pending_.begin(), pending_.end(), [&](const Item& queued) {
|
||||
return queued.id == item.id && queued.IsCleanup() && !queued.mouse_mask;
|
||||
});
|
||||
if (queued_full_cleanup) {
|
||||
retries_.erase(item.id); // The queued full cleanup owns recovery now.
|
||||
return;
|
||||
}
|
||||
auto retry = retries_.find(item.id);
|
||||
if (item.mouse_mask && retry != retries_.end() && !retry->second.mouse_mask) {
|
||||
if (outcome == InputOutcome::failed)
|
||||
ScheduleRetryLocked(item.id, 0, finished);
|
||||
else
|
||||
retry->second.due = finished;
|
||||
return;
|
||||
}
|
||||
RemoveQueuedCleanupLocked(item.id);
|
||||
if (outcome == InputOutcome::failed) {
|
||||
ScheduleRetryLocked(item.id, item.mouse_mask, finished);
|
||||
} else {
|
||||
retries_.erase(item.id);
|
||||
blocked_.erase(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
InputOutcome RemoteInputDispatcher::Execute(const Item& item) {
|
||||
if (item.IsReset()) return reset_display_();
|
||||
if (item.IsInput()) return apply_(item.id, *item.action);
|
||||
if (item.mouse_mask && release_mouse_)
|
||||
return release_mouse_(item.id, item.mouse_mask);
|
||||
return release_(item.id);
|
||||
}
|
||||
|
||||
void RemoteInputDispatcher::Run() {
|
||||
std::unique_lock lock(mutex_);
|
||||
for (;;) {
|
||||
auto next = NextLocked();
|
||||
if (!next) {
|
||||
if (stopping_) return;
|
||||
auto due = Clock::time_point::max();
|
||||
for (const auto& [_, retry] : retries_) due = std::min(due, retry.due);
|
||||
if (due == Clock::time_point::max())
|
||||
ready_.wait(lock);
|
||||
else
|
||||
ready_.wait_until(lock, due);
|
||||
continue;
|
||||
}
|
||||
Item item = std::move(*next);
|
||||
executing_ = true;
|
||||
lock.unlock();
|
||||
const auto started = Clock::now();
|
||||
InputOutcome outcome = InputOutcome::failed;
|
||||
bool threw = false;
|
||||
try {
|
||||
outcome = Execute(item);
|
||||
} catch (...) {
|
||||
threw = true;
|
||||
}
|
||||
const auto finished = Clock::now();
|
||||
const auto queued_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(started -
|
||||
item.queued_at)
|
||||
.count();
|
||||
const auto execution_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(finished -
|
||||
started)
|
||||
.count();
|
||||
lock.lock();
|
||||
if (item.IsCleanup()) {
|
||||
CompleteCleanupLocked(item, outcome, finished);
|
||||
} else if (item.IsInput() && !stopping_) {
|
||||
if (threw && connected_.count(item.id)) {
|
||||
QueueReleaseLocked(item.id);
|
||||
} else if (outcome == InputOutcome::failed) {
|
||||
if (const unsigned mask = MouseUpMask(*item.action))
|
||||
QueueMouseReleaseLocked(item.id, mask);
|
||||
}
|
||||
}
|
||||
const bool report =
|
||||
ShouldReportLocked(item, finished, queued_ms, execution_ms, outcome);
|
||||
if (!connected_.count(item.id) && !blocked_.count(item.id) &&
|
||||
!pending_counts_.count(item.id))
|
||||
logs_.erase(item.id);
|
||||
lock.unlock();
|
||||
if (report) {
|
||||
try {
|
||||
report_(item.id, queued_ms, execution_ms, outcome);
|
||||
} catch (...) {
|
||||
} // Diagnostics must not terminate the input worker.
|
||||
}
|
||||
lock.lock();
|
||||
executing_ = false;
|
||||
if (pending_.empty()) idle_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _REMOTE_INPUT_DISPATCHER_H_
|
||||
#define _REMOTE_INPUT_DISPATCHER_H_
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "input_outcome.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Serializes native input, recovery and display resets off the RTC thread.
|
||||
class RemoteInputDispatcher {
|
||||
public:
|
||||
using Apply =
|
||||
std::function<InputOutcome(const std::string&, const RemoteAction&)>;
|
||||
using Release = std::function<InputOutcome(const std::string&)>;
|
||||
using Report =
|
||||
std::function<void(const std::string&, int64_t, int64_t, InputOutcome)>;
|
||||
using ResetDisplay = std::function<InputOutcome()>;
|
||||
// Optional targeted adapter; legacy callers fall back to full Release.
|
||||
using ReleaseMouse =
|
||||
std::function<InputOutcome(const std::string&, unsigned)>;
|
||||
enum class Result { queued, coalesced, rejected, overflow };
|
||||
static constexpr size_t kMaxPendingPerController = 128;
|
||||
|
||||
RemoteInputDispatcher(Apply apply, Release release, Report report = {},
|
||||
ResetDisplay reset_display = {},
|
||||
ReleaseMouse release_mouse = {});
|
||||
~RemoteInputDispatcher();
|
||||
static bool Accepts(const RemoteAction& action);
|
||||
void Connect(const std::string& id);
|
||||
Result Push(const std::string& id, const RemoteAction& action);
|
||||
void Disconnect(const std::string& id);
|
||||
|
||||
// Scheduled recovery is not idle work. Check HasPendingReleases() as well,
|
||||
// or Stop() before destroying the devices used by callbacks.
|
||||
void WaitIdle();
|
||||
bool HasPendingReleases();
|
||||
// Cancels pending input/timers and makes a final bounded cleanup attempt.
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
enum class ItemKind { input, cleanup, reset_display };
|
||||
struct Item {
|
||||
std::string id;
|
||||
std::optional<RemoteAction> action; // Present only for ordinary input.
|
||||
Clock::time_point queued_at;
|
||||
ItemKind kind = ItemKind::cleanup;
|
||||
// Zero requests full controller cleanup; otherwise only these mouse
|
||||
// buttons.
|
||||
unsigned mouse_mask = 0;
|
||||
bool IsReset() const { return kind == ItemKind::reset_display; }
|
||||
bool IsInput() const { return kind == ItemKind::input; }
|
||||
bool IsCleanup() const { return kind == ItemKind::cleanup; }
|
||||
};
|
||||
struct Retry {
|
||||
Clock::time_point due;
|
||||
std::chrono::milliseconds delay{100};
|
||||
unsigned mouse_mask = 0;
|
||||
};
|
||||
struct LogState {
|
||||
Clock::time_point slow{};
|
||||
std::array<Clock::time_point, 2> failure{};
|
||||
};
|
||||
|
||||
static bool IsMove(const RemoteAction& action);
|
||||
static unsigned MouseUpMask(const RemoteAction& action);
|
||||
void RemoveResetsLocked();
|
||||
void QueueReleaseLocked(const std::string& id);
|
||||
void QueueMouseReleaseLocked(const std::string& id, unsigned mask);
|
||||
void RemoveQueuedCleanupLocked(const std::string& id);
|
||||
void ScheduleRetryLocked(const std::string& id, unsigned mouse_mask,
|
||||
Clock::time_point finished);
|
||||
void CompleteCleanupLocked(const Item& item, InputOutcome outcome,
|
||||
Clock::time_point finished);
|
||||
std::optional<Item> NextLocked();
|
||||
InputOutcome Execute(const Item& item);
|
||||
bool ShouldReportLocked(const Item& item, Clock::time_point now,
|
||||
int64_t queued_ms, int64_t execution_ms,
|
||||
InputOutcome outcome);
|
||||
void Run();
|
||||
|
||||
Apply apply_;
|
||||
Release release_;
|
||||
Report report_;
|
||||
ResetDisplay reset_display_;
|
||||
ReleaseMouse release_mouse_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable ready_, idle_;
|
||||
std::deque<Item> pending_;
|
||||
std::unordered_set<std::string> connected_, blocked_;
|
||||
std::unordered_map<std::string, size_t> pending_counts_;
|
||||
std::unordered_map<std::string, Retry> retries_;
|
||||
std::unordered_map<std::string, LogState> logs_;
|
||||
bool last_was_retry_ = false;
|
||||
bool stopping_ = false;
|
||||
bool executing_ = false;
|
||||
std::once_flag stop_once_;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-22
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _REMOTE_MOUSE_BUTTONS_H_
|
||||
#define _REMOTE_MOUSE_BUTTONS_H_
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "input_outcome.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// One physical button cannot belong to two controllers at once. A second
|
||||
// controller's down/up must not steal or release an existing drag.
|
||||
class RemoteMouseButtons {
|
||||
public:
|
||||
template <typename Send>
|
||||
InputOutcome Apply(const std::string& id, MouseFlag flag, Send send) {
|
||||
if (id.empty()) return InputOutcome::ignored;
|
||||
std::lock_guard lock(mutex_);
|
||||
const int value = static_cast<int>(flag);
|
||||
if (value < 1 || value > 6) return send();
|
||||
auto& owner = owners_[(value - 1) / 2];
|
||||
const bool down = value % 2 == 1;
|
||||
if ((!owner.empty() && owner != id) || (!down && owner.empty()))
|
||||
return InputOutcome::ignored;
|
||||
// A service timeout may follow successful injection. Retain an attempted
|
||||
// down until an up or explicit cleanup is confirmed, including exceptions.
|
||||
if (down) owner = id;
|
||||
const auto outcome = send();
|
||||
if (!down && outcome == InputOutcome::applied) owner.clear();
|
||||
return outcome;
|
||||
}
|
||||
|
||||
template <typename Release>
|
||||
bool ReleaseController(const std::string& id, Release release,
|
||||
unsigned requested_mask = 7) {
|
||||
if (id.empty()) return true;
|
||||
std::lock_guard lock(mutex_);
|
||||
unsigned mask = 0;
|
||||
for (size_t i = 0; i < owners_.size(); ++i)
|
||||
if (owners_[i] == id && (requested_mask & (1u << i))) mask |= 1u << i;
|
||||
return ReleaseLocked(mask, release);
|
||||
}
|
||||
|
||||
template <typename Release>
|
||||
bool ReleaseAll(Release release) {
|
||||
std::lock_guard lock(mutex_);
|
||||
// Also clear any backend state retained after a desktop/route transition.
|
||||
return ReleaseLocked(7, release);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Release>
|
||||
bool ReleaseLocked(unsigned mask, Release release) {
|
||||
if (!mask) return true;
|
||||
// The sink returns the subset confirmed released, so one failed button
|
||||
// does not retain ownership of other successfully released buttons.
|
||||
const unsigned released = release(mask) & mask;
|
||||
for (size_t i = 0; i < owners_.size(); ++i)
|
||||
if (released & (1u << i)) owners_[i].clear();
|
||||
return released == mask;
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
std::array<std::string, 3> owners_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user