Compare commits

...
44 Commits
Author SHA1 Message Date
dijunkun 25fb6ee051 [fix] dismiss stale recent connection tooltips and update popup tests 2026-10-27 17:46:43 +08:00
dijunkun 20ede65038 [ui] unify floating logo hand cursors with arrow fallback 2026-10-27 17:07:03 +08:00
dijunkun bfb2e4a84e [fix] install Python build tools for iOS CI 2026-10-27 15:55:02 +08:00
dijunkun 99bb63f641 [fix] fix iOS native builds and expose CI diagnostics 2026-10-27 15:21:32 +08:00
dijunkun 4506c27e7c [fix] fix Windows privacy guard and iOS host tool selection 2026-10-27 13:00:34 +08:00
dijunkun b07f457e76 [feat] add system language support across desktop and mobile 2026-10-27 12:35:47 +08:00
dijunkun b4acf0b597 [build] align mobile Xmake repository selection with desktop 2026-10-27 12:35:11 +08:00
dijunkun 9e7848b9dd [ui] remove trailing colons from settings labels in all languages 2026-10-27 10:36:08 +08:00
dijunkun ea3a2e1faa [feat] add privacy unlock controls and remote relocking 2026-10-27 10:22:25 +08:00
dijunkun d1e4a31efb [ui] unify mobile privacy screen icons by state 2026-10-27 04:30:01 +08:00
dijunkun a6658452c2 [ui] remove network summaries from mobile control headers 2026-10-27 04:23:20 +08:00
dijunkun e123d5f2cc [feat] add mobile codec status and scrolling codec names 2026-10-27 04:16:41 +08:00
dijunkun 1687816bde [fix] show shortcut controls only for Windows hosts 2026-10-27 04:01:39 +08:00
dijunkun a9e3178042 [fix] make mobile landscape locking opt-in 2026-10-27 03:55:17 +08:00
dijunkun 0e052e6d7a [fix] hide privacy controls on locked desktops across clients 2026-10-27 03:46:19 +08:00
dijunkun 922cd41f70 [docs] clarify Windows service updates for development builds 2026-10-27 03:07:55 +08:00
dijunkun b15925f857 [feat] add mobile privacy controls and Windows secure text input 2026-10-27 02:37:23 +08:00
dijunkun 1e25ec15e1 [fix] support hotfix versions in Android signing 2026-10-26 17:50:11 +08:00
dijunkun 9f7411e5c0 [fix] update MiniRTC for WebSocket++ C++20 compatibility 2026-10-26 16:38:15 +08:00
dijunkun 8fd7ca1e8d [fix] correct Windows LAN identity file ownership 2026-10-26 15:52:21 +08:00
dijunkun c3ea4eb8fc [ui] flatten desktop surfaces and controls 2026-10-26 14:53:09 +08:00
dijunkun 38d0ce7728 [build] remove license validation from mobile builds 2026-10-26 13:12:11 +08:00
dijunkun 778c3575cd [ui] refine desktop controls and align localized copy 2026-10-26 13:00:03 +08:00
dijunkun dad8384666 [docs] add README banner and clarify license metadata 2026-10-26 11:36:36 +08:00
dijunkun 9496bc8f1c [docs] update README screenshots for refreshed desktop UI 2026-10-26 11:23:02 +08:00
dijunkun e551e04e8f [ui] refine desktop shadows and control contrast 2026-10-26 11:18:41 +08:00
dijunkun e3b3f78b06 [fix] invalidate stale Slint build artifacts after UI changes 2026-10-26 10:29:12 +08:00
dijunkun 5db213d126 [ui] replace connection mode button with a sliding toggle 2026-10-26 10:16:17 +08:00
dijunkun bdd04603c7 [feat] enable screen lock on disconnect by default 2026-10-25 03:07:13 +08:00
dijunkun f3d8689b55 [fix] allow alphanumeric password input on iOS, fixes #127 2026-10-25 03:01:45 +08:00
dijunkun 0fb4b031c7 [fix] adapt recent connection thumbnails to display DPI 2026-10-25 02:57:33 +08:00
dijunkun 7fc21f01bc [ui] add a compact edit button for self-hosted settings 2026-10-25 02:43:24 +08:00
dijunkun 86bae6e1f4 [ui] remove TURN service toggle and simplify relay settings 2026-10-25 02:40:21 +08:00
dijunkun c03c8a63d4 [fix] avoid Windows interface macro collision 2026-10-25 02:32:42 +08:00
dijunkun ec9ac6a7cd [chore] simplify issue templates and reduce required fields 2026-10-25 01:51:48 +08:00
dijunkun 2a0b14035d [fix] preserve Unicode device names, fixes #126 2026-10-25 01:47:10 +08:00
dijunkun 652378afa8 [fix] add missing stream header and update libnice recipe checksum 2026-10-25 01:39:49 +08:00
dijunkun 0cbdbb0c96 [feat] add lock screen on disconnect option, refs #125 2026-10-25 01:35:40 +08:00
dijunkun 63bc5648c0 [chore] update MiniRTC for IP direct API grouping 2026-10-25 01:24:39 +08:00
dijunkun 859aaa29d4 [docs] document ID and direct IP connection architecture 2026-10-25 01:19:04 +08:00
dijunkun cc48eb651f [docs] document IP direct mode and refresh desktop screenshots 2026-10-25 01:05:43 +08:00
dijunkun ae6f81e83a [feat] integrate IP direct mode with desktop controls and history 2026-10-25 00:54:13 +08:00
dijunkun 2445f79f92 [feat] add authenticated LAN transport and per-IP listeners 2026-10-25 00:54:10 +08:00
dijunkun bd7f9505d9 [docs] document update public key build option 2026-10-24 21:03:55 +08:00
248 changed files with 29721 additions and 3465 deletions
+12 -104
View File
@@ -7,124 +7,32 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
请先查看 [常见问题 / FAQ](https://github.com/kunkundi/crossdesk/blob/deskhub/docs/FAQ.md) 并搜索 [已有问题 / Existing issues](https://github.com/kunkundi/crossdesk/issues?q=is%3Aissue)。可使用中文或英文填写。 只需描述问题,其余选填。中文或英文均可。
Please check the FAQ and existing issues first. You can write in Chinese or English. Only a problem description is required. Chinese or English is welcome.
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:
label: 问题与预期 / Problem and expected behavior label: 问题描述 / Problem description
description: 描述实际发生的情况、错误提示,以及期望的行为。Describe what happened, any error messages, and what you expected. description: 遇到了什么问题?如方便,也请说明触发操作或错误提示。What went wrong? If possible, include what you were doing or any error message.
placeholder: |
实际结果 / Actual behavior:
预期结果 / Expected behavior:
validations: validations:
required: true required: true
- type: textarea - type: input
id: reproduction id: environment
attributes: attributes:
label: 复现步骤 / Steps to reproduce label: 使用环境(选填) / Environment (optional)
description: 说明操作步骤和出现频率;偶发问题可描述发生前的操作。Include the steps and frequency. For intermittent issues, describe what you were doing. description: 操作系统和 CrossDesk 版本;连接问题可填写两端信息。OS and CrossDesk version; include both devices for connection issues.
placeholder: | placeholder: Windows 11 → macOS 15; CrossDesk x.y.z
1.
2.
3.
出现频率 / Frequency:
validations:
required: true
- type: dropdown
id: clients
attributes:
label: 涉及客户端 / Affected clients
description: 可多选;Web 与原生移动端请分别选择。Select all that apply; distinguish Web from native mobile apps.
multiple: true
options:
- 桌面客户端(Windows / macOS / Linux) / Desktop client
- iOS / iPadOS(原生 / Native)
- Android(原生 / Native)
- 鸿蒙 PC / HarmonyOS PC
- 鸿蒙手机或平板 / HarmonyOS mobile
- Web 客户端 / Web client
- 服务端 / Server
- 其他 / Other
validations:
required: true
- type: textarea
id: controller
attributes:
label: 控制端或本机环境 / Controller or local environment
description: |
控制端是发起远程操作的设备。请填写完整系统和客户端版本;Web 用户另填浏览器及其版本。自行构建请附提交号。
仅涉及安装、启动或服务端时,在此填写发生问题的本机环境。
The controller initiates remote access. Include full OS and client versions; Web users should also include the browser and its version. For source builds, include the commit.
For installation, startup, or server-only issues, describe the affected local environment here.
placeholder: |
操作系统及版本 / OS and version:
CPU 架构(x64 / arm64 等) / CPU architecture:
CrossDesk 版本或提交号 / CrossDesk version or commit:
浏览器及版本(仅 Web) / Browser and version (Web only):
validations:
required: true
- type: textarea
id: host
attributes:
label: 被控端环境 / Remote host environment
description: |
被控端是接受远程操作的电脑。连接或会话问题请补充这一端的信息;不涉及远程设备时可留空。
The host is the computer being controlled. Include its details for connection or session issues; leave empty if no remote device is involved.
placeholder: |
操作系统及版本 / OS and version:
CPU 架构(x64 / arm64 等) / CPU architecture:
CrossDesk 版本或提交号 / CrossDesk version or commit:
validations:
required: false
- type: dropdown
id: service
attributes:
label: 服务部署方式 / Server deployment
options:
- 公共服务 / Public service
- 自托管 / Self-hosted
- 不确定 / Unknown
- 不适用 / Not applicable
validations:
required: false
- type: dropdown
id: connection
attributes:
label: 连接方式 / Connection type
description: 可在会话的网络状态中查看;无法确认时选择“不确定”。Check the session's network status, or choose Unknown.
options:
- P2P 直连 / Direct P2P
- TURN 中继 / TURN relay
- 尚未建立连接 / Connection not established
- 不确定 / Unknown
- 不适用 / Not applicable
validations:
required: false
- type: textarea
id: context
attributes:
label: 补充环境 / Additional environment
description: |
按问题补充相关信息,例如普通会话或桌面墙、自托管服务端版本、局域网或公网、VPN;画面问题可补充显卡、编码格式、硬件编解码设置和显示器情况。
Include relevant details such as a normal session or DeskHub, self-hosted server version, LAN or Internet, and VPN use. For video issues, add GPU, codec, hardware codec settings, and display configuration.
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: attachments id: attachments
attributes: attributes:
label: 日志与截图 / Logs and screenshots label: 截图或日志(选填) / Screenshots or logs (optional)
description: | description: |
可粘贴相关日志,或拖入日志文件、截图和录屏。上传前请隐去连接密码等敏感信息。 可直接拖入截图、录屏或日志文件,请隐去密码等敏感信息。
Paste relevant logs or attach log files, screenshots, and recordings. Redact connection passwords and other private information before uploading. Drag in screenshots, recordings, or logs. Hide passwords and other sensitive information.
validations: validations:
required: false required: false
+6 -51
View File
@@ -7,66 +7,21 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
提交前请搜索 [已有建议 / Existing requests](https://github.com/kunkundi/crossdesk/issues?q=is%3Aissue)。可使用中文或英文填写。 只需描述你的建议,其余选填。中文或英文均可。
Please search existing requests before submitting. You can write in Chinese or English. Only a description of your idea is required. Chinese or English is welcome.
- type: textarea
id: use-case
attributes:
label: 使用场景与问题 / Use case and problem
description: 你希望完成什么?目前遇到什么限制?What are you trying to do, and what limitation do you encounter?
validations:
required: true
- type: textarea - type: textarea
id: proposal id: proposal
attributes: attributes:
label: 功能建议与预期效果 / Proposal and expected outcome label: 你的建议 / Your idea
description: 描述希望新增或改进的行为,以及理想的使用方式。Describe the behavior you would like to add or improve and how you expect to use it. description: 希望新增或改进什么?如方便,也请说明使用场景。What would you like to add or improve? If possible, describe when you would use it.
validations: validations:
required: true required: true
- type: dropdown
id: scope
attributes:
label: 适用范围 / Scope
description: 可多选;通用建议可选择“跨平台或通用”。Select all that apply, or choose Cross-platform or general.
multiple: true
options:
- 跨平台或通用 / Cross-platform or general
- 桌面客户端(Windows / macOS / Linux) / Desktop client
- iOS / iPadOS
- Android
- 鸿蒙 PC / HarmonyOS PC
- 鸿蒙手机或平板 / HarmonyOS mobile
- Web 客户端 / Web client
- 桌面墙 / DeskHub
- 自托管服务 / Self-hosted services
- 文档或安装体验 / Documentation or installation
- 其他 / Other
validations:
required: false
- type: textarea
id: impact
attributes:
label: 影响范围与使用频率 / Impact and frequency
description: 哪些用户或场景会受益?你会多频繁地使用?Who would benefit, in which situations, and how often would you use this?
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: 替代方案 / Alternatives
description: 目前如何处理这个需求?是否考虑过其他实现方式?How do you handle this today, and what other approaches have you considered?
validations:
required: false
- type: textarea - type: textarea
id: references id: references
attributes: attributes:
label: 参考资料与补充说明 / References and additional context label: 补充说明(选填) / Additional context (optional)
description: 可提供参考链接、截图、示意图或其他要求。Include links, screenshots, mockups, or any additional requirements. description: 可附截图、参考链接或其他说明。Add screenshots, links, or any other details.
validations: validations:
required: false required: false
+2 -12
View File
@@ -36,7 +36,6 @@ on:
- "apps/android/**" - "apps/android/**"
- "apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json" - "apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json"
- "apps/ios/licenses/**" - "apps/ios/licenses/**"
- "apps/ios/scripts/generate_third_party_licenses.py"
- "deps/submodules/minirtc" - "deps/submodules/minirtc"
- "libs/wire/**" - "libs/wire/**"
@@ -135,14 +134,8 @@ jobs:
actions-cache-folder: .xmake-toolchain-cache actions-cache-folder: .xmake-toolchain-cache
actions-cache-key: ${{ runner.os }}-${{ runner.arch }}-ubuntu24.04-android-xmake${{ env.XMAKE_MIN_VERSION }} actions-cache-key: ${{ runner.os }}-${{ runner.arch }}-ubuntu24.04-android-xmake${{ env.XMAKE_MIN_VERSION }}
- name: Cache Xmake package repository # Repository selection follows desktop Xmake behavior. Keep installed
uses: actions/cache@v5 # packages isolated; never reuse another platform's build/config.
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.
- name: Cache native dependencies - name: Cache native dependencies
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
@@ -155,9 +148,6 @@ jobs:
cache-provider: basic cache-provider: basic
cache-read-only: ${{ github.event_name == 'pull_request' }} 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 - name: Resolve Android version
shell: python shell: python
env: env:
+50 -13
View File
@@ -53,7 +53,6 @@ jobs:
EXPECTED_IOS_SDK: "26.5" EXPECTED_IOS_SDK: "26.5"
# Exercise the minimum supported version; local builds may use newer Xmake. # Exercise the minimum supported version; local builds may use newer Xmake.
XMAKE_MIN_VERSION: "3.1.1" XMAKE_MIN_VERSION: "3.1.1"
XMAKE_REPO_REVISION: "121109352da272cd1a3d4ed52f2dcda34f839955"
steps: steps:
- name: Extract version number - name: Extract version number
@@ -160,17 +159,21 @@ jobs:
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
path: apps/ios/.xmake/packages 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: | 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 - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.13" python-version: "3.13"
- name: Verify iOS license catalog - name: Install Python build tools
run: python3 -B apps/ios/scripts/generate_third_party_licenses.py 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 - name: Install xmake
uses: xmake-io/github-action-setup-xmake@v1 uses: xmake-io/github-action-setup-xmake@v1
@@ -181,19 +184,40 @@ jobs:
shell: bash shell: bash
run: | run: |
xmake --version | tee "${RUNNER_TEMP}/xmake-version.txt" 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" --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 shell: bash
env:
CONFIGURATION: Release
PLATFORM_NAME: iphoneos
CURRENT_ARCH: arm64
CROSSDESK_NATIVE_VERBOSE: "1"
run: | run: |
xmake repo -u set -o pipefail
XMAKE_REPO_DIR="${HOME}/.xmake/repositories/xmake-repo" (
git -C "${XMAKE_REPO_DIR}" fetch --depth=1 origin "${XMAKE_REPO_REVISION}" trap 'kill "${SLEEP_PID:-}" 2>/dev/null || true; exit 0' TERM INT
git -C "${XMAKE_REPO_DIR}" checkout --detach "${XMAKE_REPO_REVISION}" while true; do
test "$(git -C "${XMAKE_REPO_DIR}" rev-parse HEAD)" = "${XMAKE_REPO_REVISION}" 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 - name: Build CrossDesk Mobile
timeout-minutes: 30
shell: bash shell: bash
run: | run: |
DERIVED_DATA="${RUNNER_TEMP}/CrossDeskMobileDerivedData" DERIVED_DATA="${RUNNER_TEMP}/CrossDeskMobileDerivedData"
@@ -209,7 +233,20 @@ jobs:
COMPILER_INDEX_STORE_ENABLE=NO \ COMPILER_INDEX_STORE_ENABLE=NO \
MARKETING_VERSION="${IOS_VERSION_BASE}" \ MARKETING_VERSION="${IOS_VERSION_BASE}" \
IPHONEOS_DEPLOYMENT_TARGET="${IOS_DEPLOYMENT_TARGET}" \ 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 - name: Verify CrossDesk Mobile
shell: bash shell: bash
+5 -5
View File
@@ -4,7 +4,7 @@ on:
workflow_call: workflow_call:
inputs: inputs:
version: 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 required: true
type: string type: string
secrets: secrets:
@@ -19,7 +19,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
source_tag: 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 required: true
type: string type: string
@@ -61,11 +61,11 @@ jobs:
if os.environ["GITHUB_EVENT_NAME"] == "workflow_dispatch" and not version: if os.environ["GITHUB_EVENT_NAME"] == "workflow_dispatch" and not version:
tag = os.environ["SOURCE_TAG"] tag = os.environ["SOURCE_TAG"]
if not tag.startswith("android-v"): 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-") 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: 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") datetime.datetime.strptime(match[1], "%Y%m%d")
with open(os.environ["GITHUB_OUTPUT"], "a") as stream: with open(os.environ["GITHUB_OUTPUT"], "a") as stream:
stream.write(f"version={version}\n") stream.write(f"version={version}\n")
+3
View File
@@ -1,3 +1,6 @@
CrossDesk
SPDX-License-Identifier: GPL-3.0-only
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007
+31 -8
View File
@@ -1,6 +1,5 @@
<div align="center"> <div align="center">
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk 标志" /> <img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk 标志:图标在左,双色文字在右" />
<h1>CrossDesk</h1>
<p>轻量、跨平台的远程桌面,支持电脑、浏览器和移动端连接,用桌面墙集中查看与操控多台设备。</p> <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> <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>
@@ -22,13 +21,14 @@
CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过 **桌面墙(DeskHub)** 集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。 CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过 **桌面墙(DeskHub)** 集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。
[主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor) [主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [IP 直连](#lan-direct) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor)
<a id="features"></a> <a id="features"></a>
## 主要功能 ## 主要功能
- **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。 - **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。
- **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。
- **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。 - **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。
- **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。 - **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。
- **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。 - **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。
@@ -61,22 +61,42 @@ CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过
## 快速连接 ## 快速连接
1. **准备被控电脑。** 安装并运行 CrossDesk,等待显示“已连接服务器”,查看“本桌面”中的本机 ID 和密码。 ### 通过设备 ID 连接
1. **准备被控电脑。** 安装并运行 CrossDesk,切换至 ID 连接模式,等待显示“已连接服务器”,查看“本桌面”中的本机 ID 和密码。
2. **发起连接。** 在另一台电脑的 CrossDesk 中输入被控电脑的 9 位 ID,点击“连接”;也可使用原生移动端或 [Web 客户端](https://web.crossdesk.cn/)。 2. **发起连接。** 在另一台电脑的 CrossDesk 中输入被控电脑的 9 位 ID,点击“连接”;也可使用原生移动端或 [Web 客户端](https://web.crossdesk.cn/)。
3. **验证密码。** 输入被控电脑当前的连接密码,等待远程画面出现。桌面端可勾选“记住密码”,后续从“近期连接”重连。 3. **验证密码。** 输入被控电脑当前的连接密码,等待远程画面出现。桌面端可勾选“记住密码”,后续从“近期连接”重连。
使用自建服务器时,两端需要接入同一服务。更多连接、密码及浏览器操作见 [使用指南](docs/USER_GUIDE.md#quick-start);连接失败、黑屏或无法操作键鼠时先查阅 [常见问题](docs/FAQ.md)。 使用自建服务器时,两端需要接入同一服务。更多连接、密码及浏览器操作见 [使用指南](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> <a id="preview"></a>
## 界面预览 ## 界面预览
### 桌面客户端 ### 桌面客户端
本机凭据、远程连接和近期设备集中在主窗口,左上角进入桌面墙。 本机凭据、远程连接和近期设备集中在主窗口,左上角提供连接模式切换和桌面墙入口。
<p align="center"> <p align="center">
<img src="docs/images/desktop-main-zh.png" width="800" alt="CrossDesk 主窗口:本机凭据、远程连接入口和近期设备;数据为演示" /> <img src="docs/images/desktop-main-zh.png" width="800" alt="CrossDesk ID 连接模式:模式切换、本机 ID、远程连接入口和近期设备;数据为演示" />
</p>
### IP 直连模式
切换本机 IP、修改端口与密码,通过对方地址连接;下方展示独立的局域网近期设备。
<p align="center">
<img src="docs/images/desktop-lan-zh.png" width="800" alt="CrossDesk IP 直连模式:本机 IP 和端口编辑入口、六位密码、远端地址输入和局域网近期设备;数据为演示" />
</p> </p>
<a id="deskhub"></a> <a id="deskhub"></a>
@@ -96,7 +116,7 @@ CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过
## 会话操作与设置 ## 会话操作与设置
远程会话支持显示器切换、画质与帧率调整、声音、文本剪贴板同步和文件传输。工具栏还可查看网络状态、控制隐私屏;桌面设置中可配置编码、中继、自托管与桌面墙画面预算。 远程会话支持显示器切换、画质与帧率调整、声音、文本剪贴板同步和文件传输。工具栏还可查看网络状态、控制隐私屏;桌面设置中可配置 IP 直连开关、编码、中继、自托管与桌面墙画面预算。
- [会话操作](docs/USER_GUIDE.md#session):工具栏、文本剪贴板、文件保存和隐私屏。 - [会话操作](docs/USER_GUIDE.md#session):工具栏、文本剪贴板、文件保存和隐私屏。
- [客户端设置与更新](docs/USER_GUIDE.md#settings):常规设置、桌面墙传输及版本检查。 - [客户端设置与更新](docs/USER_GUIDE.md#settings):常规设置、桌面墙传输及版本检查。
@@ -118,7 +138,7 @@ CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过
### Android ### Android
原生控制端支持触控与外接键鼠、多屏切换、文件收发、文本剪贴板和会话画面设置。H.264 / AV1 优先使用硬件解码,不支持或运行失败时回退到软件解码;切换应用时优先通过画中画保持会话,画中画不可用时尝试前台服务保活。当前不提供手机被控或隐私屏功能。 原生控制端支持触控与外接键鼠、多屏切换、文件收发、文本剪贴板和会话画面设置。H.264 / AV1 优先使用硬件解码,不支持或运行失败时回退到软件解码;切换应用时优先通过画中画保持会话,画中画不可用时尝试前台服务保活。当前不提供手机被控功能。悬浮菜单中的“隐私屏”按钮可直接开启或关闭电脑本地遮罩,按钮按电脑确认的状态显示“隐私屏已开启”或“隐私屏已关闭”,开启与关闭使用相同配色,正常切换不显示额外文字,仅在异常时显示提示;开启时与桌面端一致屏蔽电脑本地键鼠。
[使用说明](docs/USER_GUIDE.md#android) · [源码构建](apps/android/README.md) [使用说明](docs/USER_GUIDE.md#android) · [源码构建](apps/android/README.md)
@@ -151,9 +171,12 @@ PC 端可控制和被控制,复用桌面界面与桌面墙;手机和平板
- [源码构建](docs/BUILD.md):桌面端依赖、编译参数与打包;移动端请使用上方各平台的构建指南。 - [源码构建](docs/BUILD.md):桌面端依赖、编译参数与打包;移动端请使用上方各平台的构建指南。
- [GUI 架构](docs/gui-architecture.md):桌面界面、平台实现与工程结构。 - [GUI 架构](docs/gui-architecture.md):桌面界面、平台实现与工程结构。
- [连接模式架构](docs/connection-architecture.md):ID 连接与 IP 直连的寻址、认证、协商、数据路径及生命周期。
- [常见问题](docs/FAQ.md):连接失败、黑屏、系统权限、文件位置与构建问题。 - [常见问题](docs/FAQ.md):连接失败、黑屏、系统权限、文件位置与构建问题。
- [提交问题或建议](https://github.com/kunkundi/crossdesk/issues):请提供两端系统、客户端版本、连接方式和可复现步骤。 - [提交问题或建议](https://github.com/kunkundi/crossdesk/issues):请提供两端系统、客户端版本、连接方式和可复现步骤。
桌面端的 `CROSSDESK_UPDATE_PUBLIC_KEY` 是可选编译参数,用于验证应用内升级清单的 Ed25519 签名。不配置仍可正常编译、运行和检查版本,但更新只能手动下载;需要应用内自动安装更新时,须配置与发布私钥匹配的公钥。配置方式见 [更新公钥](docs/BUILD.md#update-public-key),密钥生成与发布流程见 [桌面端应用内升级](docs/UPDATER.md)。
<a id="sponsor"></a> <a id="sponsor"></a>
## 支持 CrossDesk ## 支持 CrossDesk
+31 -8
View File
@@ -1,6 +1,5 @@
<div align="center"> <div align="center">
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk logo" /> <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" />
<h1>CrossDesk</h1>
<p>A lightweight, cross-platform remote desktop for computers, browsers, and mobile devices, with DeskHub for viewing and controlling multiple desktops.</p> <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> <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>
@@ -22,13 +21,14 @@
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. 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.
[Features](#features) · [Download and platforms](#download) · [Quick start](#quick-start) · [Preview](#preview) · [User guide](docs/USER_GUIDE_EN.md) · [Self-hosting](#self-hosting) · [Build and feedback](#development) · [Sponsor](#sponsor) [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)
<a id="features"></a> <a id="features"></a>
## Features ## 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. - **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. - **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. - **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. - **Everyday controls:** monitor switching, keyboard and mouse input, shortcuts, text clipboard, and file transfer.
@@ -61,22 +61,42 @@ Requirements follow the current source and build configuration. Check the releas
## Quick start ## Quick start
1. **Prepare the host.** Install and run CrossDesk on the computer to control. Wait for the server-connected status, then find its ID and password under **Local Desktop**. ### 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. 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**. 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. 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> <a id="preview"></a>
## Preview ## Preview
### Desktop client ### Desktop client
Local credentials, remote connections, and recent devices share the main window. Open DeskHub from the top-left corner. Local credentials, remote connections, and recent devices share the main window. The top-left controls switch connection modes and open DeskHub.
<p align="center"> <p align="center">
<img src="docs/images/desktop-main-en.png" width="800" alt="CrossDesk main window with local credentials, remote connection controls, and recent devices; demonstration data" /> <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>
### Direct IP mode
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-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> </p>
<a id="deskhub"></a> <a id="deskhub"></a>
@@ -96,7 +116,7 @@ Group control sends input to all connected devices; closing DeskHub disconnects
## Session controls and settings ## Session controls and settings
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 codecs, relay, self-hosting, and the DeskHub video budget. 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.
- [Session controls](docs/USER_GUIDE_EN.md#session): toolbar, text clipboard, received files, and privacy screens. - [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. - [Client settings and updates](docs/USER_GUIDE_EN.md#settings): general options, DeskHub video settings, and version checks.
@@ -118,7 +138,7 @@ The native controller supports touch and a virtual mouse, multiple monitors, rem
### Android ### 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 and privacy screens are not currently supported. 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) [User guide](docs/USER_GUIDE_EN.md#android) · [Build from source](apps/android/README.md) (Chinese)
@@ -151,9 +171,12 @@ Host your own signaling and TURN services, then enable them under **Settings →
- [Build from source](docs/BUILD_EN.md): desktop dependencies, build options, and packaging. Use the platform guides above for mobile builds. - [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. - [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. - [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. - [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> <a id="sponsor"></a>
## Support CrossDesk ## Support CrossDesk
+9 -6
View File
@@ -9,11 +9,12 @@
- 48 kHz 单声道 PCM 播放,支持静音,音频队列有容量限制。 - 48 kHz 单声道 PCM 播放,支持静音,音频队列有容量限制。
- 相对鼠标(触控板)和绝对位置模式;单指点击、绝对模式按住拖动、长按右键,以及外接键盘、鼠标。画面手势与 iOS 一致:双指可在整个触控区域(包括画面外黑边)捏合,以触点为中心缩放(1–10 倍),放大后双指拖动画面,原始比例下双指上下滑动控制滚轮。相对鼠标移到可见画面的上、下边缘后继续向外移动,会反向上下平移画面,不触发左右平移;未放大时和按住虚拟鼠标键拖动时同样生效,停止移动即停止平移,抬手保留位置。移动到极限时,画面可见部分至少占当前显示区域高度的 20%,与缩放倍数无关;整幅画面高度不足该比例时完整保留,范围计入上下黑边;缩放结束及缩回 1 倍时保留平移位置,仅在超过可见边界时限制位置;切换显示器恢复居中。 - 相对鼠标(触控板)和绝对位置模式;单指点击、绝对模式按住拖动、长按右键,以及外接键盘、鼠标。画面手势与 iOS 一致:双指可在整个触控区域(包括画面外黑边)捏合,以触点为中心缩放(1–10 倍),放大后双指拖动画面,原始比例下双指上下滑动控制滚轮。相对鼠标移到可见画面的上、下边缘后继续向外移动,会反向上下平移画面,不触发左右平移;未放大时和按住虚拟鼠标键拖动时同样生效,停止移动即停止平移,抬手保留位置。移动到极限时,画面可见部分至少占当前显示区域高度的 20%,与缩放倍数无关;整幅画面高度不足该比例时完整保留,范围计入上下黑边;缩放结束及缩回 1 倍时保留平移位置,仅在超过可见边界时限制位置;切换显示器恢复居中。
- 横竖屏等比例显示、远端多显示器切换。与 iOS 一致的画面质量(低/中/高)、采集帧率(30/60 fps)、画面偏好;支持能力检测、电脑端确认、失败/5 秒超时回退及连续操作的应答排序。 - 横竖屏等比例显示、远端多显示器切换。与 iOS 一致的画面质量(低/中/高)、采集帧率(30/60 fps)、画面偏好;支持能力检测、电脑端确认、失败/5 秒超时回退及连续操作的应答排序。
- “设置 → 屏幕方向 → 连接后自动横屏”默认开启并保存在本机。手机进入会话后先保持横屏,检测到手机实际横过来后才恢复跟随设备方向;竖持或平放不会立即切回竖屏。关闭后沿用自动旋转,平板与多窗口不受此选项影响。退出会话恢复首页方向,返回同一后台会话不重复锁定已解锁的方向。 - “设置 → 屏幕方向 → 连接建立后锁定横屏”默认关闭并保存在本机。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受此选项影响。退出会话恢复首页方向,返回后台会话沿用该会话的方向设置。已移除检测手机姿态后解除初始横屏的逻辑。
- 根据常见移动设备解码能力和屏幕尺寸设置起始参数:显示长边不超过 1280 像素时默认低画质(最高 720p)/30 fps,其余默认中画质(最高 1080p)/30 fps。用户保存的画面偏好不变,仍可手动选择高画质或 60 fps。 - 根据常见移动设备解码能力和屏幕尺寸设置起始参数:显示长边不超过 1280 像素时默认低画质(最高 720p)/30 fps,其余默认中画质(最高 1080p)/30 fps。用户保存的画面偏好不变,仍可手动选择高画质或 60 fps。
- 网络状态按视频、音频、数据、合计展示收发速率和丢包率,并显示实际提交帧率、分辨率、画面延时、平滑 RTT、P2P/TURN 和实际 SRTP 状态。每秒刷新,过期或无效测量显示“—”。 - 网络状态按视频、音频、数据、合计展示收发速率和丢包率,并显示实际提交帧率、分辨率、画面延时、平滑 RTT、P2P/TURN 和实际 SRTP 状态。每秒刷新,过期或无效测量显示“—”。
- 可拖动的悬浮控制球、三列控制菜单;默认使用系统输入法,保留候选词、纠错和听写,确认文字后直接输入远端。电脑键盘固定底部,分“字母数字 / 符号功能”两页,共用末两行布局,按系统键盘高度适配横竖屏;统一切换、收起按钮和 Android 键盘触感反馈。 - 可拖动的悬浮控制球、三列控制菜单;默认使用系统输入法,保留候选词、纠错和听写,确认文字后直接输入远端。电脑键盘固定底部,分“字母数字 / 符号功能”两页,共用末两行布局,按系统键盘高度适配横竖屏;统一切换、收起按钮和 Android 键盘触感反馈。
- 悬浮菜单中的“虚拟鼠标”展开 140 × 48 dp 横向左键/滚轮/右键条,跟随悬浮球移动,可随时收起,不压缩远程画面。按住左键或右键后,同一根手指拖动即可拖拽,滑出按键范围仍继续,松开结束;也支持另一指在画面上移动光标。采用无文字的扁平分体按键,中间滚轮外观为 18 × 30 dp,三键均保留 44 × 44 dp 触控范围,通过留白分隔。无阴影、凹凸或按压位移,按压仅变色。等色横条随手指上下循环移动,松手停止;上下滑动可超出按键范围,在画面放大后仍控制远端滚动。切换模式、显示器、进入后台或取消触摸会释放按键。状态菜单按内容高度完整展开,只有安全区域空间不足时才滚动。 - 悬浮菜单中的“虚拟鼠标”展开 140 × 48 dp 横向左键/滚轮/右键条,跟随悬浮球移动,可随时收起,不压缩远程画面。按住左键或右键后,同一根手指拖动即可拖拽,滑出按键范围仍继续,松开结束;也支持另一指在画面上移动光标。采用无文字的扁平分体按键,中间滚轮外观为 18 × 30 dp,三键均保留 44 × 44 dp 触控范围,通过留白分隔。无阴影、凹凸或按压位移,按压仅变色。等色横条随手指上下循环移动,松手停止;上下滑动可超出按键范围,在画面放大后仍控制远端滚动。切换模式、显示器、进入后台或取消触摸会释放按键。状态菜单按内容高度完整展开,只有安全区域空间不足时才滚动。
- 悬浮菜单提供“隐私屏”直接开关,无需进入详情页;按钮按电脑确认的状态显示“隐私屏已开启”或“隐私屏已关闭”,开启与关闭使用相同配色,正常切换不显示额外文字,仅在异常时显示提示。开启时与桌面端一致屏蔽电脑本地键鼠;声明控制能力后可使用电脑的连接时自动开启设置。状态超过 5 秒未更新时停止显示成功,指令 15 秒未确认时提示超时;后台保活与画中画恢复沿用同一会话状态。
- 悬浮控制栏支持选择并发送文件、接收电脑端文件、查看确认进度及保存收到的文件;Ctrl+Alt+Del 使用远端 Windows 系统服务,未启用服务时显示提示。 - 悬浮控制栏支持选择并发送文件、接收电脑端文件、查看确认进度及保存收到的文件;Ctrl+Alt+Del 使用远端 Windows 系统服务,未启用服务时显示提示。
- 设备身份与选择保存的连接密码使用 Android Keystore / AES-GCM 加密;首次联网需同意。远程会话在切换应用时自动使用系统画中画,不可用时自动使用通知保活。 - 设备身份与选择保存的连接密码使用 Android Keystore / AES-GCM 加密;首次联网需同意。远程会话在切换应用时自动使用系统画中画,不可用时自动使用通知保活。
- 与 iOS 一致的首页卡片、顶部状态/公告/设置入口、密码弹层及分组设置页;最近连接按服务器隔离,长按可删除记录及密码。 - 与 iOS 一致的首页卡片、顶部状态/公告/设置入口、密码弹层及分组设置页;最近连接按服务器隔离,长按可删除记录及密码。
@@ -25,7 +26,7 @@
- 可选保存远程画面预览:默认关闭,经确认开启后,从下一次连接起保存一张 640×360 预览,用于最近连接卡片。支持清除预览,关闭选项或撤回授权会清除已有图片。 - 可选保存远程画面预览:默认关闭,经确认开启后,从下一次连接起保存一张 640×360 预览,用于最近连接卡片。支持清除预览,关闭选项或撤回授权会清除已有图片。
- “关于 CrossDesk”对齐 iOS 的版本、软件许可、开源组件及源码与构建说明页面,包含 OpenFEC 告知、组件详情和可离线选择复制的完整许可;版本号读取当前 APK,构建时记录应用与 MiniRTC 的基础版本。 - “关于 CrossDesk”对齐 iOS 的版本、软件许可、开源组件及源码与构建说明页面,包含 OpenFEC 告知、组件详情和可离线选择复制的完整许可;版本号读取当前 APK,构建时记录应用与 MiniRTC 的基础版本。
当前不包含安卓被控端、隐私屏或 iOS 全部附加功能。硬件能力取决于设备和编码格式,软件回退时高分辨率或高帧率可能增加功耗;可通过悬浮菜单的画面设置降低画质或采集帧率。暂只构建 `arm64-v8a`,不支持 x86 模拟器或 32 位设备。 当前不包含安卓被控端或 iOS 全部附加功能。硬件能力取决于设备和编码格式,软件回退时高分辨率或高帧率可能增加功耗;可通过悬浮菜单的画面设置降低画质或采集帧率。暂只构建 `arm64-v8a`,不支持 x86 模拟器或 32 位设备。
## 本机更新提示 ## 本机更新提示
@@ -71,7 +72,7 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
首次构建会下载并从源码编译依赖,耗时取决于网络。后续编译复用缓存。Android 直接编译 `deps/submodules/minirtc` 中的源码;构建产物和依赖缓存位于 `.native/`,Xmake 工程配置位于 `native/.xmake/`,与桌面和 iOS 分开保存: 首次构建会下载并从源码编译依赖,耗时取决于网络。后续编译复用缓存。Android 直接编译 `deps/submodules/minirtc` 中的源码;构建产物和依赖缓存位于 `.native/`,Xmake 工程配置位于 `native/.xmake/`,与桌面和 iOS 分开保存:
- Gradle 9.3.1、AGP 9.1.0、NDK r28c 固定在工程中。 - Gradle 9.3.1、AGP 9.1.0、NDK r28c 固定在工程中。
- Xmake 包仓库固定到 `eda39de3fb99b420c168f1ab9ab2d1791e11b662`。 - Xmake 依赖仓库沿用 PC 端策略:使用默认仓库或调用环境指定的仓库,不固定或校验仓库提交。Android 编译产物和安装包缓存仍独立保存。
- `scripts/build_native.py` 调用独立的 `native/xmake.lua`,直接包含 MiniRTC 子模块的构建定义。构建过程不复制 MiniRTC 源码,也不应用补丁。 - `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 源码内的 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 静态库。 - MiniRTC 的 `thirdparty/openssl/xmake.lua` 统一维护 iOS 和 Android 配方;Android 显式使用 NDK 的归档索引工具,避免 macOS `ranlib` 损坏 ELF 静态库。
@@ -97,7 +98,7 @@ git push origin android-v1.6.1-20261010
已有 Release 需要补传服务器或刷新下载信息时,可手动运行 `Update version.json from Release` 并填写已有 `source_tag`,无需重新构建。 已有 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` 预热,再创建发布标签。 Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。GitHub 托管 runner 每次启动仍是新环境;第一次运行或缓存失效时需要安装。不同分支和标签可读取默认分支的缓存,因此建议先在默认分支运行一次 `Build Android` 预热,再创建发布标签。
@@ -152,7 +153,9 @@ gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。 身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的 Ctrl / Win / Alt / Shift 可锁定组合键;macOS 的 Command 对应 Win。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。Windows 通过当前已解锁用户桌面的 Unicode 键盘事件输入文本,无需切换电脑输入法或覆盖剪贴板;锁屏和 UAC 安全桌面不接受此文本通道。Windows 被控端也必须更新到包含文本输入支持的版本,仅更新 Android 不会启用该能力。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。 会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 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 秒没有有效进展则报告失败。接收时校验大小并追踪完整字节范围,允许跨线程回调乱序,未完成的文件不会提供保存入口。同名文件分开存储,不覆盖已有文件;可通过“保存收到的文件”导出最近收到的文件,连续接收多个文件时应逐个保存。 “发送文件”调用系统文件选择器,后台读取所选文件后使用桌面端相同的 `file` / `file_feedback` 协议传输,支持空文件和中文文件名。每次发送一个文件,以 64 KiB 分块、最多 1 MiB 未确认数据限制发送队列;进度来自接收端确认,30 秒没有有效进展则报告失败。接收时校验大小并追踪完整字节范围,允许跨线程回调乱序,未完成的文件不会提供保存入口。同名文件分开存储,不覆盖已有文件;可通过“保存收到的文件”导出最近收到的文件,连续接收多个文件时应逐个保存。
@@ -246,4 +249,4 @@ AV1 硬件路径尚无对应实机验证;Retroid 上使用 dav1d 软件解码
设置 → 关于使用与 iOS 相同的分组及入口:版本、软件许可、开源组件、源码与构建说明。软件许可中可分别阅读 GNU GPL 第 3 版及开源软件权利说明;开源组件页单独展示 OpenFEC 告知,其余组件列出名称和许可证,详情包含版本、源码、适用的构建配方和每一份许可文档。内容按 Android 构建生成,不显示未使用的 iOS 依赖,Android 签名说明替换 Apple 签名说明。 设置 → 关于使用与 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.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <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:icon="@mipmap/ic_launcher" android:allowBackup="false"
android:fullBackupContent="false" android:fullBackupContent="false"
android:dataExtractionRules="@xml/data_extraction_rules" 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" <service android:name=".RemoteSessionService" android:exported="false"
android:foregroundServiceType="specialUse"> android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" <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; LicenseCatalog loaded;try{loaded=new LicenseCatalog(activity);}catch(Exception error){loaded=null;}catalog=loaded;
} }
void show(){ void show(){
LinearLayout content=activity.formPage("about","关于 CrossDesk",settings); LinearLayout content=activity.formPage("about",L10n.tr("关于 CrossDesk"),settings);
LinearLayout version=activity.section(content,"");value(version,"版本",applicationVersion()); 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)); 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); 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); 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;} if(catalog==null){unavailable(content);return;}
LinearLayout links=activity.section(content,""); LinearLayout links=activity.section(content,"");
activity.link(links,"软件许可","",()->component(catalog.find("crossdesk"),this::show));ui.divider(links); activity.link(links,L10n.tr("软件许可"),"",()->component(catalog.find("crossdesk"),this::show));ui.divider(links);
activity.link(links,"开源组件","",this::components);ui.divider(links); activity.link(links,L10n.tr("开源组件"),"",this::components);ui.divider(links);
activity.link(links,"源码与构建说明","",this::source); activity.link(links,L10n.tr("源码与构建说明"),"",this::source);
activity.footer(content,"许可与版权声明可离线查阅。"); activity.footer(content,L10n.tr("许可与版权声明可离线查阅。"));
} }
private String applicationVersion(){ private String applicationVersion(){
return activity.applicationVersion(); return activity.applicationVersion();
@@ -45,11 +45,11 @@ final class AboutPages {
void renderUpdates(){ void renderUpdates(){
if(updateStatus==null)return; if(updateStatus==null)return;
updateRow.setVisibility(updates.updateAvailable()?View.VISIBLE:View.GONE); updateRow.setVisibility(updates.updateAvailable()?View.VISIBLE:View.GONE);
updateLink.setText("新版本可用:v"+updates.availableVersion); updateLink.setText(L10n.format("新版本可用:v{0}",updates.availableVersion));
String status=!activity.hasNetworkConsent()?"完成隐私授权后可检查更新":switch(updates.status){ String status=!activity.hasNetworkConsent()?L10n.tr("完成隐私授权后可检查更新"):switch(updates.status){
case CHECKING -> "正在检查更新…"; case CHECKING -> L10n.tr("正在检查更新…");
case UP_TO_DATE -> "当前已是最新版本"; case UP_TO_DATE -> L10n.tr("当前已是最新版本");
case FAILED -> "检查失败,请稍后重试"; case FAILED -> L10n.tr("检查失败,请稍后重试");
default -> ""; default -> "";
}; };
updateStatus.setText(status);updateStatus.setVisibility(status.isEmpty()?View.GONE:View.VISIBLE); updateStatus.setText(status);updateStatus.setVisibility(status.isEmpty()?View.GONE:View.VISIBLE);
@@ -58,11 +58,11 @@ final class AboutPages {
checkButton.setAlpha(checkButton.isEnabled()?1f:.5f); checkButton.setAlpha(checkButton.isEnabled()?1f:.5f);
} }
private void components(){ private void components(){
LinearLayout content=activity.formPage("open-source","开源组件",this::show); LinearLayout content=activity.formPage("open-source",L10n.tr("开源组件"),this::show);
LinearLayout notice=activity.section(content,"OpenFEC 告知"); LinearLayout notice=activity.section(content,L10n.tr("OpenFEC 告知"));
paragraph(notice,"本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。",SECONDARY); 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); 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; LinearLayout list=activity.section(content,"");boolean first=true;
for(LicenseCatalog.Component item:catalog.components){ for(LicenseCatalog.Component item:catalog.components){
if(item.id.equals("crossdesk")||item.id.equals("openfec"))continue; 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.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); 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){ private void component(LicenseCatalog.Component item,Runnable back){
LinearLayout content=activity.formPage("license:"+item.id,item.name,back);LinearLayout info=activity.section(content,""); 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; 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); value(info,L10n.tr("版本"),version);ui.divider(info);value(info,L10n.tr("许可"),item.license);ui.divider(info);
external(info,"查看源码",item.sourceURL); external(info,L10n.tr("查看源码"),item.sourceURL);
if(!item.buildDocuments.isEmpty()||!item.buildSourceURL.isEmpty()){ if(!item.buildDocuments.isEmpty()||!item.buildSourceURL.isEmpty()){
ui.divider(info); ui.divider(info);
if(!item.buildDocuments.isEmpty())activity.link(info,"构建配方与项目补丁","",()->buildSources(item,back)); if(!item.buildDocuments.isEmpty())activity.link(info,L10n.tr("构建配方与项目补丁"),"",()->buildSources(item,back));
else external(info,"构建配方与项目补丁",item.buildSourceURL); 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++){ for(int i=0;i<item.documents.size();i++){
if(i>0)ui.divider(docs);LicenseCatalog.Document document=item.documents.get(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))); activity.link(docs,item.documentTitle(document),"",()->document(item.documentTitle(document),document,()->component(item,back)));
} }
} }
private void source(){ private void source(){
LinearLayout content=activity.formPage("source","源码与构建说明",this::show); LinearLayout content=activity.formPage("source",L10n.tr("源码与构建说明"),this::show);
LinearLayout info=activity.section(content,"本版本源码与构建说明"); LinearLayout info=activity.section(content,L10n.tr("本版本源码与构建说明"));
if(catalog.source==null){paragraph(info,"本版本源码信息暂不可用。",SECONDARY);ui.divider(info);external(info,"项目主页","https://github.com/kunkundi/crossdesk");return;} 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",""); 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); 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,"开发版本包含尚未发布的修改,以下链接为基础版本源码。",SECONDARY);ui.divider(info);} if(catalog.source.optBoolean("isModified")){paragraph(info,L10n.tr("开发版本包含尚未发布的修改,以下链接为基础版本源码。"),SECONDARY);ui.divider(info);}
external(info,"查看源码",catalog.source.optString("sourceURL",catalog.find("crossdesk").sourceURL));ui.divider(info); external(info,L10n.tr("查看源码"),catalog.source.optString("sourceURL",catalog.find("crossdesk").sourceURL));ui.divider(info);
try{ try{
List<LicenseCatalog.Document> documents=LicenseCatalog.documents(catalog.source.optJSONArray("buildDocuments")); List<LicenseCatalog.Document> documents=LicenseCatalog.documents(catalog.source.optJSONArray("buildDocuments"));
if(!documents.isEmpty())actionLink(info,"构建说明",()->document("构建说明",documents.get(0),this::source)); if(!documents.isEmpty())actionLink(info,L10n.tr("构建说明"),()->document(L10n.tr("构建说明"),documents.get(0),this::source));
else external(info,"构建说明",catalog.source.optString("buildInstructionsURL","https://github.com/kunkundi/crossdesk")); else external(info,L10n.tr("构建说明"),catalog.source.optString("buildInstructionsURL","https://github.com/kunkundi/crossdesk"));
}catch(org.json.JSONException error){paragraph(info,"本版本源码信息暂不可用。",SECONDARY);} }catch(org.json.JSONException error){paragraph(info,L10n.tr("本版本源码信息暂不可用。"),SECONDARY);}
} }
private void buildSources(LicenseCatalog.Component item,Runnable back){ private void buildSources(LicenseCatalog.Component item,Runnable back){
LinearLayout content=activity.formPage("build:"+item.id,"构建配方与项目补丁",()->component(item,back)); LinearLayout content=activity.formPage("build:"+item.id,L10n.tr("构建配方与项目补丁"),()->component(item,back));
if(!item.buildSourceURL.isEmpty()){LinearLayout upstream=activity.section(content,"");external(upstream,"查看源码",item.buildSourceURL);ui.gap(content,28);} if(!item.buildSourceURL.isEmpty()){LinearLayout upstream=activity.section(content,"");external(upstream,L10n.tr("查看源码"),item.buildSourceURL);ui.gap(content,28);}
LinearLayout docs=activity.section(content,""); LinearLayout docs=activity.section(content,"");
for(int i=0;i<item.buildDocuments.size();i++){ for(int i=0;i<item.buildDocuments.size();i++){
if(i>0)ui.divider(docs);LicenseCatalog.Document document=item.buildDocuments.get(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 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){ private void open(String url){
Uri uri=Uri.parse(url);if(!"https".equals(uri.getScheme())&&!"http".equals(uri.getScheme()))return; 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){ 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.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; TextView text=ui.text(value,14,false);text.setTextColor(color);text.setLineSpacing(ui.dp(3),1);return text;
} }
private void renderList(){ 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()){ if(!inbox.isConnected()){
content.addView(message("连接服务器后可获取最新公告。",SECONDARY)); content.addView(message(L10n.tr("连接服务器后可获取最新公告。"),SECONDARY));
content.addView(ui.action("连接服务器",BLUE,authorize));ui.gap(content,8); content.addView(ui.action(L10n.tr("连接服务器"),BLUE,authorize));ui.gap(content,8);
}else if(inbox.failed){ }else if(inbox.failed){
LinearLayout error=ui.row();error.addView(message("公告加载失败,请重试。",SECONDARY),new LinearLayout.LayoutParams(0,-2,1)); LinearLayout error=ui.row();error.addView(message(L10n.tr("公告加载失败,请重试。"),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); 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); 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); LinearLayout card=ui.column();ui.card(card,12);content.addView(card);
for(AnnouncementInbox.Item item:inbox.items){ for(AnnouncementInbox.Item item:inbox.items){
@@ -69,11 +69,11 @@ final class AnnouncementView extends ScrollView {
if(inbox.loading){ if(inbox.loading){
LinearLayout loading=ui.row();loading.setGravity(Gravity.CENTER);loading.setPadding(0,ui.dp(18),0,ui.dp(18)); 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)); 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){ }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()){ }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){ 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); 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)); 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); 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;}); row.setOnClickListener(v->open.accept(item));row.setOnLongClickListener(v->{confirmDelete(item);return true;});
return new SwipeRow(row,()->confirmDelete(item)); return new SwipeRow(row,()->confirmDelete(item));
} }
private void confirmDelete(AnnouncementInbox.Item item){ private void confirmDelete(AnnouncementInbox.Item item){
if(deleteDialog!=null)deleteDialog.dismiss(); if(deleteDialog!=null)deleteDialog.dismiss();
deleteDialog=new AlertDialog.Builder(getContext()).setTitle("删除公告").setMessage("删除这条公告?") deleteDialog=new AlertDialog.Builder(getContext()).setTitle(L10n.tr("删除公告")).setMessage(L10n.tr("删除这条公告?"))
.setNegativeButton("取消",null).setPositiveButton("删除",(dialog,which)->inbox.dismiss(item)).show(); .setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("删除"),(dialog,which)->inbox.dismiss(item)).show();
} }
private void renderDetail(){ private void renderDetail(){
AnnouncementInbox.Item item=inbox.find(id,revision); 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); 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){ if(inbox.readSaveFailed){
content.addView(message("无法保存本地阅读状态,请重试。",RED)); content.addView(message(L10n.tr("无法保存本地阅读状态,请重试。"),RED));
content.addView(ui.action("重试保存阅读状态",BLUE,()->inbox.markRead(item)));ui.gap(content,16); 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); 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); 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); trackPull(event);
if(!pulling)return super.onTouchEvent(event); if(!pulling)return super.onTouchEvent(event);
boolean reached=event.getY()-downY>=ui.dp(72); 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){ if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL){
pulling=false;if(reached&&event.getActionMasked()==MotionEvent.ACTION_UP)inbox.refresh();else render(); 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; private boolean dragging;
SwipeRow(View front,Runnable delete){ SwipeRow(View front,Runnable delete){
super(ui.context);this.front=front;setClipChildren(true); 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.setVisibility(INVISIBLE);
remove.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);remove.setFocusable(false); 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)); addView(remove,new FrameLayout.LayoutParams(ui.dp(76),-1,Gravity.END));addView(front,new FrameLayout.LayoutParams(-1,-2));
@@ -19,7 +19,7 @@ final class BackgroundSession implements NativeSession.Listener {
RemoteVideoSettings videoSettings; RemoteVideoSettings videoSettings;
int signalState, selectedDisplay, width, height; int signalState, selectedDisplay, width, height;
boolean muted, remoteServiceAvailable, background, ended; boolean muted, remoteServiceAvailable, background, ended;
boolean waitingForLandscape; boolean autoLandscape;
File receivedFile; File receivedFile;
BackgroundSession(Context context, NativeSession session, String host, int port, String remote) { BackgroundSession(Context context, NativeSession session, String host, int port, String remote) {
@@ -46,7 +46,7 @@ final class BackgroundSession implements NativeSession.Listener {
if (listener != null) listener.ended(reason); if (listener != null) listener.ended(reason);
} }
@Override public void ended(String reason) { finish(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 connected() { /* Already connected before the service takes ownership. */ }
@Override public void status(String message) { if (view != null) view.status(message); } @Override public void status(String message) { if (view != null) view.status(message); }
@Override public void signaling(int state, String id) { @Override public void signaling(int state, String id) {
@@ -75,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 statistics(RemoteNetworkStatistics.Snapshot value) { if (view != null) view.statistics(value); }
@Override public void fileTransfer(String name, double progress, boolean sending, File received) { @Override public void fileTransfer(String name, double progress, boolean sending, File received) {
if (received != null) receivedFile = received; if (received != null) receivedFile = received;
fileStatus = progress < 0 ? name + " 传输失败" : (sending ? "发送 " : "接收 ") + name fileStatus = progress < 0 ? L10n.format("{0} 传输失败", name) : (sending ? L10n.tr("发送 ") : L10n.tr("接收 ")) + name
+ (progress >= 1 ? " · 已完成" : " · " + (int)(progress * 100) + "%"); + (progress >= 1 ? L10n.tr(" · 已完成") : " · " + (int)(progress * 100) + "%");
if (view != null) view.fileTransfer(name, progress, sending, received); if (view != null) view.fileTransfer(name, progress, sending, received);
} }
@Override public void controlFeedback(String text) { if (view != null) view.controlFeedback(text); } @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 presence(JSONObject message) { if (view != null) view.presence(message); }
@Override public void announcement(JSONObject message) { if (view != null) view.announcement(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); } @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"); if(documents.isEmpty())throw new JSONException("Missing documents");
} }
String documentTitle(Document document){ String documentTitle(Document document){
if(document.title.equals("Source copyright and license notices"))return "版权与许可声明"; if(document.title.equals("Source copyright and license notices"))return L10n.tr("版权与许可声明");
if(document.title.equals("apps/android/licenses/OPEN_SOURCE_NOTICE.txt"))return "开源软件权利说明"; if(document.title.equals("apps/android/licenses/OPEN_SOURCE_NOTICE.txt"))return L10n.tr("开源软件权利说明");
if(id.equals("crossdesk")&&document.title.equals("LICENSE"))return "GNU GPL 第 3 版"; if(id.equals("crossdesk")&&document.title.equals("LICENSE"))return L10n.tr("GNU GPL 第 3 版");
return document.title; 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() {}
}
@@ -79,6 +79,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private AlertDialog connectionDialog; private AlertDialog connectionDialog;
private String pendingConnectionMessage=""; private String pendingConnectionMessage="";
private AlertDialog remoteUpdateDialog; private AlertDialog remoteUpdateDialog;
private AlertDialog privacyUnlockDialog;
private RemoteVersionCheck remoteVersionCheck; private RemoteVersionCheck remoteVersionCheck;
private String host, connectedRemote="", hostPlatform="", remoteClipboard="", pendingPassword="", page="home"; private String host, connectedRemote="", hostPlatform="", remoteClipboard="", pendingPassword="", page="home";
private int port,selectedDisplay; private int port,selectedDisplay;
@@ -88,7 +89,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private static final int PICK_FILE=41,SAVE_FILE=42; private static final int PICK_FILE=41,SAVE_FILE=42;
private NativeSession documentSession; private NativeSession documentSession;
private java.io.File receivedFile,documentToSave; 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 RemoteVideoSettings videoSettings=new RemoteVideoSettings(1);
private final android.os.Handler mainHandler=new android.os.Handler(android.os.Looper.getMainLooper()); private final android.os.Handler mainHandler=new android.os.Handler(android.os.Looper.getMainLooper());
private Runnable videoSettingsTimeout; private Runnable videoSettingsTimeout;
@@ -157,7 +158,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
} }
private LinearLayout navigation(String title,Runnable back){ 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)); 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)); 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; bar.addView(new View(this),ui.size(44,44));backAction=back;return bar;
} }
@@ -199,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=text("",12,true);signalBadge.setGravity(Gravity.CENTER);signalBadge.setPadding(dp(11),0,dp(11),0);signalBadge.setSingleLine(true);
signalBadge.setOnClickListener(v->{ signalBadge.setOnClickListener(v->{
if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling); 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();} else {stopSignaling();ensureSignaling();}
});renderSignaling();toolbar.addView(signalBadge,ui.size(-2,34));toolbar.addView(new View(this),new LinearLayout.LayoutParams(0,1,1)); });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); 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(); 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)); 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; 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); 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(); 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; 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)); 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)); 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.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",""))); remote.setFilters(new InputFilter[]{new InputFilter.LengthFilter(11)});remote.setText(formatId(preferences.getString("lastRemote","")));
entry.addView(remote,new LinearLayout.LayoutParams(0,-1,1)); 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(); 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;}}}); 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); 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)); 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(records.length()+" 台设备",12,false);count.setTextColor(SECONDARY);heading.addView(count);}recent.addView(heading); 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){ 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); 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{ }else{
ui.gap(recent,14);ScrollView scroll=new ScrollView(this);recent.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));recentGrid=column();scroll.addView(recentGrid); 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)->{ recentGrid.addOnLayoutChangeListener((view,left,top,right,bottom,oldLeft,oldTop,oldRight,oldBottom)->{
@@ -242,7 +243,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private void showConnectionMessage(){ private void showConnectionMessage(){
if(pendingConnectionMessage.isEmpty()||!foreground||isFinishing()||isDestroyed())return; if(pendingConnectionMessage.isEmpty()||!foreground||isFinishing()||isDestroyed())return;
if(connectionDialog!=null)connectionDialog.dismiss(); 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); connectionDialog.setOnDismissListener(dialog->connectionDialog=null);
pendingConnectionMessage="";connectionDialog.show(); pendingConnectionMessage="";connectionDialog.show();
} }
@@ -258,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"); 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));}}; 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); 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)); 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); 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(); 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 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)); 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);} 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?"在线":"离线";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); 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("连接 "+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.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[]{"连接","删除记录"},(d,w)->{if(w==0)promptConnection(id);else{history.delete(serverKey(),id);home("");}}).show();return true;});return card; 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){ 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); if(!preferences.getBoolean("networkConsent",false)){consent(()->connectOrPrompt(id));return;}connectOrPrompt(id);
} }
private void connectOrPrompt(String id){ private void connectOrPrompt(String id){
String stored; 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;} if(!stored.isEmpty()){rememberPassword=true;begin(id,stored);return;}
boolean remember=history.remembersPassword(serverKey(),id); boolean remember=history.remembersPassword(serverKey(),id);
passwordSheet(id,remember?"已保存的密码未能保留,请重新输入一次":"",remember); passwordSheet(id,remember?L10n.tr("已保存的密码未能保留,请重新输入一次"):"",remember);
} }
private void consent(Runnable accepted){ private void consent(Runnable accepted){
if(privacyDialog!=null&&privacyDialog.isShowing())return; if(privacyDialog!=null&&privacyDialog.isShowing())return;
@@ -290,15 +291,15 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private void passwordSheet(String id,String error,boolean rememberDefault){ 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)); 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); 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 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("访问密码",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);password.setBackgroundColor(Color.TRANSPARENT);passwordRow.addView(password,new LinearLayout.LayoutParams(0,dp(50),1)); 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","显示密码",()->{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); 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("保存密码",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 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); remember.setChecked(rememberDefault);
ui.gap(sheet,18);TextView connect=ui.primary("连接 →",()->{ 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("请输入有效访问密码");return;} 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); 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); 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.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;}); dialog.setOnDismissListener(d->{password.setText("");passwordDialog=null;});
@@ -318,27 +319,29 @@ public final class MainActivity extends Activity implements NativeSession.Listen
previewCapture=previews.begin(serverKey(),id);previewCopying=false;previewAttempts=0; 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(); connectedRemote=id;pendingPassword=rememberPassword?secret:"";ready=false;muted=false;selectedDisplay=0;hostPlatform="";remoteClipboard="";displays=new JSONArray();preferences.edit().putString("lastRemote",id).apply();
ensureSignaling(); ensureSignaling();
if(signaling==null){pendingPassword="";toast("请先完成联网授权");return;} if(signaling==null){pendingPassword="";toast(L10n.tr("请先完成联网授权"));return;}
session=signaling;showProgress();session.connect(id,secret); session=signaling;showProgress();session.connect(id,secret);
} }
private void showProgress(){ 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)); 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); 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(boolean waitForLandscape){ private void sessionScreen(boolean autoLandscape){
installRoot("session",true);remoteOrientation.begin(waitForLandscape&&!isTablet()&&!isInMultiWindowMode());video=new RemoteVideoView(this,session);video.setRelative(preferences.getBoolean("relativeMouse",true));canvas.addView(video,0,new FrameLayout.LayoutParams(-1,-1)); 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(){ controls=new SessionControls(this,ui,new SessionControls.Actions(){
public void disconnect(){confirmDisconnect();}public void display(){selectDisplay();}public void clipboard(){MainActivity.this.clipboard();} 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 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 void key(int code){tapKey(code);}
public boolean textEdit(int count,String value){return sendTextEdit(count,value);} 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 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 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 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));} 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)); controls.attachMouseBar(canvas,new VirtualMouseBar(ui,video.mouseInput));
privacyChanged();
video.remoteFocusChanged=()->{if(controls!=null)controls.resetKeyboardContext();}; video.remoteFocusChanged=()->{if(controls!=null)controls.resetKeyboardContext();};
controls.requestApplyInsets(); controls.requestApplyInsets();
video.addOnLayoutChangeListener((view,left,top,right,bottom,oldLeft,oldTop,oldRight,oldBottom)->{ video.addOnLayoutChangeListener((view,left,top,right,bottom,oldLeft,oldTop,oldRight,oldBottom)->{
@@ -346,8 +349,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
}); });
pictureInPicture.connected();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){ private void end(String reason){
dismissPrivacyUnlockDialog();
remoteOrientation.end(); remoteOrientation.end();
if(backgroundSession!=null){BackgroundSession retained=backgroundSession;backgroundSession=null;retained.detach(this);retained.finish(reason);} if(backgroundSession!=null){BackgroundSession retained=backgroundSession;backgroundSession=null;retained.detach(this);retained.finish(reason);}
boolean closeWindow=isInPictureInPictureMode(); boolean closeWindow=isInPictureInPictureMode();
@@ -363,48 +367,48 @@ public final class MainActivity extends Activity implements NativeSession.Listen
appUpdates.checkNow(); appUpdates.checkNow();
} }
private void settings(){ private void settings(){
LinearLayout content=formPage("settings","设置",()->home("")); LinearLayout content=formPage("settings",L10n.tr("设置"),()->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); 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[]{"相对位置","绝对位置"},relative?0:1,true,index->preferences.edit().putBoolean("relativeMouse",index==0).apply())); 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,"画面偏好",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))); 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); picture.addView(segments(RemoteVideoSettings.preferences(),preference,true,index->preferences.edit().putInt("videoPreference",index).apply()));ui.gap(content,28);
LinearLayout direction=section(content,"屏幕方向",MobileUi.SETTINGS_CARD_RADIUS);LinearLayout directionRow=row();directionRow.setPadding(dp(16),dp(10),dp(16),dp(10));directionRow.addView(text("连接后自动横屏",16,false),new LinearLayout.LayoutParams(0,-2,1)); 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("连接后自动横屏");autoLandscape.setChecked(preferences.getBoolean("autoLandscape",true));autoLandscape.setOnCheckedChangeListener((button,checked)->preferences.edit().putBoolean("autoLandscape",checked).apply());directionRow.addView(autoLandscape);direction.addView(directionRow); 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,"手机连接后先进入横屏,将手机横过来后恢复跟随设备方向。关闭后始终跟随设备方向。平板和多窗口不受影响。"); 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; 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); 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("使用自定义服务器",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 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("服务器地址",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); 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("完成",BLUE,()->{if(saveServer(address,number)){hideKeyboard();settings();}}));fields.addView(inputs);inputs.setVisibility(custom?View.VISIBLE:View.GONE); 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();} }); 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("");}; 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(); 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 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("保存远程画面预览",16,false),new LinearLayout.LayoutParams(0,-2,1));previewSwitch=new Switch(this);previewSwitch.setContentDescription("保存远程画面预览");preview.addView(previewSwitch);privacy.addView(preview); 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);}); 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(); 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); 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(); 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(){ private void confirmPreviewSaving(){
previewDialog=new AlertDialog.Builder(this).setTitle("保存远程画面预览?") previewDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("保存远程画面预览?"))
.setMessage("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。") .setMessage(L10n.tr("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。"))
.setNegativeButton("取消",null).setPositiveButton("开启保存",(d,w)->previews.setEnabled(true)).show(); .setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("开启保存"),(d,w)->previews.setEnabled(true)).show();
previewDialog.setOnDismissListener(d->{previewDialog=null;renderPreviews();}); previewDialog.setOnDismissListener(d->{previewDialog=null;renderPreviews();});
} }
private void confirmClearPreviews(){ private void confirmClearPreviews(){
previewDialog=new AlertDialog.Builder(this).setTitle("清除预览图?") previewDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("清除预览图?"))
.setMessage("将删除本机保存的所有远程预览图,保留连接记录和密码。"+(previews.enabled()?"后续连接仍会保存新的预览图。":"")) .setMessage(L10n.tr("将删除本机保存的所有远程预览图,保留连接记录和密码。")+(previews.enabled()?L10n.tr("后续连接仍会保存新的预览图。"):""))
.setNegativeButton("取消",null).setPositiveButton("清除",(d,w)->previews.clear()).show(); .setNegativeButton(L10n.tr("取消"),null).setPositiveButton(L10n.tr("清除"),(d,w)->previews.clear()).show();
previewDialog.setOnDismissListener(d->previewDialog=null); previewDialog.setOnDismissListener(d->previewDialog=null);
} }
private void renderPreviews(){ private void renderPreviews(){
if(previews==null)return; if(previews==null)return;
if(!previews.enabled()||previews.cleaning)previewCapture=null; 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(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);} if(previewCleanupError!=null){previewCleanupError.setText(previews.cleanupError);previewCleanupError.setVisibility(previews.cleanupError.isEmpty()?View.GONE:View.VISIBLE);}
for(Runnable card:previewCards)card.run(); for(Runnable card:previewCards)card.run();
} }
@@ -423,17 +427,17 @@ public final class MainActivity extends Activity implements NativeSession.Listen
return new SegmentedControl(ui,titles,selected,enabled,changed); return new SegmentedControl(ui,titles,selected,enabled,changed);
} }
private boolean saveServer(EditText address,EditText number){ 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;} 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("填写主机名或 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; 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(){ private void announcements(){
installRoot("announcements",false);LinearLayout bar=navigation("通知公告",()->home("")); installRoot("announcements",false);LinearLayout bar=navigation(L10n.tr("通知公告"),()->home(""));
bar.removeViewAt(bar.getChildCount()-1);announcementRefresh=ui.iconButton("refresh","刷新公告",announcementInbox::refresh);bar.addView(announcementRefresh,ui.size(44,44)); 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); announcementView=new AnnouncementView(ui,announcementInbox,null,this::announcementDetail,this::connectAnnouncements);
root.addView(announcementView,new LinearLayout.LayoutParams(-1,0,1));announcementInbox.refresh();renderAnnouncements(); root.addView(announcementView,new LinearLayout.LayoutParams(-1,0,1));announcementInbox.refresh();renderAnnouncements();
} }
private void announcementDetail(AnnouncementInbox.Item item){ 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); announcementView=new AnnouncementView(ui,announcementInbox,item,this::announcementDetail,this::connectAnnouncements);
root.addView(announcementView,new LinearLayout.LayoutParams(-1,0,1));announcementInbox.markRead(item); root.addView(announcementView,new LinearLayout.LayoutParams(-1,0,1));announcementInbox.markRead(item);
} }
@@ -443,7 +447,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
} }
private void renderAnnouncements(){ 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(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(announcementRefresh!=null){boolean enabled=announcementInbox.isConnected()&&!announcementInbox.loading;announcementRefresh.setEnabled(enabled);announcementRefresh.setAlpha(enabled?1:.4f);}
if(announcementView!=null)announcementView.render(); if(announcementView!=null)announcementView.render();
} }
@@ -453,12 +457,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
} }
@Override public void announcementFailed(String requestId){announcementInbox.failed(requestId);} @Override public void announcementFailed(String requestId){announcementInbox.failed(requestId);}
private void privacyPage(){ 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); 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,"设备身份及选择保存的访问密码使用 Android Keystore 加密。连接记录仅保存在本机,不纳入设备备份。剪贴板内容在结束会话时清除。离开应用后通过画中画或连接通知保持会话,可在应用或通知中断开。"); footer(content,L10n.tr("设备身份及选择保存的访问密码使用 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))); 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(){ 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)); root.addView(new PrivacyPolicyDocument(this).view(ui),new LinearLayout.LayoutParams(-1,0,1));
} }
String applicationVersion(){ String applicationVersion(){
@@ -475,8 +479,8 @@ public final class MainActivity extends Activity implements NativeSession.Listen
if(settingsUpdateDot!=null)settingsUpdateDot.setVisibility(available?View.VISIBLE:View.GONE); if(settingsUpdateDot!=null)settingsUpdateDot.setVisibility(available?View.VISIBLE:View.GONE);
if(aboutUpdateDot!=null)aboutUpdateDot.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(aboutUpdateLabel!=null)aboutUpdateLabel.setVisibility(available?View.VISIBLE:View.GONE);
if(settingsUpdateButton!=null)settingsUpdateButton.setContentDescription(available?"设置,有新版本":"设置"); if(settingsUpdateButton!=null)settingsUpdateButton.setContentDescription(available?L10n.tr("设置,有新版本"):L10n.tr("设置"));
if(aboutUpdateRow!=null)aboutUpdateRow.setContentDescription(available?"关于,有新版本":"关于"); if(aboutUpdateRow!=null)aboutUpdateRow.setContentDescription(available?L10n.tr("关于,有新版本"):L10n.tr("关于"));
if(aboutPages!=null&&page.equals("about"))aboutPages.renderUpdates(); if(aboutPages!=null&&page.equals("about"))aboutPages.renderUpdates();
} }
private void about(){ private void about(){
@@ -486,16 +490,16 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private boolean sendTextEdit(int count,String value){ private boolean sendTextEdit(int count,String value){
if(session==null)return false; if(session==null)return false;
if(session.textEdit(count,value))return true; if(session.textEdit(count,value))return true;
toast("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送");return false; toast(L10n.tr("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送"));return false;
} }
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,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 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(){ 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(){ private void secureAttention(){
if(!ready||session==null)return; if(!ready||session==null||!hostPlatform.equals("windows"))return;
if(!remoteServiceAvailable){controlFeedback(hostPlatform.equals("windows")?"请在远端启用 CrossDesk 系统服务后重试":"Ctrl+Alt+Del 需要远端 Windows 系统服务支持");return;} if(!remoteServiceAvailable){controlFeedback(L10n.tr("请在远端启用 CrossDesk 系统服务后重试"));return;}
session.secureAttention(); session.secureAttention();
} }
private void chooseFile(){ private void chooseFile(){
@@ -512,7 +516,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
documentSession=session;releaseKeys(); documentSession=session;releaseKeys();
pictureInPicture.choosingDocument(true);updatePictureInPicture(); pictureInPicture.choosingDocument(true);updatePictureInPicture();
try{startActivityForResult(intent,request);mainHandler.postDelayed(documentTimeout,5*60*1000);} 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){ @Override protected void onActivityResult(int request,int result,Intent data){
super.onActivityResult(request,result,data); super.onActivityResult(request,result,data);
@@ -526,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){ @Override public void fileTransfer(String name,double progress,boolean sending,java.io.File received){
if(!ready)return; if(!ready)return;
if(received!=null)receivedFile=received; 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(controls!=null)controls.fileStatus(fileStatus,receivedFile!=null);
if(progress<0||progress>=1)toast(fileStatus); if(progress<0||progress>=1)toast(fileStatus);
} }
@@ -535,14 +539,14 @@ public final class MainActivity extends Activity implements NativeSession.Listen
} }
private void clipboard(){ private void clipboard(){
if(!ready)return; if(!ready)return;
EditText input=field("要发送到远端的文本",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_MULTI_LINE);input.setSingleLine(false);input.setMinLines(3); 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("剪贴板").setView(input) new AlertDialog.Builder(this).setTitle(L10n.tr("剪贴板")).setView(input)
.setMessage("发送后,在远程键盘中点击“粘贴”。接收的远端文本可手动复制到本机。") .setMessage(L10n.tr("发送后,在远程键盘中点击“粘贴”。接收的远端文本可手动复制到本机。"))
.setNegativeButton("关闭",null).setNeutralButton("复制远端文本",(d,w)->{ .setNegativeButton(L10n.tr("关闭"),null).setNeutralButton(L10n.tr("复制远端文本"),(d,w)->{
if(!remoteClipboard.isEmpty())((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("CrossDesk",remoteClipboard)); 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(); 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); if(session!=null)session.clipboard(value);
}).show(); }).show();
} }
@@ -577,17 +581,17 @@ public final class MainActivity extends Activity implements NativeSession.Listen
boolean consent=preferences.getBoolean("networkConsent",false); boolean consent=preferences.getBoolean("networkConsent",false);
boolean connected=consent&&signalError.isEmpty()&&signalState==1; boolean connected=consent&&signalError.isEmpty()&&signalState==1;
String label;int tint,background; String label;int tint,background;
if(!consent){label="等待隐私授权";tint=0xFFB76A00;background=0xFFFFEBD2;} if(!consent){label=L10n.tr("等待隐私授权");tint=0xFFB76A00;background=0xFFFFEBD2;}
else if(!signalError.isEmpty()){label=signalError.contains("TLS")?"证书校验失败 · 点击重试":"连接失败 · 点击重试";tint=RED;background=0xFFFFE8E6;} else if(!signalError.isEmpty()){label=signalError.contains("TLS")?L10n.tr("证书校验失败 · 点击重试"):L10n.tr("连接失败 · 点击重试");tint=RED;background=0xFFFFE8E6;}
else if(connected){label="已连接服务器";tint=GREEN;background=0xFFE5F4E8;} else if(connected){label=L10n.tr("已连接服务器");tint=GREEN;background=0xFFE5F4E8;}
else{label=signalState==0?"正在连接服务器…":signalState==4?"正在重连服务器…":"未连接服务器 · 点击重试";tint=0xFFB76A00;background=0xFFFFEBD2;} else{label=signalState==0?L10n.tr("正在连接服务器…"):signalState==4?L10n.tr("正在重连服务器…"):L10n.tr("未连接服务器 · 点击重试");tint=0xFFB76A00;background=0xFFFFEBD2;}
if(signalBadge!=null){ if(signalBadge!=null){
android.graphics.drawable.Drawable icon=connected?getDrawable(R.drawable.ic_check_circle):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);} if(icon!=null){int size=dp(14);icon.setBounds(0,0,size,size);}
signalBadge.setCompoundDrawablesRelative(icon,null,null,null);signalBadge.setCompoundDrawablePadding(dp(6)); 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){ @Override public void signaling(int state,String deviceId){
signalState=state;localIdentity=deviceId;signalError="";renderSignaling(); signalState=state;localIdentity=deviceId;signalError="";renderSignaling();
@@ -602,7 +606,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
// Transport readiness can be reported again after ICE changes its selected path. // Transport readiness can be reported again after ICE changes its selected path.
// The first success consumes pendingPassword; a duplicate must not save it again. // 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;} 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(preferences.getBoolean("autoLandscape",true)); 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(!canUsePictureInPicture())prepareBackgroundSession(true);
if(videoSettings.supported&&videoSettings.requestId==0)updateVideoSettings(videoSettings.selection); if(videoSettings.supported&&videoSettings.requestId==0)updateVideoSettings(videoSettings.selection);
if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.connected(); if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.connected();
@@ -616,12 +620,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
String id=connectedRemote;boolean remember=rememberPassword; String id=connectedRemote;boolean remember=rememberPassword;
end(""); end("");
// Let the user replace an expired credential; never retry it automatically. // 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 cancelVideoSettingsTimeout(){if(videoSettingsTimeout!=null)mainHandler.removeCallbacks(videoSettingsTimeout);videoSettingsTimeout=null;}
private void showRemoteUpdate(){ private void showRemoteUpdate(){
if(!ready||session==null||!foreground||pictureInPictureUi||isInPictureInPictureMode()||isFinishing()||!preferences.getBoolean("networkConsent",false))return; 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.setOnDismissListener(dialog->remoteUpdateDialog=null);
remoteUpdateDialog.setCanceledOnTouchOutside(false); remoteUpdateDialog.setCanceledOnTouchOutside(false);
remoteUpdateDialog.show(); remoteUpdateDialog.show();
@@ -648,6 +652,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
JSONObject info=message.optJSONObject("host_info"); JSONObject info=message.optJSONObject("host_info");
if(info!=null){ 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); 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)); boolean wasSupported=videoSettings.supported;videoSettings.setSupported(info.optBoolean("supports_video_settings",false));
if(!videoSettings.supported)cancelVideoSettingsTimeout(); if(!videoSettings.supported)cancelVideoSettingsTimeout();
if(controls!=null)controls.videoSettings(videoSettings); if(controls!=null)controls.videoSettings(videoSettings);
@@ -655,10 +660,42 @@ public final class MainActivity extends Activity implements NativeSession.Listen
if(foreground&&preferences.getBoolean("networkConsent",false))remoteVersionCheck.hostInfo(info); 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 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 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 void statistics(RemoteNetworkStatistics.Snapshot value){if(controls!=null)controls.statistics(value);if(value.fps>0)capturePreviewIfNeeded();}
@Override public boolean dispatchKeyEvent(KeyEvent event){ @Override public boolean dispatchKeyEvent(KeyEvent event){
if (privacyUnlockDialog != null) return super.dispatchKeyEvent(event);
int code=KeyMap.windowsCode(event.getKeyCode());View focus=getCurrentFocus(); int code=KeyMap.windowsCode(event.getKeyCode());View focus=getCurrentFocus();
boolean remoteEditor=focus instanceof RemoteKeyboardInput; boolean remoteEditor=focus instanceof RemoteKeyboardInput;
boolean desktopKey=event.isCtrlPressed()||event.isAltPressed()||event.isMetaPressed()||pressed.contains(code) boolean desktopKey=event.isCtrlPressed()||event.isAltPressed()||event.isMetaPressed()||pressed.contains(code)
@@ -679,7 +716,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
super.onConfigurationChanged(config); super.onConfigurationChanged(config);
releaseKeys(); releaseKeys();
if(!page.equals("session"))updateHomeOrientation(); if(!page.equals("session"))updateHomeOrientation();
else if(!isInPictureInPictureMode()&&(isTablet()||isInMultiWindowMode()))remoteOrientation.followDevice(); else if(!isInPictureInPictureMode())remoteOrientation.update(!isTablet()&&!isInMultiWindowMode());
sizePasswordDialog(); sizePasswordDialog();
if(privacyDialog instanceof PrivacyConsentDialog)((PrivacyConsentDialog)privacyDialog).updateSize(); if(privacyDialog instanceof PrivacyConsentDialog)((PrivacyConsentDialog)privacyDialog).updateSize();
if(controls!=null&&!pictureInPictureUi)controls.post(controls::constrainPanels); if(controls!=null&&!pictureInPictureUi)controls.post(controls::constrainPanels);
@@ -698,7 +735,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
retained.muted=muted;retained.selectedDisplay=selectedDisplay;retained.remoteServiceAvailable=remoteServiceAvailable; retained.muted=muted;retained.selectedDisplay=selectedDisplay;retained.remoteServiceAvailable=remoteServiceAvailable;
retained.clipboard=remoteClipboard;retained.videoSettings=videoSettings;retained.receivedFile=receivedFile; retained.clipboard=remoteClipboard;retained.videoSettings=videoSettings;retained.receivedFile=receivedFile;
retained.width=videoWidth;retained.height=videoHeight; retained.width=videoWidth;retained.height=videoHeight;
retained.waitingForLandscape=remoteOrientation.waitingForLandscape(); retained.autoLandscape=remoteOrientation.autoLandscape();
} }
private boolean prepareBackgroundSession(boolean inform){ private boolean prepareBackgroundSession(boolean inform){
if(!ready||session==null||session.isClosed()||isFinishing()||!hasNetworkConsent())return false; if(!ready||session==null||session.isClosed()||isFinishing()||!hasNetworkConsent())return false;
@@ -707,10 +744,10 @@ public final class MainActivity extends Activity implements NativeSession.Listen
backgroundSession=retained;captureBackgroundSession();retained.attach(this);session.setListener(retained); backgroundSession=retained;captureBackgroundSession();retained.attach(this);session.setListener(retained);
if(!RemoteSessionService.retain(this,retained)){ if(!RemoteSessionService.retain(this,retained)){
session.setListener(this);retained.detach(this);backgroundSession=null; session.setListener(this);retained.detach(this);backgroundSession=null;
if(inform)toast("后台保活未能启动,请保持应用在前台");return false; if(inform)toast(L10n.tr("后台保活未能启动,请保持应用在前台"));return false;
} }
if(inform){ if(inform){
toast("已启用后台保活,可从连接通知返回或断开"); toast(L10n.tr("已启用后台保活,可从连接通知返回或断开"));
requestBackgroundNotifications(); requestBackgroundNotifications();
} }
return true; return true;
@@ -725,7 +762,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
BackgroundSession retained=backgroundSession!=null?backgroundSession:RemoteSessionService.current(); BackgroundSession retained=backgroundSession!=null?backgroundSession:RemoteSessionService.current();
if(retained==null)return false; if(retained==null)return false;
if(retained.ended||retained.session.isClosed()||!hasNetworkConsent()||!host.equals(retained.host)||port!=retained.port){ 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; if(session==retained.session)end(retained.endReason);return false;
} }
boolean rebuild=session!=retained.session||video==null||!page.equals("session"); boolean rebuild=session!=retained.session||video==null||!page.equals("session");
@@ -734,9 +771,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
hostPlatform=retained.platform;displays=retained.displays;muted=retained.muted;selectedDisplay=retained.selectedDisplay; hostPlatform=retained.platform;displays=retained.displays;muted=retained.muted;selectedDisplay=retained.selectedDisplay;
remoteServiceAvailable=retained.remoteServiceAvailable;remoteClipboard=retained.clipboard;receivedFile=retained.receivedFile; remoteServiceAvailable=retained.remoteServiceAvailable;remoteClipboard=retained.clipboard;receivedFile=retained.receivedFile;
videoSettings=retained.videoSettings;ready=true;retained.attach(this); videoSettings=retained.videoSettings;ready=true;retained.attach(this);
if(rebuild)sessionScreen(retained.waitingForLandscape); if(rebuild)sessionScreen(retained.autoLandscape);
if(retained.width>0&&retained.height>0)videoSize(retained.width,retained.height); 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); retained.setBackground(false);signaling(signalState,localIdentity);
return true; return true;
} }
@@ -782,11 +819,11 @@ public final class MainActivity extends Activity implements NativeSession.Listen
super.onPictureInPictureUiStateChanged(state); super.onPictureInPictureUiStateChanged(state);
if(android.os.Build.VERSION.SDK_INT>=35&&state.isTransitioningToPip())setPictureInPictureUi(true); if(android.os.Build.VERSION.SDK_INT>=35&&state.isTransitioningToPip())setPictureInPictureUi(true);
} }
@Override protected void onResume(){super.onResume();remoteOrientation.resume();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. // onStart marks a new visit; focus/resume callbacks must not reopen a refused notice.
@Override protected void onPause(){remoteOrientation.pause();releaseKeys();if(controls!=null)controls.cancelMouseInput();if(video!=null)video.releaseMouse();super.onPause();} @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 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(){if(controls!=null)controls.closeKeyboard();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(); if(connectionDialog!=null)connectionDialog.dismiss();
// Closing native PiP is an explicit end, not a request for invisible // Closing native PiP is an explicit end, not a request for invisible
// fallback. An unavailable/failed PiP can hand its live session off. // fallback. An unavailable/failed PiP can hand its live session off.
@@ -795,9 +832,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
mainHandler.removeCallbacks(presenceTick);recentPresence.setConnected(false,android.os.SystemClock.elapsedRealtime());announcementInbox.setConnected(false); mainHandler.removeCallbacks(presenceTick);recentPresence.setConnected(false,android.os.SystemClock.elapsedRealtime());announcementInbox.setConnected(false);
cancelVideoSettingsTimeout();if(videoSettings.pending)videoSettings.expire(videoSettings.requestId); cancelVideoSettingsTimeout();if(videoSettings.pending)videoSettings.expire(videoSettings.requestId);
resetRemoteVersionCheck();if(disconnectDialog!=null)disconnectDialog.dismiss(); 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();} 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(); if(connectionDialog!=null)connectionDialog.dismiss();
boolean retained=!isFinishing()&&backgroundSession!=null&&!backgroundSession.ended&&RemoteSessionService.current()==backgroundSession; boolean retained=!isFinishing()&&backgroundSession!=null&&!backgroundSession.ended&&RemoteSessionService.current()==backgroundSession;
if(backgroundSession!=null)backgroundSession.detach(this); if(backgroundSession!=null)backgroundSession.detach(this);
@@ -52,6 +52,7 @@ final class MobileUi {
} }
TextView primary(String title,Runnable click) { TextView primary(String title,Runnable click) {
TextView v=action(title,Color.WHITE,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; 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) { 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); 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; 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 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)));} 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));} 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. @android.annotation.SuppressLint("ViewConstructor") // Programmatic icon primitive, not inflated from XML.
static final class Icon extends View { 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);} 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){ @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); 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){ switch(name){
@@ -136,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 "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 "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":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); default:c.drawCircle(12,12,9,paint);path(c,12,10,12,17);c.drawPoint(12,6,paint);
}c.restore(); }c.restore();
} }
@@ -30,11 +30,12 @@ final class NativeSession implements AutoCloseable {
void statistics(RemoteNetworkStatistics.Snapshot value); void statistics(RemoteNetworkStatistics.Snapshot value);
default void fileTransfer(String name,double progress,boolean sending,File received) { } default void fileTransfer(String name,double progress,boolean sending,File received) { }
default void controlFeedback(String message) { } default void controlFeedback(String message) { }
default void privacyChanged() { }
default void signaling(int state, String deviceId) { } default void signaling(int state, String deviceId) { }
default void presence(JSONObject message) { } default void presence(JSONObject message) { }
default void announcement(JSONObject message) { } default void announcement(JSONObject message) { }
default void announcementFailed(String requestId) { } 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. // Serializes all peer creation/API calls/destruction, including across activities.
private static final ExecutorService RTC = Executors.newSingleThreadExecutor(r -> new Thread(r, "CrossDesk RTC")); private static final ExecutorService RTC = Executors.newSingleThreadExecutor(r -> new Thread(r, "CrossDesk RTC"));
@@ -49,6 +50,7 @@ final class NativeSession implements AutoCloseable {
private long handle; private long handle;
private boolean signalReady, joining, started; private boolean signalReady, joining, started;
private volatile boolean connected, supportsTextInput; private volatile boolean connected, supportsTextInput;
final RemotePrivacyScreen privacyScreen = new RemotePrivacyScreen();
private int signalState; private int signalState;
private volatile long signalGeneration; private volatile long signalGeneration;
private boolean presenceRequested; private boolean presenceRequested;
@@ -63,12 +65,14 @@ final class NativeSession implements AutoCloseable {
private final RemoteNetworkStatistics networkStatistics=new RemoteNetworkStatistics(); private final RemoteNetworkStatistics networkStatistics=new RemoteNetworkStatistics();
private final Runnable statisticsTick=new Runnable(){public void run(){ private final Runnable statisticsTick=new Runnable(){public void run(){
if(closed)return; if(closed)return;
privacyScreen.tick(android.os.SystemClock.elapsedRealtime());
listener.privacyChanged();
listener.statistics(networkStatistics.snapshot(RemoteNetworkStatistics.now())); listener.statistics(networkStatistics.snapshot(RemoteNetworkStatistics.now()));
if(!closed)main.postDelayed(this,1000); if(!closed)main.postDelayed(this,1000);
}}; }};
private final Runnable timeout = () -> fail(remote.isEmpty() private final Runnable timeout = () -> fail(remote.isEmpty()
? "服务器连接超时,请检查服务器地址和网络" ? L10n.tr("服务器连接超时,请检查服务器地址和网络")
: "连接超时,请检查设备 ID、服务器及远端在线状态"); : L10n.tr("连接超时,请检查设备 ID、服务器及远端在线状态"));
NativeSession(Context context, String host, int port, String remote, String password, Listener listener) { NativeSession(Context context, String host, int port, String remote, String password, Listener listener) {
this.context = context.getApplicationContext(); this.listener = listener; this.context = context.getApplicationContext(); this.listener = listener;
@@ -88,10 +92,10 @@ final class NativeSession implements AutoCloseable {
File logs = new File(context.getNoBackupFilesDir(), "logs"); File logs = new File(context.getNoBackupFilesDir(), "logs");
if (!logs.isDirectory() && !logs.mkdirs()) throw new IllegalStateException("Log directory unavailable"); if (!logs.isDirectory() && !logs.mkdirs()) throw new IllegalStateException("Log directory unavailable");
handle = nCreate(this, host, port, identity, logs.getAbsolutePath(), certificates.getAbsolutePath()); 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()); nFileDirectory(handle,new File(documents.directory,"received").getAbsolutePath());
if (surface != null && surface.isValid()) nSurface(handle, surface); 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. */ /** Promote the already registered identity peer to an outgoing desktop session. */
@@ -143,7 +147,7 @@ final class NativeSession implements AutoCloseable {
if (handle == 0 || joining || remote.isEmpty() || !signalReady || identity.isEmpty()) return; if (handle == 0 || joining || remote.isEmpty() || !signalReady || identity.isEmpty()) return;
joining = true; joining = true;
String base = identity.split("@", 2)[0]; String base = identity.split("@", 2)[0];
if (!nConnect(handle, base, remote, password)) fail("无法发起远程连接"); if (!nConnect(handle, base, remote, password)) fail(L10n.tr("无法发起远程连接"));
password = ""; password = "";
} }
// Called on arbitrary native callback threads; borrowed payloads were copied by JNI. // Called on arbitrary native callback threads; borrowed payloads were copied by JNI.
@@ -171,10 +175,10 @@ final class NativeSession implements AutoCloseable {
publishSignaling(); publishSignaling();
tryJoin(); tryJoin();
} }
if (status == 6) fail("服务器 TLS 证书校验失败"); if (status == 6) fail(L10n.tr("服务器 TLS 证书校验失败"));
else if (status == 2) fail("服务器登录失败,请检查服务器地址和网络"); else if (status == 2) fail(L10n.tr("服务器登录失败,请检查服务器地址和网络"));
else if (status >= 3 && connected) fail("信令连接中断,请重新连接"); else if (status >= 3 && connected) fail(L10n.tr("信令连接中断,请重新连接"));
else if (status == 0 || status == 4) ui(() -> listener.status("正在连接信令服务器…")); else if (status == 0 || status == 4) ui(() -> listener.status(L10n.tr("正在连接信令服务器…")));
} else if (type == 2) { } else if (type == 2) {
int status = event.getInt("status"); int status = event.getInt("status");
if (status == 1) { if (status == 1) {
@@ -183,17 +187,31 @@ final class NativeSession implements AutoCloseable {
audio = new AudioPlayer(); audio = new AudioPlayer();
nControl(handle, 2, 1); nControl(handle, 2, 1);
ui(() -> { listener.connected(); if(!closed){statisticsTick.run();fileTick.run();} }); 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 if (status == 6) ui(() -> { listener.passwordRejected(); close(); });
else { else {
String[] reasons = {"", "", "", "连接已断开", "连接失败", "远端已关闭连接", String[] reasons = {"", "", "", L10n.tr("连接已断开"), L10n.tr("连接失败"), L10n.tr("远端已关闭连接"),
"远端密码错误", "找不到该设备 ID", "远端当前不可用"}; L10n.tr("远端密码错误"), L10n.tr("找不到该设备 ID"), L10n.tr("远端当前不可用")};
fail(status >= 0 && status < reasons.length ? reasons[status] : "连接已结束"); fail(status >= 0 && status < reasons.length ? reasons[status] : L10n.tr("连接已结束"));
} }
} else if (type == 4) { } 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"); JSONObject info=event.optJSONObject("host_info");
if(info!=null)supportsTextInput=info.optBoolean("supports_text_input",false); if(info!=null)supportsTextInput=info.optBoolean("supports_text_input",false);
ui(() -> listener.data(event)); 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) { else if (type == 5) {
int width = event.getInt("width"), height = event.getInt("height"); int width = event.getInt("width"), height = event.getInt("height");
@@ -210,7 +228,7 @@ final class NativeSession implements AutoCloseable {
listener.fileTransfer(name,progress,sending,path.isEmpty()?null:new File(path));}); listener.fileTransfer(name,progress,sending,path.isEmpty()?null:new File(path));});
} }
} catch (Exception error) { } catch (Exception error) {
if (type == 3) fail("设备身份保存失败,请检查安全存储"); if (type == 3) fail(L10n.tr("设备身份保存失败,请检查安全存储"));
} }
}); });
} }
@@ -266,6 +284,18 @@ final class NativeSession implements AutoCloseable {
if (type == 2 && player != null) player.setEnabled(value != 0); if (type == 2 && player != null) player.setEnabled(value != 0);
execute(() -> { if (connected){nControl(handle, type, value);if(type==4)networkStatistics.resetVideo();} }); 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) { void clipboard(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8); byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
if (bytes.length == 0 || bytes.length > 128 * 1024) return; if (bytes.length == 0 || bytes.length > 128 * 1024) return;
@@ -273,24 +303,24 @@ final class NativeSession implements AutoCloseable {
} }
void secureAttention(){execute(()->{ void secureAttention(){execute(()->{
boolean sent=connected&&nSecureAttention(handle); 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){ void sendFile(android.net.Uri uri){
if(closed)return; if(closed)return;
if(sendingFile){listener.controlFeedback("请等待当前文件发送完成");return;} if(sendingFile){listener.controlFeedback(L10n.tr("请等待当前文件发送完成"));return;}
sendingFile=true;listener.fileTransfer("正在读取所选文件…",0,true,null); sendingFile=true;listener.fileTransfer(L10n.tr("正在读取所选文件…"),0,true,null);
documents.prepare(uri,new SessionDocuments.Prepared(){ documents.prepare(uri,new SessionDocuments.Prepared(){
public void ready(File file,String name){execute(()->{ public void ready(File file,String name){execute(()->{
if(!connected||!nSendFile(handle,file.getAbsolutePath(),name.getBytes(StandardCharsets.UTF_8))){ if(!connected||!nSendFile(handle,file.getAbsolutePath(),name.getBytes(StandardCharsets.UTF_8))){
file.delete();ui(()->{sendingFile=false;listener.fileTransfer(name,-1,true,null);}); 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){ void saveFile(File file,android.net.Uri destination){
if(closed)return; 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() { @Override public void close() {
if (closed) return; if (closed) return;
@@ -319,6 +349,7 @@ final class NativeSession implements AutoCloseable {
private static native boolean nPresenceRequest(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 void nClipboard(long handle, byte[] data);
private static native boolean nSecureAttention(long handle); 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 void nFileDirectory(long handle,String directory);
private static native boolean nSendFile(long handle,String path,byte[] name); private static native boolean nSendFile(long handle,String path,byte[] name);
private static native boolean nPumpFiles(long handle); 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){ 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); 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); 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); 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); 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); 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("同意并继续",()->{ TextView agree=ui.primary(L10n.tr("同意并继续"),()->{
if(!hasReadPolicy.isChecked()||!policy.available)return; if(!hasReadPolicy.isChecked()||!policy.available)return;
dismiss();accepted.run(); dismiss();accepted.run();
});agree.setEnabled(false);agree.setAlpha(.45f);actions.addView(agree,ui.size(-1,48)); });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);}); 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);} setContentView(sheet);Window window=getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setDimAmount(.22f);}
} }
@Override public void show(){ @Override public void show(){
@@ -13,12 +13,14 @@ final class PrivacyPolicyDocument {
final boolean available; final boolean available;
final String text; final String text;
PrivacyPolicyDocument(Context context){this(load(context));} PrivacyPolicyDocument(Context context){this(load(context),L10n.isChinese());}
PrivacyPolicyDocument(String document){ PrivacyPolicyDocument(String document){this(document,true);}
private PrivacyPolicyDocument(String document,boolean chinese){
available=document!=null&&!document.trim().isEmpty(); 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); 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){ private static String load(Context context){
try{return LicenseCatalog.read(context,"PRIVACY.md");}catch(IOException error){return null;} 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); 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")){ for(String paragraph:text.split("\n\n")){
paragraph=paragraph.trim();if(paragraph.isEmpty()||paragraph.equals("---"))continue; 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("**",""); 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); TextView label=ui.text(value,date?12:16,heading);label.setTextIsSelectable(true);label.setLineSpacing(ui.dp(4),1);
if(date)label.setTextColor(SECONDARY); if(date)label.setTextColor(SECONDARY);
@@ -39,12 +39,12 @@ final class RemoteKeyboard extends LinearLayout {
header.addView(editor,new FrameLayout.LayoutParams(1,1,Gravity.BOTTOM)); 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); 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)); header.addView(toolbar,new FrameLayout.LayoutParams(-1,-1));
switchButton=button("电脑键盘",this::switchKeyboard);switchButton.setPadding(ui.dp(8),0,ui.dp(8),0); switchButton=button(L10n.tr("电脑键盘"),this::switchKeyboard);switchButton.setPadding(ui.dp(8),0,ui.dp(8),0);
toolbar.addView(switchButton,new LayoutParams(-2,-1)); toolbar.addView(switchButton,new LayoutParams(-2,-1));
LinearLayout pages=ui.row();pages.setGravity(Gravity.CENTER);toolbar.addView(pages,new LayoutParams(0,-1,1)); LinearLayout pages=ui.row();pages.setGravity(Gravity.CENTER);toolbar.addView(pages,new LayoutParams(0,-1,1));
lettersButton=button("字母数字",()->page(false));functionsButton=button("符号功能",()->page(true)); 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))); 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("收起键盘");hide.setClickable(true);hide.setFocusable(true); 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.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))); 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)); keys=ui.column();keys.setPadding(ui.dp(3),ui.dp(4),ui.dp(3),ui.dp(4));addView(keys,new LayoutParams(-1,0,1));
@@ -157,7 +157,7 @@ final class RemoteKeyboard extends LinearLayout {
} }
private void render(){ private void render(){
setBackgroundColor(dark()?0xFF202124:0xFFE8EAED); setBackgroundColor(dark()?0xFF202124:0xFFE8EAED);
switchButton.setText(computer?"系统键盘":"电脑键盘");switchButton.setTextColor(foreground()); switchButton.setText(computer?L10n.tr("系统键盘"):L10n.tr("电脑键盘"));switchButton.setTextColor(foreground());
lettersButton.setVisibility(computer?VISIBLE:GONE);functionsButton.setVisibility(computer?VISIBLE:GONE); lettersButton.setVisibility(computer?VISIBLE:GONE);functionsButton.setVisibility(computer?VISIBLE:GONE);
for(TextView view:new TextView[]{lettersButton,functionsButton}){ for(TextView view:new TextView[]{lettersButton,functionsButton}){
boolean selected=(view==functionsButton)==functions;view.setSelected(selected); boolean selected=(view==functionsButton)==functions;view.setSelected(selected);
@@ -185,12 +185,12 @@ final class RemoteKeyboard extends LinearLayout {
LinearLayout line=ui.row();line.setGravity(Gravity.CENTER); LinearLayout line=ui.row();line.setGravity(Gravity.CENTER);
for(String value:values){ for(String value:values){
boolean shift=modifiers.contains(16);String shown=shift&&value.length()==1?shifted(value):value; boolean shift=modifiers.contains(16);String shown=shift&&value.length()==1?shifted(value):value;
TextView key=button(shown,()->key(value));key.setPadding(0,0,0,0); TextView key=button(L10n.tr(shown),()->key(value));key.setPadding(0,0,0,0);
key.setTextSize(value.length()>1?12:18);key.setSingleLine(true); 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 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); 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); 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("退格"); 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. // Both penultimate rows allocate exactly 1/8 to Shift and Backspace.
boolean penultimate=values[0].equals("⇧");float weight=value.equals("空格")?4:1; boolean penultimate=values[0].equals("⇧");float weight=value.equals("空格")?4:1;
if(penultimate)weight=value.equals("⇧")||value.equals("⌫")?1:(6f/(values.length-2)); if(penultimate)weight=value.equals("⇧")||value.equals("⌫")?1:(6f/(values.length-2));
@@ -12,31 +12,32 @@ final class RemoteNetworkStatistics {
} }
static final class Report { static final class Report {
final Traffic[] traffic=new Traffic[4];final int mode;final boolean srtp;final double rtt; final Traffic[] traffic=new Traffic[4];final int mode;final boolean srtp;final double rtt;
final String decoder;
Report(JSONObject json){ Report(JSONObject json){
String[] keys={"video","audio","data","total"}; 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);} 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); mode=json.optInt("mode",2);srtp=json.optBoolean("srtp");rtt=json.optDouble("rtt",Double.NaN);
decoder=json.optString("decoder","");
} }
} }
static final class Snapshot { static final class Snapshot {
final Report report;final int fps,width,height;final double latency,rtt; 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;} final String remoteEncoder,localDecoder;
String mode(){return report==null?"—":report.mode==0?"P2P 直连":report.mode==1?"TURN 中继":"—";} 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 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 { private static final class Frame {
final double time,latency;Frame(double time,double latency){this.time=time;this.latency=latency;} final double time,latency;Frame(double time,double latency){this.time=time;this.latency=latency;}
} }
private Report report; private Report report;
private String remoteEncoder="";
private double reportTime,rttTime,averageRtt=Double.NaN; private double reportTime,rttTime,averageRtt=Double.NaN;
private long lastFrameId; private long lastFrameId;
private int width,height; private int width,height;
private final ArrayDeque<Frame> frames=new ArrayDeque<>(); private final ArrayDeque<Frame> frames=new ArrayDeque<>();
static double now(){return System.nanoTime()/1_000_000_000.0;} 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){ synchronized void receive(Report value,double now){
report=value;reportTime=now; report=value;reportTime=now;
if(!Double.isFinite(value.rtt)||value.rtt<0||value.rtt>2000)return; if(!Double.isFinite(value.rtt)||value.rtt<0||value.rtt>2000)return;
@@ -53,7 +54,7 @@ final class RemoteNetworkStatistics {
synchronized Snapshot snapshot(double now){ synchronized Snapshot snapshot(double now){
prune(now);double total=0;int count=0; prune(now);double total=0;int count=0;
for(Frame frame:frames)if(Double.isFinite(frame.latency)){total+=frame.latency;count++;} 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();} 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";} 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";}
@@ -2,54 +2,27 @@ package cn.crossdesk.mobile;
import android.app.Activity; import android.app.Activity;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.view.OrientationEventListener;
/** Holds the initial landscape until the phone itself has been turned sideways. */ /** Applies the selected orientation policy for the whole remote session. */
final class RemoteOrientation { final class RemoteOrientation {
private final Activity activity; private final Activity activity;
private final OrientationEventListener sensor; private boolean autoLandscape;
private boolean waitingForLandscape, resumed;
RemoteOrientation(Activity activity) { RemoteOrientation(Activity activity) {
this.activity = activity; this.activity = activity;
sensor = new OrientationEventListener(activity) {
@Override public void onOrientationChanged(int degrees) {
if (!resumed || !waitingForLandscape || activity.isInPictureInPictureMode()) return;
// Use physical orientation, never the window rotated by our own request.
// Ignore flat/unknown poses and keep a margin away from the diagonal.
if ((degrees >= 60 && degrees <= 120) || (degrees >= 240 && degrees <= 300)) {
followDevice();
}
}
};
} }
void begin(boolean autoLandscape) { void begin(boolean autoLandscape, boolean fullScreenPhone) {
sensor.disable(); this.autoLandscape = autoLandscape;
waitingForLandscape = autoLandscape && sensor.canDetectOrientation(); update(fullScreenPhone);
apply();
if (resumed && waitingForLandscape) sensor.enable();
} }
boolean waitingForLandscape() { return waitingForLandscape; } boolean autoLandscape() { return autoLandscape; }
void resume() { void end() { autoLandscape = false; }
resumed = true;
if (waitingForLandscape && !activity.isInPictureInPictureMode()) sensor.enable();
}
void pause() { resumed = false; sensor.disable(); } void update(boolean fullScreenPhone) {
int orientation = autoLandscape && fullScreenPhone
void end() { waitingForLandscape = false; sensor.disable(); }
void followDevice() {
waitingForLandscape = false;
sensor.disable();
apply();
}
private void apply() {
int orientation = waitingForLandscape
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
: ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; : ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
if (activity.getRequestedOrientation() != orientation) activity.setRequestedOrientation(orientation); if (activity.getRequestedOrientation() != orientation) activity.setRequestedOrientation(orientation);
@@ -127,7 +127,7 @@ final class RemotePreviewStore {
new AtomicFile(new File(directory,key+".jpg")).delete(); new AtomicFile(new File(directory,key+".jpg")).delete();
synchronized(this){ synchronized(this){
boolean saved=index.edit().putStringSet("files",new HashSet<>(files)).commit(); 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();
});changed(); });changed();
@@ -139,7 +139,7 @@ final class RemotePreviewStore {
try{deleteTree(directory);}catch(IOException error){success=false;} try{deleteTree(directory);}catch(IOException error){success=false;}
synchronized(this){ synchronized(this){
if(epoch!=generation)return; if(epoch!=generation)return;
cleaning=false;cleanupError=success?"":"部分预览图未能清除,请重试。"; cleaning=false;cleanupError=success?"":L10n.tr("部分预览图未能清除,请重试。");
} }
changed(); 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() { @Override public void onCreate() {
super.onCreate(); running = this; super.onCreate(); running = this;
getSystemService(NotificationManager.class).createNotificationChannel( 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 = getSystemService(PowerManager.class).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CrossDesk:RemoteSession");
wakeLock.setReferenceCounted(false); 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 (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; if (intent == null || !value.token.equals(intent.getStringExtra("session"))) return START_NOT_STICKY;
owned = value; 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 { try {
Notification notification = notification(); Notification notification = notification();
if (Build.VERSION.SDK_INT >= 34) startForeground(NOTIFICATION, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); 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(); update();
} catch (RuntimeException error) { } catch (RuntimeException error) {
Log.w("CrossDesk", "Background session foreground promotion failed", error); Log.w("CrossDesk", "Background session foreground promotion failed", error);
value.finish("后台保活未能启动,请保持应用在前台后重新连接"); value.finish(L10n.tr("后台保活未能启动,请保持应用在前台后重新连接"));
} }
return START_NOT_STICKY; 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); .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); 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) return new Notification.Builder(this, CHANNEL).setSmallIcon(R.drawable.ic_remote_session)
.setContentTitle("CrossDesk 远程连接") .setContentTitle(L10n.tr("CrossDesk 远程连接"))
.setContentText(owned.background ? "后台保持连接,点按返回远程桌面" : "已启用后台保活,切换应用可继续保持连接") .setContentText(owned.background ? L10n.tr("后台保持连接,点按返回远程桌面") : L10n.tr("已启用后台保活,切换应用可继续保持连接"))
.setContentIntent(open).setOngoing(true).setOnlyAlertOnce(true).setShowWhen(false) .setContentIntent(open).setOngoing(true).setOnlyAlertOnce(true).setShowWhen(false)
.setCategory(Notification.CATEGORY_SERVICE).setVisibility(Notification.VISIBILITY_PRIVATE) .setCategory(Notification.CATEGORY_SERVICE).setVisibility(Notification.VISIBILITY_PRIVATE)
.addAction(new Notification.Action.Builder(null, "返回", open).build()) .addAction(new Notification.Action.Builder(null, L10n.tr("返回"), open).build())
.addAction(new Notification.Action.Builder(null, "断开", stop).build()).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. @android.annotation.SuppressLint("WakelockTimeout") // Released on foreground return, disconnect, task removal and service destruction.
private void update() { private void update() {
@@ -96,14 +96,14 @@ public final class RemoteSessionService extends Service {
} }
private void releaseWakeLock() { if (wakeLock != null && wakeLock.isHeld()) wakeLock.release(); } private void releaseWakeLock() { if (wakeLock != null && wakeLock.isHeld()) wakeLock.release(); }
@Override public void onTaskRemoved(Intent rootIntent) { @Override public void onTaskRemoved(Intent rootIntent) {
if (current == owned && owned != null) owned.finish("远程任务已关闭"); if (current == owned && owned != null) owned.finish(L10n.tr("远程任务已关闭"));
stopSelf(); stopSelf();
} }
@Override public void onDestroy() { @Override public void onDestroy() {
if (running == this) running = null; if (running == this) running = null;
releaseWakeLock(); stopForeground(STOP_FOREGROUND_REMOVE); releaseWakeLock(); stopForeground(STOP_FOREGROUND_REMOVE);
BackgroundSession value = owned; 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(); super.onDestroy();
} }
} }
@@ -4,11 +4,11 @@ import org.json.JSONObject;
/** Same request/acknowledgement and rollback rules as the iOS controller. */ /** Same request/acknowledgement and rollback rules as the iOS controller. */
final class RemoteVideoSettings { final class RemoteVideoSettings {
static final String[] PREFERENCES={"帧率优先","画质优先","平衡"}; static String[] preferences(){return new String[]{L10n.tr("帧率优先"),L10n.tr("画质优先"),L10n.tr("平衡")};}
static final String[] DETAILS={ static String[] details(){return new String[]{
"优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。", L10n.tr("优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。"),
"始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。", L10n.tr("始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。"),
"兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。"}; L10n.tr("兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。")};}
static final class Values { static final class Values {
final int quality,frameRate,preference; final int quality,frameRate,preference;
Values(int quality,int frameRate,int preference){this.quality=quality;this.frameRate=frameRate;this.preference=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){ } }catch(org.json.JSONException ignored){ }
} }
void expire(long expiredId){if(pending&&expiredId==requestId){selection=applied;pending=false;failed=true;}} 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("调整失败,请重试。"):"";}
} }
@@ -104,7 +104,7 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
} }
return true; return true;
}); });
setContentDescription("远程桌面:轻触点击,拖动移动,长按右键,双指捏合缩放,放大后双指拖动画面,原始比例下双指上下滑动滚动"); setContentDescription(L10n.tr("远程桌面:轻触点击,拖动移动,长按右键,双指捏合缩放,放大后双指拖动画面,原始比例下双指上下滑动滚动"));
} }
void videoSize(int width, int height) { void videoSize(int width, int height) {
if (width <= 0 || height <= 0) return; if (width <= 0 || height <= 0) return;
@@ -244,7 +244,7 @@ final class RemoteVideoView extends FrameLayout implements SurfaceHolder.Callbac
private final Path arrow = new Path(); private final Path arrow = new Path();
private final float arrowWidth = 12, arrowHeight = 15; private final float arrowWidth = 12, arrowHeight = 15;
Cursor(Context context) { Cursor(Context context) {
super(context); setClickable(true); setContentDescription("远程桌面触控区域"); super(context); setClickable(true); setContentDescription(L10n.tr("远程桌面触控区域"));
// Match the iOS RemoteCursorArrowShape in density-independent units. // Match the iOS RemoteCursorArrowShape in density-independent units.
arrow.moveTo(arrowWidth*.08f,arrowHeight*.04f); arrow.moveTo(arrowWidth*.08f,arrowHeight*.04f);
arrow.lineTo(arrowWidth*.08f,arrowHeight*.78f); arrow.lineTo(arrowWidth*.08f,arrowHeight*.78f);
@@ -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); 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++){ for(int i=0;i<titles.length;i++){
final int index=i;TextView choice=ui.action(titles[i],MobileUi.INK,()->select(index)); 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)); addView(choice,new LayoutParams(0,height,1));
} }
} }
@@ -15,6 +15,7 @@ final class SessionControls extends FrameLayout {
default boolean textEdit(int deletions,String value){return false;} default boolean textEdit(int deletions,String value){return false;}
void videoSettings(int field,int value); void videoSettings(int field,int value);
void sendFile(); void saveFile(); void secureAttention(); void sendFile(); void saveFile(); void secureAttention();
default void togglePrivacy() { }
} }
private final MobileUi ui; private final MobileUi ui;
private final Actions actions; private final Actions actions;
@@ -35,7 +36,11 @@ final class SessionControls extends FrameLayout {
int height=Math.max(0,getRootView().getHeight()-visible.bottom); int height=Math.max(0,getRootView().getHeight()-visible.bottom);
keyboardInsets(height,height>ui.dp(80),0); keyboardInsets(height,height>ui.dp(80),0);
} }
private TextView networkLabel,videoFeedback; private TextView videoFeedback;
private RemotePrivacyScreen privacy=new RemotePrivacyScreen();
private View privacyButton;
private MobileUi.Icon privacyIcon;
private TextView privacyFeedback,privacyCaption;
private TextView transferLabel,saveFileButton; private TextView transferLabel,saveFileButton;
private String transferStatus=""; private String transferStatus="";
private boolean hasReceivedFile; private boolean hasReceivedFile;
@@ -47,21 +52,27 @@ final class SessionControls extends FrameLayout {
private LinearLayout[] trafficRows; private LinearLayout[] trafficRows;
private TextView[] networkDetails; private TextView[] networkDetails;
private View encryptionLock,encryptionOpen; private View encryptionLock,encryptionOpen;
private boolean muted,relative=true,orbPositioned; private boolean muted,relative=true,orbPositioned,secureAttentionVisible;
SessionControls(Context context,MobileUi ui,Actions actions){ SessionControls(Context context,MobileUi ui,Actions actions){
super(context);this.ui=ui;this.actions=actions; super(context);this.ui=ui;this.actions=actions;
keyboard=new RemoteKeyboard(context,ui,actions,()->{requestLayout();positionMouse();}); keyboard=new RemoteKeyboard(context,ui,actions,()->{requestLayout();positionMouse();});
addView(keyboard,new LayoutParams(-1,ui.dp(44),Gravity.BOTTOM)); addView(keyboard,new LayoutParams(-1,ui.dp(44),Gravity.BOTTOM));
// This container must remain transparent to remote touch outside its controls. // 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)); 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)); 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","展开远程控制菜单",()->{if(menu==null)openMenu("controls");else closeMenu();}); 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); 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)); ((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))); 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();}); 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 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) { void attachMouseBar(FrameLayout host, VirtualMouseBar bar) {
mouseHost=host;mouseBar=bar; mouseHost=host;mouseBar=bar;
// A direct, tightly bounded sibling of the video is essential: otherwise // A direct, tightly bounded sibling of the video is essential: otherwise
@@ -97,23 +108,24 @@ final class SessionControls extends FrameLayout {
} }
void setMuted(boolean value){muted=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");} 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 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 statistics(RemoteNetworkStatistics.Snapshot value){network=value;renderNetwork();}
void videoSettings(RemoteVideoSettings value){settings=value;renderVideoSettings();} 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();} void fileStatus(String status,boolean received){transferStatus=status;hasReceivedFile=received;renderFileStatus();}
private void renderFileStatus(){ private void renderFileStatus(){
if(transferLabel!=null){transferLabel.setText(transferStatus);transferLabel.setVisibility(transferStatus.isEmpty()?GONE:VISIBLE);} if(transferLabel!=null){transferLabel.setText(transferStatus);transferLabel.setVisibility(transferStatus.isEmpty()?GONE:VISIBLE);}
if(saveFileButton!=null)saveFileButton.setVisibility(hasReceivedFile?VISIBLE:GONE); 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){ private void openMenu(String page){
cancelMouseInput(); cancelMouseInput();
closeMenu();menuPage=page;shield=new View(getContext());shield.setOnClickListener(v->closeMenu());addView(shield,new LayoutParams(-1,-1)); 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)); 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(); LinearLayout header=ui.row();
if(!page.equals("controls"))header.addView(ui.iconButton("back","返回控制栏",()->openMenu("controls")),ui.size(28,32)); if(!page.equals("controls"))header.addView(ui.iconButton("back",L10n.tr("返回控制栏"),()->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); TextView title=ui.text(page.equals("controls")?L10n.tr("● 已连接"):page.equals("video")?L10n.tr("画面设置"):L10n.tr("网络状态"),14,true);
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); 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);} 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); 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")){ if(page.equals("network")){
@@ -121,26 +133,48 @@ final class SessionControls extends FrameLayout {
scroll.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET);body.setPadding(0,0,ui.dp(6),0); scroll.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET);body.setPadding(0,0,ui.dp(6),0);
} }
if(page.equals("controls")){ if(page.equals("controls")){
String[] labels={"键盘",mouseVisible?"收起鼠标":"虚拟鼠标",relative?"相对鼠标":"绝对鼠标","显示器","画面设置",muted?"静音":"声音","发送文件","网络状态","Ctrl+Alt+Del","剪贴板"}; 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"}; 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}; 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(); 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); 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); 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")){ }else if(page.equals("network")){
networkPage(body); networkPage(body);
}else{ }else{
settingRows.add(new SettingRow(body,0,"画面质量",new String[]{"低","中","高"},new int[]{0,1,2}));ui.gap(body,12); 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,"画面采集帧率",new String[]{"30 fps","60 fps"},new int[]{30,60}));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,"画面偏好",RemoteVideoSettings.PREFERENCES,new int[]{0,1,2}));renderVideoSettings(); 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)); 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("收起远程控制菜单"); addView(menu,new LayoutParams(panelWidth(getWidth()),panelHeight(getHeight())));orb.bringToFront();orb.setContentDescription(L10n.tr("收起远程控制菜单"));
positionMouse(); 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 { private final class SettingRow {
final int field;final int[] values;final SegmentedControl group; final int field;final int[] values;final SegmentedControl group;
SettingRow(LinearLayout body,int field,String title,String[] labels,int[] values){ SettingRow(LinearLayout body,int field,String title,String[] labels,int[] values){
@@ -149,7 +183,7 @@ final class SessionControls extends FrameLayout {
group=new SegmentedControl(ui,labels,selectionIndex(),settings.supported,index->actions.videoSettings(field,values[index])); 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); 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++){ 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)); button.setLayoutParams(new LinearLayout.LayoutParams(0,ui.dp(28),1));
} }
} }
@@ -163,15 +197,17 @@ final class SessionControls extends FrameLayout {
for(SettingRow row:settingRows)row.render(); for(SettingRow row:settingRows)row.render();
} }
private void networkPage(LinearLayout body){ private void networkPage(LinearLayout body){
trafficCells=new TextView[4][3];trafficRows=new LinearLayout[4];String[] names={"视频","音频","数据","合计"}; 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("接收");((TextView)heading.getChildAt(2)).setText("发送");((TextView)heading.getChildAt(3)).setText("丢包率"); 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<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); 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); ui.gap(body,10);ui.divider(body);ui.gap(body,10);
String[] titles={"帧率","分辨率","画面延时","连接延时(RTT)","连接方式","媒体加密"};networkDetails=new TextView[titles.length]; 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,new LinearLayout.LayoutParams(0,-2,1)); 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);}} 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(); renderNetwork();
} }
private LinearLayout trafficRow(LinearLayout body,String title,TextView[] cells,boolean total){ private LinearLayout trafficRow(LinearLayout body,String title,TextView[] cells,boolean total){
@@ -182,16 +218,16 @@ final class SessionControls extends FrameLayout {
return row; return row;
} }
private void renderNetwork(){ private void renderNetwork(){
if(networkLabel!=null)networkLabel.setText(network.summary());
if(trafficCells==null)return; if(trafficCells==null)return;
String[] names={"视频","音频","数据","合计"}; String[] names={L10n.tr("视频"),L10n.tr("音频"),L10n.tr("数据"),L10n.tr("合计")};
for(int i=0;i<4;i++){ for(int i=0;i<4;i++){
RemoteNetworkStatistics.Traffic traffic=network.report==null?null:network.report.traffic[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); 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 已启用":"未启用"}; 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};
for(int i=0;i<values.length;i++)networkDetails[i].setText(values[i]);networkDetails[5].setTextColor(network.report!=null&&network.report.srtp?GREEN:SECONDARY); // 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); 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;} private int controlColumns(){return getMeasuredHeight()<ui.dp(400)&&getMeasuredWidth()>=ui.dp(400)?4:3;}
@@ -29,11 +29,11 @@ final class VirtualMouseBar extends LinearLayout {
shell.setStroke(ui.dp(.5f), 0xFFD4DAE2); shell.setStroke(ui.dp(.5f), 0xFFD4DAE2);
setBackground(shell); setBackground(shell);
setMotionEventSplittingEnabled(true); setMotionEventSplittingEnabled(true);
keys[0] = key("鼠标左键,按住并拖动,松开结束拖拽", 0); keys[0] = key(L10n.tr("鼠标左键,按住并拖动,松开结束拖拽"), 0);
keys[1] = key("鼠标滚轮,上下滑动,可滑出按键范围", 1); keys[1] = key(L10n.tr("鼠标滚轮,上下滑动,可滑出按键范围"), 1);
keys[2] = key("鼠标右键,按住并拖动,松开结束拖拽", 2); keys[2] = key(L10n.tr("鼠标右键,按住并拖动,松开结束拖拽"), 2);
input.didCancel = this::resetTouches; input.didCancel = this::resetTouches;
setContentDescription("虚拟鼠标"); setContentDescription(L10n.tr("虚拟鼠标"));
} }
@SuppressLint("ClickableViewAccessibility") // Touch sends real down/up; performClick serves accessibility only. @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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>
@@ -10,6 +10,7 @@
#include "../../../libs/controller/include/mobile_controller_protocol.h" #include "../../../libs/controller/include/mobile_controller_protocol.h"
namespace crossdesk::android_controller { namespace crossdesk::android_controller {
using mobile_controller::SecureAttentionMessage; using mobile_controller::SecureAttentionMessage;
using mobile_controller::PrivacyMessage;
using mobile_controller::VideoSettingsMessage; using mobile_controller::VideoSettingsMessage;
using mobile_controller::NetworkReport; using mobile_controller::NetworkReport;
} }
+10 -1
View File
@@ -370,7 +370,8 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nConnect)(
} }
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv*, jclass, jlong handle) { extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv*, jclass, jlong handle) {
auto* s = From(handle); if (!s || !s->controller) return; 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); s->Send(kControlStream, action.to_json()); FreeRemoteAction(action);
} }
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nPointer)( extern "C" JNIEXPORT void JNICALL JNI_METHOD(nPointer)(
@@ -429,6 +430,14 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nSecureAttention)(JNIEnv*, jcla
const auto data = android_controller::SecureAttentionMessage(); const auto data = android_controller::SecureAttentionMessage();
return SendReliableDataFrame(s->controller, data.data(), data.size(), kControlStream) == 0; 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) { 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)); auto* s = From(handle); if (s) s->files.SetDirectory(String(env, directory));
} }
+2 -9
View File
@@ -11,7 +11,6 @@ ANDROID = Path(__file__).resolve().parents[1]
ROOT = ANDROID.parents[1] ROOT = ANDROID.parents[1]
SOURCE = ROOT / 'deps/submodules/minirtc' SOURCE = ROOT / 'deps/submodules/minirtc'
CACHE = ANDROID / '.native' CACHE = ANDROID / '.native'
PACKAGE_REVISION = 'eda39de3fb99b420c168f1ab9ab2d1791e11b662'
XMAKE_MIN_VERSION = (3, 1, 1) XMAKE_MIN_VERSION = (3, 1, 1)
@@ -38,18 +37,12 @@ def main():
raise SystemExit('Initialize the MiniRTC submodule before building.') raise SystemExit('Initialize the MiniRTC submodule before building.')
env = os.environ.copy() env = os.environ.copy()
env['XMAKE_PKG_INSTALLDIR'] = str(CACHE / 'packages') env['XMAKE_PKG_INSTALLDIR'] = str(CACHE / 'packages')
env['XMAKE_GLOBALDIR'] = str(CACHE / 'global')
env['ANDROID_NDK_HOME'] = str(ndk) env['ANDROID_NDK_HOME'] = str(ndk)
env['ANDROID_NDK_ROOT'] = str(ndk) env['ANDROID_NDK_ROOT'] = str(ndk)
env['NO_COLOR'] = '1' env['NO_COLOR'] = '1'
work = ANDROID / 'native' work = ANDROID / 'native'
repository = CACHE / 'global/.xmake/repositories/xmake-repo' # Use the desktop repository policy; Xmake manages recipe initialization
if not (repository / '.git').is_dir(): # and updates. Only installed Android packages/build products are isolated.
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)
subprocess.run([xmake, 'f', '-P', str(work), '-y', '-p', 'android', '-a', 'arm64-v8a', subprocess.run([xmake, 'f', '-P', str(work), '-y', '-p', 'android', '-a', 'arm64-v8a',
'-m', 'release', '--ndk=' + str(ndk), '--ndk_sdkver=26', '-m', 'release', '--ndk=' + str(ndk), '--ndk_sdkver=26',
'--runtimes=c++_static', '--policies=package.precompiled:n', '--runtimes=c++_static', '--policies=package.precompiled:n',
+1 -3
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Package existing reviewed notices for the Android runtime dependency set.""" """Package existing reviewed notices for the Android runtime dependency set."""
import json import json
import hashlib
import os import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
@@ -70,8 +69,7 @@ def write_pages(output, catalog, additional, revision, minirtc):
target = output / asset target = output / asset
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(document['text'], encoding='utf-8') target.write_text(document['text'], encoding='utf-8')
documents.append({'title': document['title'], 'asset': asset, documents.append({'title': document['title'], 'asset': asset})
'sha256': hashlib.sha256(document['text'].encode('utf-8')).hexdigest()})
component['documents'] = documents component['documents'] = documents
(output / 'ThirdPartyLicenses.json').write_text(json.dumps( (output / 'ThirdPartyLicenses.json').write_text(json.dumps(
{'schemaVersion': 1, 'components': components}, ensure_ascii=False, indent=2)+'\n', encoding='utf-8') {'schemaVersion': 1, 'components': components}, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
+1 -20
View File
@@ -2,7 +2,6 @@
"""Check the shipped native ABI, ELF load alignment and APK zip alignment.""" """Check the shipped native ABI, ELF load alignment and APK zip alignment."""
from pathlib import Path from pathlib import Path
import argparse import argparse
import hashlib
import json import json
import re import re
import struct import struct
@@ -40,33 +39,15 @@ def verify(path):
name, extra = struct.unpack('<HH', raw.read(4)) name, extra = struct.unpack('<HH', raw.read(4))
if (library.header_offset + 30 + name + extra) % 16384: if (library.header_offset + 30 + name + extra) % 16384:
raise ValueError('APK native library entry is not 16 KB aligned') 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(): if archive.read('assets/PRIVACY.md') != (Path(__file__).resolve().parents[3] / 'PRIVACY.md').read_bytes():
raise ValueError('Missing or outdated bundled privacy policy') raise ValueError('Missing or outdated bundled privacy policy')
catalog = json.loads(archive.read('assets/ThirdPartyLicenses.json'))
source = json.loads(archive.read('assets/SourceMetadata.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: if source['schemaVersion'] != 1 or len(source['revision']) != 40:
raise ValueError('Missing source revision') raise ValueError('Missing source revision')
for document in source['buildDocuments']: for document in source['buildDocuments']:
if not archive.read('assets/'+document['asset']): if not archive.read('assets/'+document['asset']):
raise ValueError('Missing source build instructions') 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__': if __name__ == '__main__':
+1 -1
View File
@@ -427,7 +427,7 @@ int main(int argc, char* argv[]) {
CSimpleIniA config; CSimpleIniA config;
config.SetUnicode(true); config.SetUnicode(true);
config.LoadFile((paths.GetCachePath() / "config.ini").string().c_str()); 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"); std::cout << console.Text("console_cli_help");
return 0; return 0;
} }
+18
View File
@@ -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
@@ -62,12 +62,19 @@ int ConfigCenter::Load() {
const long language_value = const long language_value =
ini_->GetLongValue(section_, "language", static_cast<long>(language_)); ini_->GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) || 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; language_ = LANGUAGE::ENGLISH;
} else { } else {
language_ = static_cast<LANGUAGE>(language_value); 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( const long screen_capture_method_value = ini_->GetLongValue(
section_, "screen_capture_method", section_, "screen_capture_method",
static_cast<long>(ScreenCaptureMethod::Auto)); static_cast<long>(ScreenCaptureMethod::Auto));
@@ -130,6 +137,10 @@ int ConfigCenter::Load() {
enable_daemon_ = ini_->GetBoolValue(section_, "enable_daemon", enable_daemon_); enable_daemon_ = ini_->GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_privacy_screen_.store( enable_privacy_screen_.store(
ini_->GetBoolValue(section_, "enable_privacy_screen", true)); 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( save_remote_previews_.store(
ini_->GetBoolValue(section_, "save_remote_previews", true)); ini_->GetBoolValue(section_, "save_remote_previews", true));
portable_service_prompt_suppressed_ = portable_service_prompt_suppressed_ =
@@ -201,6 +212,9 @@ int ConfigCenter::StoreValues(
int ConfigCenter::Save() { int ConfigCenter::Save() {
ini_->SetLongValue(section_, "language", static_cast<long>(language_)); 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", ini_->SetLongValue(section_, "screen_capture_method",
static_cast<long>(screen_capture_method_)); static_cast<long>(screen_capture_method_));
ini_->SetLongValue(section_, "video_encode_format", ini_->SetLongValue(section_, "video_encode_format",
@@ -222,6 +236,9 @@ int ConfigCenter::Save() {
ini_->SetBoolValue(section_, "enable_daemon", enable_daemon_); ini_->SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_->SetBoolValue(section_, "enable_privacy_screen", ini_->SetBoolValue(section_, "enable_privacy_screen",
enable_privacy_screen_.load()); 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", ini_->SetBoolValue(section_, "save_remote_previews",
save_remote_previews_.load()); save_remote_previews_.load());
ini_->SetBoolValue(section_, "portable_service_prompt_suppressed", ini_->SetBoolValue(section_, "portable_service_prompt_suppressed",
@@ -244,11 +261,39 @@ int ConfigCenter::Save() {
int ConfigCenter::SetLanguage(LANGUAGE language) { int ConfigCenter::SetLanguage(LANGUAGE language) {
const int value = static_cast<int>(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; language_ = language;
return 0; 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) { int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) { if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
return -1; return -1;
@@ -278,10 +323,7 @@ int ConfigCenter::SetTurnMode(TURN_MODE mode) {
return 0; return 0;
} }
int ConfigCenter::SetTurn(bool enable_turn, bool force_relay) { int ConfigCenter::SetForceRelay(bool force_relay) {
if (!enable_turn) {
return SetTurnMode(TURN_MODE::DISABLED);
}
if (!force_relay) { if (!force_relay) {
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP); return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
} }
@@ -371,6 +413,29 @@ int ConfigCenter::SetPrivacyScreen(bool enable_privacy_screen) {
return 0; 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) { int ConfigCenter::SetSaveRemotePreviews(bool enabled) {
if (StoreValues({{"save_remote_previews", enabled ? "true" : "false"}}) != 0) { if (StoreValues({{"save_remote_previews", enabled ? "true" : "false"}}) != 0) {
return -1; return -1;
@@ -409,6 +474,12 @@ int ConfigCenter::SetSponsorshipPromptDismissedVersion(
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } 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 { ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
return screen_capture_method_; return screen_capture_method_;
} }
@@ -421,10 +492,6 @@ ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
return turn_mode_; return turn_mode_;
} }
bool ConfigCenter::IsEnableTurn() const {
return turn_mode_ != TURN_MODE::DISABLED;
}
bool ConfigCenter::IsForceRelay() const { bool ConfigCenter::IsForceRelay() const {
return turn_mode_ == TURN_MODE::FORCE_UDP || turn_mode_ == TURN_MODE::FORCE_TCP; return turn_mode_ == TURN_MODE::FORCE_UDP || turn_mode_ == TURN_MODE::FORCE_TCP;
} }
+20 -4
View File
@@ -20,7 +20,12 @@ namespace crossdesk {
class ConfigCenter { class ConfigCenter {
public: 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 VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
enum class TURN_MODE { enum class TURN_MODE {
DISABLED = 0, DISABLED = 0,
@@ -35,10 +40,12 @@ class ConfigCenter {
// write config // write config
int SetLanguage(LANGUAGE language); int SetLanguage(LANGUAGE language);
int SetConnectionMode(CONNECTION_MODE mode);
int SetIpDirectEnabled(bool enabled);
int SetScreenCaptureMethod(ScreenCaptureMethod method); int SetScreenCaptureMethod(ScreenCaptureMethod method);
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format); int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
int SetTurnMode(TURN_MODE turn_mode); 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 SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port); int SetServerPort(int signal_server_port);
int SetServerEndpoint(const std::string& address, int port); int SetServerEndpoint(const std::string& address, int port);
@@ -46,6 +53,8 @@ class ConfigCenter {
int SetAutostart(bool enable_autostart); int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon); int SetDaemon(bool enable_daemon);
int SetPrivacyScreen(bool enable_privacy_screen); int SetPrivacyScreen(bool enable_privacy_screen);
int SetPrivacyLocalUnlock(bool allowed);
int SetLockOnDisconnect(bool enabled);
int SetSaveRemotePreviews(bool enabled); int SetSaveRemotePreviews(bool enabled);
int SetPortableServicePromptSuppressed(bool suppressed); int SetPortableServicePromptSuppressed(bool suppressed);
int SetSponsorshipPromptDismissedVersion(const std::string& version); int SetSponsorshipPromptDismissedVersion(const std::string& version);
@@ -54,10 +63,11 @@ class ConfigCenter {
// read config // read config
LANGUAGE GetLanguage() const; LANGUAGE GetLanguage() const;
CONNECTION_MODE GetConnectionMode() const;
bool IsIpDirectEnabled() const;
ScreenCaptureMethod GetScreenCaptureMethod() const; ScreenCaptureMethod GetScreenCaptureMethod() const;
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const; VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
TURN_MODE GetTurnMode() const; TURN_MODE GetTurnMode() const;
bool IsEnableTurn() const;
bool IsForceRelay() const; bool IsForceRelay() const;
bool IsEnableSrtp() const; bool IsEnableSrtp() const;
std::string GetSignalServerHost() const; std::string GetSignalServerHost() const;
@@ -68,6 +78,8 @@ class ConfigCenter {
bool IsEnableAutostart() const; bool IsEnableAutostart() const;
bool IsEnableDaemon() const; bool IsEnableDaemon() const;
bool IsEnablePrivacyScreen() const; bool IsEnablePrivacyScreen() const;
bool IsPrivacyLocalUnlockAllowed() const;
bool IsLockOnDisconnect() const;
bool IsSaveRemotePreviews() const; bool IsSaveRemotePreviews() const;
bool IsPortableServicePromptSuppressed() const; bool IsPortableServicePromptSuppressed() const;
std::string GetSponsorshipPromptDismissedVersion() const; std::string GetSponsorshipPromptDismissedVersion() const;
@@ -85,7 +97,9 @@ class ConfigCenter {
std::unique_ptr<IniStorage> ini_; std::unique_ptr<IniStorage> ini_;
const char* section_ = "Settings"; 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; ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264; VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP; TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
@@ -97,6 +111,8 @@ class ConfigCenter {
bool enable_autostart_ = false; bool enable_autostart_ = false;
bool enable_daemon_ = false; bool enable_daemon_ = false;
std::atomic<bool> enable_privacy_screen_{true}; 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}; std::atomic<bool> save_remote_previews_{true};
bool portable_service_prompt_suppressed_ = false; bool portable_service_prompt_suppressed_ = false;
std::string sponsorship_prompt_dismissed_version_; std::string sponsorship_prompt_dismissed_version_;
@@ -42,6 +42,7 @@
#include "features/gallery/gallery_window_state.h" #include "features/gallery/gallery_window_state.h"
#include "features/gallery/gallery_workspace.h" #include "features/gallery/gallery_workspace.h"
#include "features/gallery/preview_bandwidth.h" #include "features/gallery/preview_bandwidth.h"
#include "features/gallery/preview_resolution.h"
#include "filesystem_utf8.h" #include "filesystem_utf8.h"
#include "localization.h" #include "localization.h"
#include "platform.h" #include "platform.h"
@@ -70,6 +71,7 @@
#elif (defined(__linux__) && !defined(__OHOS__)) #elif (defined(__linux__) && !defined(__OHOS__))
#include <GL/gl.h> #include <GL/gl.h>
#include <X11/Xatom.h> #include <X11/Xatom.h>
#include <X11/Xcursor/Xcursor.h>
#include <X11/Xlib.h> #include <X11/Xlib.h>
#include <unistd.h> #include <unistd.h>
@@ -99,6 +101,41 @@ namespace {
using namespace std::chrono_literals; using namespace std::chrono_literals;
bool SupportsControlHandCursor([[maybe_unused]] const slint::Window& window) {
#if defined(_WIN32)
return LoadCursorW(nullptr, IDC_HAND) != nullptr;
#elif defined(__linux__) && !defined(__OHOS__)
// Match winit's pointer aliases, without using any move/grab fallback.
constexpr const char* names[] = {"pointer", "hand2", "hand1", "hand",
"pointing_hand"};
if (window.wayland_surface()) {
const char* theme = std::getenv("XCURSOR_THEME");
for (const char* name : names) {
if (auto* images = XcursorLibraryLoadImages(name, theme, 24)) {
const bool available = images->nimage > 0;
XcursorImagesDestroy(images);
if (available) return true;
}
}
return false;
}
std::unique_ptr<Display, decltype(&XCloseDisplay)> display{
XOpenDisplay(nullptr), XCloseDisplay};
if (!display) return false;
for (const char* name : names) {
const Cursor cursor = XcursorLibraryLoadCursor(display.get(), name);
if (cursor != None) {
XFreeCursor(display.get(), cursor);
return true;
}
}
return false;
#else
// AppKit supplies pointingHandCursor; other adapters resolve pointer.
return true;
#endif
}
std::optional<std::string> GenerateRandomPassword(std::string_view current) { std::optional<std::string> GenerateRandomPassword(std::string_view current) {
constexpr std::string_view alphabet = constexpr std::string_view alphabet =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -1034,6 +1071,9 @@ struct GuiApplication::SlintUi {
bool portable_service_dialog_initialized = false; bool portable_service_dialog_initialized = false;
bool sponsorship_prompt_pending = false; bool sponsorship_prompt_pending = false;
std::string recent_model_signature; std::string recent_model_signature;
gallery::PreviewResolutionDebouncer recent_preview_resolution;
gallery::PreviewResolution recent_preview_size;
std::unordered_map<std::string, slint::Image> recent_preview_images;
std::optional<std::string> recent_group_filter; std::optional<std::string> recent_group_filter;
std::vector<std::string> recent_filter_groups; std::vector<std::string> recent_filter_groups;
slint::Timer timer; slint::Timer timer;
@@ -1162,6 +1202,7 @@ int GuiApplication::Run() {
updater_ = std::make_unique<updater::Controller>(path_manager_->GetCachePath()); updater_ = std::make_unique<updater::Controller>(path_manager_->GetCachePath());
#endif #endif
InitializeUi(); InitializeUi();
InitializeLan();
#ifndef __OHOS__ #ifndef __OHOS__
ui_->activation_receiver = std::move(activation_receiver); ui_->activation_receiver = std::move(activation_receiver);
#endif #endif
@@ -1196,9 +1237,8 @@ void GuiApplication::InitializeSettings() {
settings_.LoadRecentConnectionAliases(); settings_.LoadRecentConnectionAliases();
localization_language_index_ = localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_); localization::detail::ClampLanguageIndex(language_button_value_);
language_button_value_ = localization_language_index_;
localization_language_ = localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_); static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
privacy_.SetText({ privacy_.SetText({
localization::privacy_screen_unlock_hint[localization_language_index_]}); localization::privacy_screen_unlock_hint[localization_language_index_]});
} }
@@ -1296,6 +1336,10 @@ void GuiApplication::InitializeUi() {
slint::set_xdg_app_id("crossdesk"); slint::set_xdg_app_id("crossdesk");
#endif #endif
ui_ = std::make_unique<SlintUi>(); ui_ = std::make_unique<SlintUi>();
for (const auto& window : {ui_->main, ui_->wall})
window->set_ip_direct_enabled(config_center_->IsIpDirectEnabled());
ui_->main->set_lan_mode(config_center_->GetConnectionMode() ==
ConfigCenter::CONNECTION_MODE::DIRECT_IP);
#if _WIN32 #if _WIN32
ui_->shutdown_guard = ui_->shutdown_guard =
std::make_unique<ShutdownSessionGuard>(windows_rdp_handoff_connected_); std::make_unique<ShutdownSessionGuard>(windows_rdp_handoff_connected_);
@@ -1436,7 +1480,6 @@ void GuiApplication::InitializeSystemTray() {
}; };
auto exit_app = [this] { auto exit_app = [this] {
exit_ = true; exit_ = true;
slint::quit_event_loop();
}; };
#if _WIN32 #if _WIN32
ui_->tray = std::make_unique<WinTray>( ui_->tray = std::make_unique<WinTray>(
@@ -1484,7 +1527,8 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
auto& main = ui_->Window(gallery_window); auto& main = ui_->Window(gallery_window);
main->set_language_index(language_button_value_); main->set_language_index(language_button_value_);
main->set_codec_index(video_encode_format_button_value_); main->set_codec_index(video_encode_format_button_value_);
main->set_turn_enabled(enable_turn_); main->set_ip_direct_enabled_draft(config_center_->IsIpDirectEnabled());
main->set_settings_save_error("");
main->set_force_relay_enabled(config_center_->IsForceRelay()); main->set_force_relay_enabled(config_center_->IsForceRelay());
main->set_self_hosted_enabled(enable_self_hosted_); main->set_self_hosted_enabled(enable_self_hosted_);
main->set_autostart_enabled(enable_autostart_); main->set_autostart_enabled(enable_autostart_);
@@ -1495,6 +1539,8 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
main->set_save_remote_previews(config_center_->IsSaveRemotePreviews()); main->set_save_remote_previews(config_center_->IsSaveRemotePreviews());
main->set_privacy_on_connect_enabled( main->set_privacy_on_connect_enabled(
config_center_->IsEnablePrivacyScreen()); config_center_->IsEnablePrivacyScreen());
main->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed());
main->set_lock_on_disconnect_enabled(config_center_->IsLockOnDisconnect());
#ifdef _WIN32 #ifdef _WIN32
main->set_capture_method_visible(true); main->set_capture_method_visible(true);
main->set_capture_method_index( main->set_capture_method_index(
@@ -1502,8 +1548,10 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
#endif #endif
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__)) #if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
main->set_privacy_setting_available(true); main->set_privacy_setting_available(true);
main->set_lock_on_disconnect_available(true);
#else #else
main->set_privacy_setting_available(false); main->set_privacy_setting_available(false);
main->set_lock_on_disconnect_available(false);
#endif #endif
#if defined(__OHOS__) #if defined(__OHOS__)
// The sandbox default is not the system desktop. Show the actual receive // The sandbox default is not the system desktop. Show the actual receive
@@ -1544,6 +1592,15 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
SendKeyInput(std::string(text), pressed, ctrl, alt, shift, meta); SendKeyInput(std::string(text), pressed, ctrl, alt, shift, meta);
}); });
main->on_open_gallery([this] { ShowGalleryWindow(); }); main->on_open_gallery([this] { ShowGalleryWindow(); });
main->on_connection_mode_changed([this, main](bool lan_mode) {
const auto mode = lan_mode ? ConfigCenter::CONNECTION_MODE::DIRECT_IP
: ConfigCenter::CONNECTION_MODE::DEVICE_ID;
if (config_center_->SetConnectionMode(mode) != 0) {
LOG_WARN("Could not save connection mode preference");
main->set_offline_warning(UiText(
localization::Literal("无法保存连接模式,下次启动可能不会恢复当前模式。", localization_language_index_)));
}
});
main->on_toggle_gallery_fullscreen( main->on_toggle_gallery_fullscreen(
[this] { ToggleGalleryFullscreen(); }); [this] { ToggleGalleryFullscreen(); });
main->on_gallery_action( main->on_gallery_action(
@@ -1678,8 +1735,19 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
show_password_ = !show_password_; show_password_ = !show_password_;
main->set_password_visible(show_password_); main->set_password_visible(show_password_);
}); });
main->on_reset_password([this](slint::SharedString value) { main->on_reset_password([this, main](slint::SharedString value) {
return RequestPasswordChange(std::string(value)); const std::string password(value);
if (!main->get_lan_mode()) return RequestPasswordChange(password);
if (!lan::ValidPassword(password)) return false;
if (!LanAvailable()) return true; // The initialization error is shown on the main view.
try {
lan_error_.clear();
lan_->SetPassword(password);
main->set_lan_password_pending(lan_->password_pending());
} catch (const std::exception& error) {
lan_error_ = error.what();
}
return true;
}); });
main->on_refresh_password([this] { main->on_refresh_password([this] {
const auto password = GenerateRandomPassword(password_saved_); const auto password = GenerateRandomPassword(password_saved_);
@@ -1691,6 +1759,106 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
} }
RequestPasswordChange(*password); RequestPasswordChange(*password);
}); });
main->on_lan_select_address([this, main](slint::SharedString address) {
if (!LanAvailable()) {
main->set_lan_endpoint_error(UiText(lan_error_.empty() ? "Busy" : lan_error_));
return false;
}
try {
lan_->identity().SelectAddress(std::string(address));
const auto port =
UiText(std::to_string(lan_->identity().PortFor(std::string(address))));
main->set_lan_listener_ip(address);
main->set_lan_listener_port(port);
main->set_lan_local_port(port);
main->set_lan_port_dirty(false);
main->set_lan_endpoint_error("");
return true;
} catch (const std::exception& error) {
main->set_lan_endpoint_error(UiText(error.what()));
return false;
}
});
main->on_lan_configure([this, main](slint::SharedString address,
slint::SharedString port) {
if (!LanAvailable()) {
main->set_lan_endpoint_error(UiText(lan_error_.empty() ? "Busy" : lan_error_));
return false;
}
try {
const auto port_number = lan::ParsePort(std::string(port));
lan_->Configure(std::string(address), port_number);
const auto normalized_port = UiText(std::to_string(port_number));
main->set_lan_listener_ip(address);
main->set_lan_listener_port(normalized_port);
main->set_lan_local_port(normalized_port);
main->set_lan_port_dirty(false);
lan_error_.clear();
main->set_lan_endpoint_error("");
return true;
} catch (const std::exception& error) {
main->set_lan_endpoint_error(UiText(error.what()));
return false;
}
});
main->on_lan_reset_password([this] {
if (LanAvailable()) {
lan_error_.clear();
lan_->ResetPassword();
}
});
main->on_lan_copy_text([this](slint::SharedString value) {
if (!SDL_SetClipboardText(std::string(value).c_str())) {
lan_error_ = "Cannot copy text";
return false;
}
return true;
});
main->on_lan_prepare_connect([this, main](slint::SharedString endpoint) {
try {
const auto [address, port] = lan::ParseEndpoint(Trim(std::string(endpoint)), 52888);
if (!LanAvailable()) return UiText(lan_error_.empty() ? "Busy" : lan_error_);
main->set_lan_remote_address(UiText(address));
main->set_lan_remote_port(UiText(std::to_string(port)));
main->set_lan_retry_after(lan_->RetryAfter(address, port));
// History is newest first. Match both address and port, and only use
// the latest matching record when an endpoint has been reused.
for (const auto& [_, recent] : recent_connections_) {
if (recent.lan.device_id.empty() || recent.lan.address != address ||
recent.lan.port != port)
continue;
if (recent.remember_password && lan::ValidPassword(recent.password)) {
main->set_lan_access_password(UiText(recent.password));
main->set_lan_remember_password(true);
}
break;
}
lan_error_.clear();
main->set_lan_message("");
return slint::SharedString{};
} catch (const std::exception& error) {
return UiText(error.what());
}
});
main->on_lan_cancel_connect([this, main] {
if (lan_ && lan_connecting_) lan_->Close(lan_connecting_);
lan_connecting_.reset();
main->set_lan_connecting(false);
lan_error_.clear();
main->set_lan_message("");
});
main->on_lan_connect([this, main](slint::SharedString address,
slint::SharedString port,
slint::SharedString password, bool remember) {
try {
ConnectLan(std::string(address), lan::ParsePort(std::string(port)),
std::string(password), remember);
} catch (const std::exception& error) {
lan_error_ = error.what();
}
main->set_lan_connecting(lan_connecting_ != nullptr);
main->set_lan_message(UiText(lan_error_));
});
main->on_connect_requested( main->on_connect_requested(
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); }); [this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
main->on_format_remote_id([](slint::SharedString value) { main->on_format_remote_id([](slint::SharedString value) {
@@ -1703,7 +1871,7 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
if (auto props = FindRemoteSession(remote_id); if (auto props = FindRemoteSession(remote_id);
props && props->connection_status_ == ConnectionStatus::Connected && props && props->connection_status_ == ConnectionStatus::Connected &&
!props->closing_) { !props->closing_) {
CloseStreamTab(remote_id); CloseStreamTab(props->remote_id_);
SyncMainWindow(); SyncMainWindow();
} }
}); });
@@ -2008,7 +2176,7 @@ void GuiApplication::BindStreamCallbacks() {
if (name != "Ctrl+Alt+Del" && name != "Win+L") return; if (name != "Ctrl+Alt+Del" && name != "Win+L") return;
{ {
std::lock_guard lock(props->remote_version_mutex_); std::lock_guard lock(props->remote_version_mutex_);
if (name == "Ctrl+Alt+Del" && props->remote_platform_ == HostPlatform::HarmonyOS) return; if (props->remote_platform_ != HostPlatform::Windows) return;
} }
RemoteAction action{}; RemoteAction action{};
action.type = ControlType::service_command; action.type = ControlType::service_command;
@@ -2019,9 +2187,9 @@ void GuiApplication::BindStreamCallbacks() {
SendReliableDataFrame(props->peer_, message.c_str(), message.size(), SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->control_data_label_.c_str()); props->control_data_label_.c_str());
}); });
stream->on_toggle_privacy([this] { const auto send_privacy = [this](bool relock) {
auto props = SelectedSession(); auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ || if (!props || !props->peer_ || (!relock && !props->control_mouse_) ||
props->connection_status_.load() != ConnectionStatus::Connected) return; props->connection_status_.load() != ConnectionStatus::Connected) return;
RemoteAction action{}; RemoteAction action{};
action.type = ControlType::privacy_command; action.type = ControlType::privacy_command;
@@ -2032,9 +2200,12 @@ void GuiApplication::BindStreamCallbacks() {
const auto controls = GetPrivacyControlState( const auto controls = GetPrivacyControlState(
status, props->privacy_status_received_, props->privacy_command_pending_, status, props->privacy_status_received_, props->privacy_command_pending_,
now - props->privacy_status_tick_, now - props->privacy_command_tick_); now - props->privacy_status_tick_, now - props->privacy_command_tick_);
if (!controls.can_toggle) return; const bool can_lock = props->privacy_status_received_ && status.supported &&
action.pc.flag = controls.active ? PrivacyCommandFlag::disable now - props->privacy_status_tick_ <= 5000 &&
: PrivacyCommandFlag::enable; (!props->privacy_command_pending_ || now - props->privacy_command_tick_ >= 15000);
if (!props->privacy_desktop_available_ || !(relock ? can_lock : controls.can_toggle)) return;
action.pc.flag = relock ? PrivacyCommandFlag::lock
: controls.active ? PrivacyCommandFlag::disable : PrivacyCommandFlag::enable;
action.pc.block_local_input = true; action.pc.block_local_input = true;
props->privacy_command_pending_ = true; props->privacy_command_pending_ = true;
props->privacy_command_tick_ = now; props->privacy_command_tick_ = now;
@@ -2045,9 +2216,17 @@ void GuiApplication::BindStreamCallbacks() {
props->control_data_label_.c_str()) != 0) { props->control_data_label_.c_str()) != 0) {
std::lock_guard lock(props->privacy_status_mutex_); std::lock_guard lock(props->privacy_status_mutex_);
props->privacy_command_pending_ = false; props->privacy_command_pending_ = false;
props->privacy_status_received_ = false; if (!relock) props->privacy_status_received_ = false;
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_); LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
} }
};
stream->on_toggle_privacy([send_privacy] { send_privacy(false); });
stream->on_lock_privacy([send_privacy] { send_privacy(true); });
stream->on_dismiss_privacy_unlock([this] {
if (auto props = SelectedSession()) {
std::lock_guard lock(props->privacy_status_mutex_);
props->privacy_unlock_notice_ = false;
}
}); });
stream->on_toggle_mouse_control([this] { stream->on_toggle_mouse_control([this] {
auto props = SelectedSession(); auto props = SelectedSession();
@@ -2266,9 +2445,101 @@ void GuiApplication::BindServerCallbacks() {
void GuiApplication::Tick() { void GuiApplication::Tick() {
if (exit_) { if (exit_) {
slint::quit_event_loop(); BeginShutdown();
if (ShutdownDrained()) slint::quit_event_loop();
return; return;
} }
TickLan();
if (LanAvailable()) {
const auto address = lan_->identity().settings().value("address", "");
ui_->main->set_lan_password_pending(lan_->password_pending());
ui_->main->set_lan_listening(lan_->listening());
if (lan_connecting_ && lan_connecting_->authorized) {
ui_->main->set_lan_open(false);
lan_connecting_.reset();
} else if (lan_connecting_ && lan_connecting_->closing) {
lan_connecting_.reset();
}
ui_->main->set_lan_connecting(lan_connecting_ != nullptr);
int retry_after = 0;
if (ui_->main->get_lan_open()) {
try {
retry_after = lan_->RetryAfter(
std::string(ui_->main->get_lan_remote_address()),
lan::ParsePort(std::string(ui_->main->get_lan_remote_port())));
} catch (const std::exception&) {
// An incomplete address has no cooldown; input validation handles it.
}
}
ui_->main->set_lan_retry_after(retry_after);
if (auto password = lan_->TakeNewPassword(); !password.empty())
ui_->main->set_lan_new_password(UiText(password));
const auto& available_interfaces = lan_->interfaces();
const auto previous = ui_->main->get_lan_interfaces();
bool changed = !previous || previous->row_count() != available_interfaces.size();
for (size_t i = 0; !changed && i < available_interfaces.size(); ++i)
changed = std::string_view(previous->row_data(i).value()) !=
available_interfaces[i].address;
if (changed) {
std::vector<slint::SharedString> addresses;
addresses.reserve(available_interfaces.size());
for (const auto& network_interface : available_interfaces)
addresses.push_back(UiText(network_interface.address));
ui_->main->set_lan_interfaces(
std::make_shared<slint::VectorModel<slint::SharedString>>(std::move(addresses)));
}
// Keep the current displayed address while it remains available, including
// when a previously missing preferred address returns during a port edit.
const auto previous_address = std::string(ui_->main->get_lan_listener_ip());
auto display_address = previous_address.empty() ? address : previous_address;
int interface_index = -1;
for (size_t i = 0; i < available_interfaces.size(); ++i)
if (available_interfaces[i].address == display_address) {
interface_index = static_cast<int>(i);
break;
}
if (interface_index < 0) {
interface_index = available_interfaces.empty() ? -1 : 0;
display_address = interface_index == 0
? available_interfaces.front().address : "";
}
const auto port =
UiText(std::to_string(lan_->identity().PortFor(display_address)));
if (previous_address != display_address) {
// A disappearing address must not transfer its unfinished port edit to
// another interface. Explicit selection commits the old edit first.
if (!previous_address.empty() && ui_->main->get_lan_port_dirty()) {
ui_->main->set_lan_access_warning_endpoint(UiText(previous_address));
ui_->main->set_lan_access_warning("Selected LAN interface is unavailable");
}
ui_->main->set_lan_port_dirty(false);
ui_->main->set_lan_endpoint_error("");
ui_->main->set_lan_local_port(port);
}
ui_->main->set_lan_listener_ip(UiText(display_address));
ui_->main->set_lan_listener_port(port);
ui_->main->set_lan_interface_index(interface_index);
ui_->main->set_lan_interface_draft_index(interface_index);
if (!ui_->main->get_lan_port_editing() && !ui_->main->get_lan_port_dirty())
ui_->main->set_lan_local_port(port);
ui_->main->set_lan_listener_error(UiText(
display_address.empty() ? "Selected LAN interface is unavailable"
: lan_->ListenerError(display_address)));
// Show failures one address at a time, without interrupting edits or another
// dialog. A working listener must not clear another address's error.
if (ui_->main->get_lan_mode() && ui_->main->get_can_show_sponsorship() &&
!ui_->main->get_lan_port_editing() && !ui_->main->get_lan_port_dirty()) {
if (auto issue = lan_->TakeListenerWarning()) {
ui_->main->set_lan_access_warning_endpoint(
UiText(issue->address + ":" + std::to_string(issue->port)));
ui_->main->set_lan_access_warning(UiText(issue->error));
}
}
}
const auto lan_message = lan_error_.rfind("ConnectionFailed", 0) == 0
? std::string("ConnectionFailed")
: lan_error_;
ui_->main->set_lan_message(UiText(lan_message));
#if _WIN32 #if _WIN32
if (ui_->shutdown_guard) { if (ui_->shutdown_guard) {
ui_->shutdown_guard->Update(ui_->shutdown_reason); ui_->shutdown_guard->Update(ui_->shutdown_reason);
@@ -2356,11 +2627,18 @@ void GuiApplication::Tick() {
HandleRecentConnections(); HandleRecentConnections();
if (was_reloading_recent_connections && !reload_recent_connections_) { if (was_reloading_recent_connections && !reload_recent_connections_) {
ui_->recent_model_signature.clear(); ui_->recent_model_signature.clear();
ui_->recent_preview_images.clear();
} }
HandleConnectionStatusChange(); HandleConnectionStatusChange();
HandlePendingPresenceProbe(); HandlePendingPresenceProbe();
HandlePresenceProbeTimeout(); HandlePresenceProbeTimeout();
HandlePrivacy(); HandlePrivacy();
if (privacy_settings_changed_) {
for (const auto& window : {ui_->main, ui_->wall})
window->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed());
privacy_settings_changed_ = false;
}
HandleScreenLock();
HandleWindowsServiceIntegration(); HandleWindowsServiceIntegration();
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__) #if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
SyncXWaylandWindowActivation(); SyncXWaylandWindowActivation();
@@ -2461,7 +2739,7 @@ void GuiApplication::ShareLocalCursorState() {
} }
} }
} }
if (!is_server_mode_ || !peer_ || connected_controllers.empty()) { if (!is_server_mode_ || connected_controllers.empty()) {
cursor_delivery_states_.clear(); cursor_delivery_states_.clear();
return; return;
} }
@@ -2587,9 +2865,8 @@ void GuiApplication::ShareLocalCursorState() {
action.type = ControlType::cursor_state; action.type = ControlType::cursor_state;
action.cs = outgoing; action.cs = outgoing;
const std::string message = action.to_json(); const std::string message = action.to_json();
const int result = SendReliableDataFrameToPeer( const int result = SendControllerData(recipient.remote_id, message.c_str(),
peer_, message.c_str(), message.size(), mouse_label_.c_str(), message.size(), mouse_label_.c_str());
recipient.remote_id.c_str(), recipient.remote_id.size());
if (result != 0) { if (result != 0) {
LOG_WARN("Send cursor state to [{}] failed, ret={}", LOG_WARN("Send cursor state to [{}] failed, ret={}",
recipient.remote_id, result); recipient.remote_id, result);
@@ -2832,13 +3109,9 @@ void GuiApplication::UpdateLocalization() {
return; return;
} }
#if _WIN32 #if _WIN32
// All views refer to static literals; selecting the text needs no allocation static std::vector<std::wstring> shutdown_reasons(localization::GetSupportedLanguages().size());
// and only runs when the language changes. Tick still checks connection state. ui_->shutdown_reason = localization::GetWindowsLocalizedLabel(
ui_->shutdown_reason = language == 0 language, "shutdown_reason", L"Remote session active.", shutdown_reasons);
? L"远程控制连接中,保留连接以确认或取消关机。"
: language == 2
? L"Удалённый сеанс активен. Соединение сохранено для подтверждения или отмены выключения."
: L"Remote session active. Keeping the connection available to confirm or cancel shutdown.";
#endif #endif
ui_localization::ApplyMainWindowStrings(ui_->main, language); ui_localization::ApplyMainWindowStrings(ui_->main, language);
ui_localization::ApplyStreamWindowStrings(ui_->main, language); ui_localization::ApplyStreamWindowStrings(ui_->main, language);
@@ -3001,15 +3274,15 @@ void GuiApplication::SyncMainWindow() {
// Keep this filter separate from DeskHub's current group, keyed by name so // Keep this filter separate from DeskHub's current group, keyed by name so
// adding/removing groups does not silently select a different one by index. // adding/removing groups does not silently select a different one by index.
const auto tr_group = [this](const char* zh, const char* en, const char* ru) { const auto tr_group = [this](const char* zh) {
return UiText(localization_language_index_ == 0 ? zh return UiText(localization::Literal(zh, localization_language_index_).c_str());
: localization_language_index_ == 2 ? ru : en);
}; };
std::vector<slint::SharedString> recent_groups{ std::vector<slint::SharedString> recent_groups{
tr_group("全部设备", "All devices", "Все устройства"), tr_group("全部设备"),
tr_group("未分组", "Ungrouped", "Без группы")}; tr_group("未分组")};
const bool lan_mode = ui_->main->get_lan_mode();
int recent_group_index = 0; int recent_group_index = 0;
if (ui_->recent_group_filter) { if (!lan_mode && ui_->recent_group_filter) {
if (ui_->recent_group_filter->empty()) recent_group_index = 1; if (ui_->recent_group_filter->empty()) recent_group_index = 1;
else { else {
const auto found = std::find(ui_->gallery.groups.begin(), const auto found = std::find(ui_->gallery.groups.begin(),
@@ -3046,12 +3319,26 @@ void GuiApplication::SyncMainWindow() {
return props && props->connection_status_ == ConnectionStatus::Connected && return props && props->connection_status_ == ConnectionStatus::Connected &&
!props->closing_; !props->closing_;
}; };
const auto preview_size = ui_->recent_preview_resolution.Update(
gallery::PreviewResolution::Viewport(
ui_->main->get_recent_preview_width(),
ui_->main->get_recent_preview_height(),
ui_->main->window().scale_factor()),
gallery::PreviewResolutionDebouncer::Clock::now());
const bool save_previews = config_center_->IsSaveRemotePreviews();
if (!(preview_size == ui_->recent_preview_size) || !save_previews) {
ui_->recent_preview_size = preview_size;
ui_->recent_preview_images.clear();
}
std::ostringstream signature_builder; std::ostringstream signature_builder;
signature_builder << config_center_->IsSaveRemotePreviews() << '\n' signature_builder << save_previews << '\n'
<< preview_size.width << ':' << preview_size.height << '\n'
<< lan_mode << '\n'
<< bool(ui_->recent_group_filter) << '\0' << bool(ui_->recent_group_filter) << '\0'
<< ui_->recent_group_filter.value_or("") << '\n'; << ui_->recent_group_filter.value_or("") << '\n';
for (const auto& [key, connection] : recent_connections_) { for (const auto& [key, connection] : recent_connections_) {
signature_builder << key << '\0' << connection.remote_id << '\0' signature_builder << key << '\0' << connection.remote_id << '\0'
<< connection.lan.address << ':' << connection.lan.port << '\0'
<< connection.remote_host_name << '\0' << connection.remote_host_name << '\0'
<< static_cast<int>(connection.platform) << '\0' << static_cast<int>(connection.platform) << '\0'
<< settings_.RecentConnectionDisplayName(connection) << settings_.RecentConnectionDisplayName(connection)
@@ -3072,7 +3359,9 @@ void GuiApplication::SyncMainWindow() {
std::vector<ui::RecentConnection> model; std::vector<ui::RecentConnection> model;
model.reserve(recent_connections_.size()); model.reserve(recent_connections_.size());
for (const auto& [_, connection] : recent_connections_) { for (const auto& [_, connection] : recent_connections_) {
if (!ui_->gallery.MatchesGroup(connection.remote_id, ui_->recent_group_filter)) if (lan_mode != !connection.lan.device_id.empty()) continue;
if (!lan_mode &&
!ui_->gallery.MatchesGroup(connection.remote_id, ui_->recent_group_filter))
continue; continue;
// Slint-generated scalar fields have no defaults. With no image, // Slint-generated scalar fields have no defaults. With no image,
// has_thumbnail must remain false so the remote platform is rendered. // has_thumbnail must remain false so the remote platform is rendered.
@@ -3082,21 +3371,34 @@ void GuiApplication::SyncMainWindow() {
item.group_names = UiText(group_labels[connection.remote_id]); item.group_names = UiText(group_labels[connection.remote_id]);
item.display_name = item.display_name =
UiText(settings_.RecentConnectionDisplayName(connection)); UiText(settings_.RecentConnectionDisplayName(connection));
item.online = device_presence_cache_.IsOnline(connection.remote_id); item.lan = !connection.lan.device_id.empty();
item.endpoint = UiText(item.lan ? connection.lan.address + ":" +
std::to_string(connection.lan.port)
: "");
item.online =
!item.lan && device_presence_cache_.IsOnline(connection.remote_id);
item.connected = is_connected(connection.remote_id); item.connected = is_connected(connection.remote_id);
item.platform = UiText(HostPlatformName(connection.platform)); item.platform = UiText(HostPlatformName(connection.platform));
std::vector<unsigned char> rgba; if (save_previews && !connection.image_path.empty()) {
int image_width = 0; const auto path = connection.image_path.string();
int image_height = 0; auto cached = ui_->recent_preview_images.find(path);
if (config_center_->IsSaveRemotePreviews() && if (cached == ui_->recent_preview_images.end()) {
!connection.image_path.empty() && std::vector<unsigned char> rgba;
thumbnail_->DecodeImage(connection.image_path, &rgba, &image_width, int image_width = 0, image_height = 0;
&image_height)) { if (thumbnail_->DecodeImage(connection.image_path, preview_size.width,
slint::SharedPixelBuffer<slint::Rgba8Pixel> pixels(image_width, preview_size.height, &rgba, &image_width,
image_height); &image_height)) {
std::memcpy(pixels.begin(), rgba.data(), rgba.size()); slint::SharedPixelBuffer<slint::Rgba8Pixel> pixels(image_width,
item.thumbnail = slint::Image(std::move(pixels)); image_height);
item.has_thumbnail = true; std::memcpy(pixels.begin(), rgba.data(), rgba.size());
cached = ui_->recent_preview_images.emplace(
path, slint::Image(std::move(pixels))).first;
}
}
if (cached != ui_->recent_preview_images.end()) {
item.thumbnail = cached->second;
item.has_thumbnail = true;
}
} }
model.push_back(std::move(item)); model.push_back(std::move(item));
} }
@@ -3163,7 +3465,9 @@ void GuiApplication::SyncConnectionDialog() {
} else if (auth.error == minirtc::JoinFailureKind::NotAuthenticated) { } else if (auth.error == minirtc::JoinFailureKind::NotAuthenticated) {
text = localization::connection_not_authenticated[localization_language_index_]; text = localization::connection_not_authenticated[localization_language_index_];
} else { } else {
text = ConnectionStatusText(status, localization_language_index_); text = props->lan_session_ && !props->lan_session_->error.empty()
? props->lan_session_->error
: ConnectionStatusText(status, localization_language_index_);
} }
if (auth.retry_after > 0) { if (auth.retry_after > 0) {
char retry_text[256]; char retry_text[256];
@@ -3308,9 +3612,9 @@ void GuiApplication::SyncXWaylandWindowActivation() {
#endif #endif
void GuiApplication::SetStreamKeyboardFocus(bool focused) { void GuiApplication::SetStreamKeyboardFocus(bool focused) {
// The local upgrade dialog owns input even while the native window is active. // Local dialogs own input even while the native window is active.
if (focused && ui_ && ui_->stream && if (focused && ui_ && ui_->stream &&
(*ui_->stream)->get_remote_update_visible()) { (*ui_->stream)->get_modal_visible()) {
focused = false; focused = false;
} }
if (focus_on_stream_window_ == focused) { if (focus_on_stream_window_ == focused) {
@@ -3372,6 +3676,8 @@ void GuiApplication::SyncStreamWindow() {
if (!has_sessions) need_to_create_stream_window_ = false; if (!has_sessions) need_to_create_stream_window_ = false;
if (need_to_create_stream_window_ && !ui_->stream) { if (need_to_create_stream_window_ && !ui_->stream) {
ui_->stream.emplace(ui::StreamWindow::create()); ui_->stream.emplace(ui::StreamWindow::create());
(*ui_->stream)->set_control_hand_cursor_supported(
SupportsControlHandCursor(ui_->main->window()));
#if (defined(__linux__) && !defined(__OHOS__)) #if (defined(__linux__) && !defined(__OHOS__))
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_); (*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
#endif #endif
@@ -3535,9 +3841,7 @@ void GuiApplication::SyncStreamWindow() {
} }
} }
(*ui_->stream)->set_remote_update_visible(remote_update_available); (*ui_->stream)->set_remote_update_visible(remote_update_available);
// All desktop clients use the same menu for a Harmony host. Windows SAS is (*ui_->stream)->set_secure_attention_supported(remote_platform == HostPlatform::Windows);
// not an ordinary key chord and has no public Harmony implementation.
(*ui_->stream)->set_secure_attention_supported(remote_platform != HostPlatform::HarmonyOS);
if (remote_update_available) { if (remote_update_available) {
SetStreamKeyboardFocus(false); SetStreamKeyboardFocus(false);
} }
@@ -3567,11 +3871,18 @@ void GuiApplication::SyncStreamWindow() {
now - props->privacy_status_tick_, now - props->privacy_command_tick_); now - props->privacy_status_tick_, now - props->privacy_command_tick_);
const bool connected = const bool connected =
props->connection_status_.load() == ConnectionStatus::Connected; props->connection_status_.load() == ConnectionStatus::Connected;
(*ui_->stream)->set_privacy_unlock_visible(connected && props->privacy_unlock_notice_);
(*ui_->stream)->set_privacy_can_lock(connected && props->privacy_desktop_available_ &&
props->privacy_status_received_ && status.supported &&
now - props->privacy_status_tick_ <= 5000 &&
(!props->privacy_command_pending_ || now - props->privacy_command_tick_ >= 15000));
(*ui_->stream)->set_privacy_active(controls.active); (*ui_->stream)->set_privacy_active(controls.active);
(*ui_->stream)->set_privacy_off(connected && controls.off); (*ui_->stream)->set_privacy_off(connected && controls.off);
(*ui_->stream)->set_privacy_unsupported(controls.unsupported); (*ui_->stream)->set_privacy_unsupported(controls.unsupported);
(*ui_->stream)->set_privacy_visible(props->privacy_desktop_available_);
(*ui_->stream)->set_privacy_can_toggle( (*ui_->stream)->set_privacy_can_toggle(
props->control_mouse_ && connected && controls.can_toggle); props->privacy_desktop_available_ && props->control_mouse_ &&
connected && controls.can_toggle);
const int language = localization_language_index_; const int language = localization_language_index_;
std::string text = !connected || !props->privacy_status_received_ std::string text = !connected || !props->privacy_status_received_
? localization::privacy_screen[language] ? localization::privacy_screen[language]
@@ -4260,6 +4571,7 @@ bool GuiApplication::ValidateSelfHostedSettingsFromUi(bool gallery_window) {
bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
const auto previous_server = GetSignalServerSettings(*config_center_); const auto previous_server = GetSignalServerSettings(*config_center_);
auto& main = ui_->Window(gallery_window); auto& main = ui_->Window(gallery_window);
main->set_settings_save_error("");
const bool has_endpoint = main->get_self_hosted_enabled() || const bool has_endpoint = main->get_self_hosted_enabled() ||
!std::string(main->get_server_host()).empty() || !std::string(main->get_server_host()).empty() ||
!std::string(main->get_server_port()).empty(); !std::string(main->get_server_port()).empty();
@@ -4281,9 +4593,7 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
// common save succeeds. Restore the previous wall file if it fails. // common save succeeds. Restore the previous wall file if it fails.
if (!wall_settings || (wall_changed && !updated_wall.Save(ui_->gallery_path))) { if (!wall_settings || (wall_changed && !updated_wall.Save(ui_->gallery_path))) {
main->set_gallery_settings_error(UiText( main->set_gallery_settings_error(UiText(
localization_language_index_ == 0 ? "保存失败,请重试" localization::Literal("保存失败,请重试", localization_language_index_)));
: localization_language_index_ == 2 ? "Не удалось сохранить"
: "Could not save; try again"));
main->set_settings_tab(1); main->set_settings_tab(1);
return false; return false;
} }
@@ -4298,10 +4608,31 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
main->set_self_host_settings_open(true); main->set_self_host_settings_open(true);
return false; return false;
} }
if (config_center_->SetIpDirectEnabled(main->get_ip_direct_enabled_draft()) != 0) {
if (wall_changed && !ui_->gallery.Save(ui_->gallery_path))
LOG_ERROR("Failed to restore DeskHub settings after IP direct save failure");
main->set_settings_save_error(UiText(
localization::Literal("无法保存 IP 连接设置,请重试。", localization_language_index_)));
return false;
}
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
if (config_center_->SetLockOnDisconnect(main->get_lock_on_disconnect_enabled()) != 0) {
if (wall_changed && !ui_->gallery.Save(ui_->gallery_path))
LOG_ERROR("Failed to restore DeskHub settings after lock preference save failure");
main->set_settings_save_error(UiText(
localization::Literal("无法保存断开后锁屏设置,请重试。", localization_language_index_)));
return false;
}
#endif
if (config_center_->SetPrivacyLocalUnlock(main->get_privacy_local_unlock_allowed()) != 0) {
if (wall_changed) ui_->gallery.Save(ui_->gallery_path);
main->set_settings_save_error(UiText(
localization::Literal("无法保存隐私屏解锁设置,请重试。", localization_language_index_)));
return false;
}
language_button_value_ = language_button_value_ =
localization::detail::ClampLanguageIndex(main->get_language_index()); std::clamp(main->get_language_index(), 0, localization::kSystemLanguage);
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1); video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
enable_turn_ = main->get_turn_enabled();
enable_self_hosted_ = main->get_self_hosted_enabled(); enable_self_hosted_ = main->get_self_hosted_enabled();
#if !defined(__OHOS__) #if !defined(__OHOS__)
enable_autostart_ = main->get_autostart_enabled(); enable_autostart_ = main->get_autostart_enabled();
@@ -4310,14 +4641,16 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
localization_language_ = localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(language_button_value_); static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
localization_language_index_ = language_button_value_; localization_language_index_ = localization::detail::ClampLanguageIndex(language_button_value_);
config_center_->SetLanguage(localization_language_); config_center_->SetLanguage(localization_language_);
privacy_.SetText( privacy_.SetText(
{localization::privacy_screen_unlock_hint[localization_language_index_]}); {localization::privacy_screen_unlock_hint[localization_language_index_],
localization::privacy_screen_locked_hint[localization_language_index_],
config_center_->IsPrivacyLocalUnlockAllowed()});
config_center_->SetVideoEncodeFormat( config_center_->SetVideoEncodeFormat(
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>( static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
video_encode_format_button_value_)); video_encode_format_button_value_));
config_center_->SetTurn(enable_turn_, main->get_force_relay_enabled()); config_center_->SetForceRelay(main->get_force_relay_enabled());
main->set_force_relay_enabled(config_center_->IsForceRelay()); main->set_force_relay_enabled(config_center_->IsForceRelay());
#if !defined(__OHOS__) #if !defined(__OHOS__)
config_center_->SetAutostart(enable_autostart_); config_center_->SetAutostart(enable_autostart_);
@@ -4383,11 +4716,12 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
localization_language_index_ = localization::detail::ClampLanguageIndex(language_button_value_); localization_language_index_ = localization::detail::ClampLanguageIndex(language_button_value_);
localization_language_ = config_center_->GetLanguage(); localization_language_ = config_center_->GetLanguage();
video_encode_format_button_value_ = static_cast<int>(config_center_->GetVideoEncodeFormat()); video_encode_format_button_value_ = static_cast<int>(config_center_->GetVideoEncodeFormat());
enable_turn_ = config_center_->IsEnableTurn();
enable_self_hosted_ = config_center_->IsSelfHosted(); enable_self_hosted_ = config_center_->IsSelfHosted();
enable_autostart_ = config_center_->IsEnableAutostart(); enable_autostart_ = config_center_->IsEnableAutostart();
enable_daemon_ = config_center_->IsEnableDaemon(); enable_daemon_ = config_center_->IsEnableDaemon();
privacy_.SetText({localization::privacy_screen_unlock_hint[localization_language_index_]}); privacy_.SetText({localization::privacy_screen_unlock_hint[localization_language_index_],
localization::privacy_screen_locked_hint[localization_language_index_],
config_center_->IsPrivacyLocalUnlockAllowed()});
const auto path = config_center_->GetFileTransferSavePath(); const auto path = config_center_->GetFileTransferSavePath();
std::snprintf(file_transfer_save_path_buf_, sizeof(file_transfer_save_path_buf_), "%s", path.c_str()); std::snprintf(file_transfer_save_path_buf_, sizeof(file_transfer_save_path_buf_), "%s", path.c_str());
const auto host = config_center_->GetSignalServerHost(); const auto host = config_center_->GetSignalServerHost();
@@ -4397,7 +4731,6 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_), "%s", port_text.c_str()); std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_), "%s", port_text.c_str());
language_button_value_last_ = language_button_value_; language_button_value_last_ = language_button_value_;
video_encode_format_button_value_last_ = video_encode_format_button_value_; video_encode_format_button_value_last_ = video_encode_format_button_value_;
enable_turn_last_ = enable_turn_;
enable_self_hosted_last_ = enable_self_hosted_; enable_self_hosted_last_ = enable_self_hosted_;
enable_autostart_last_ = enable_autostart_; enable_autostart_last_ = enable_autostart_;
enable_daemon_last_ = enable_daemon_; enable_daemon_last_ = enable_daemon_;
@@ -4407,9 +4740,12 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
#endif #endif
if (ui_) { if (ui_) {
ui_->localized_language = -1; ui_->localized_language = -1;
for (const auto& window : {ui_->main, ui_->wall})
window->set_ip_direct_enabled(config_center_->IsIpDirectEnabled());
if (!ui_->main->get_settings_open()) ResetSettingsUi(); if (!ui_->main->get_settings_open()) ResetSettingsUi();
if (!ui_->wall->get_settings_open()) ResetSettingsUi(true); if (!ui_->wall->get_settings_open()) ResetSettingsUi(true);
} }
ApplyLanSettings();
if (reconnect && !HasActiveSession()) { if (reconnect && !HasActiveSession()) {
// Tick recreates the peer after asynchronous cleanup, using the committed // Tick recreates the peer after asynchronous cleanup, using the committed
// configuration. Keep the application and its console running throughout. // configuration. Keep the application and its console running throughout.
@@ -4434,13 +4770,30 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
void GuiApplication::ConnectFromUi(const std::string& input) { void GuiApplication::ConnectFromUi(const std::string& input) {
ui_->gallery_auth_id.clear(); ui_->gallery_auth_id.clear();
if (input.rfind("lan-", 0) == 0) {
if (!LanAvailable()) return;
if (auto existing = FindRemoteSession(input);
existing && existing->connection_established_) {
existing->tab_opened_ = true;
focused_remote_id_ = existing->remote_id_;
need_to_create_stream_window_ = true;
return;
}
for (const auto& [_, recent] : recent_connections_)
if (recent.remote_id == input && !recent.lan.device_id.empty()) {
ui_->main->invoke_request_lan_connection(
UiText(recent.lan.address +
(recent.lan.port == 52888
? ""
: ":" + std::to_string(recent.lan.port))));
return;
}
return;
}
const std::string remote_id = CompactPeerId(input); const std::string remote_id = CompactPeerId(input);
if (!gallery::Workspace::ValidId(remote_id)) { if (!gallery::Workspace::ValidId(remote_id)) {
ui_->main->set_connection_input_error( ui_->main->set_connection_input_error(
UiText(localization_language_index_ == 0 ? "请输入有效的 9 位设备 ID" UiText(localization::Literal("请输入有效的 9 位设备 ID", localization_language_index_)));
: localization_language_index_ == 2
? "Введите корректный 9-значный ID устройства"
: "Enter a valid 9-digit device ID"));
return; return;
} }
ui_->main->set_connection_input_error(""); ui_->main->set_connection_input_error("");
@@ -4994,41 +5347,31 @@ bool GuiApplication::AddGalleryDevice(const std::string& input,
const std::string& password, const std::string& password,
const std::string& group, const std::string& group,
bool remember_password) { bool remember_password) {
const auto error = [this](const char* zh, const char* en, const char* ru) { const auto error = [this](const char* zh) {
ui_->wall->set_gallery_notice(UiText( ui_->wall->set_gallery_notice(UiText(
localization_language_index_ == 0 ? zh localization::Literal(zh, localization_language_index_).c_str()));
: localization_language_index_ == 2 ? ru : en));
return false; return false;
}; };
const auto id = gallery::Workspace::NormalizeId(input); const auto id = gallery::Workspace::NormalizeId(input);
if (!gallery::Workspace::ValidId(id)) if (!gallery::Workspace::ValidId(id))
return error("请输入有效的 9 位设备 ID", return error("请输入有效的 9 位设备 ID");
"Enter a valid 9-digit device ID",
"Введите корректный 9-значный ID устройства");
if (password.empty() || password.size() > 6 || if (password.empty() || password.size() > 6 ||
password.find_first_not_of( password.find_first_not_of(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") != "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") !=
std::string::npos) std::string::npos)
return error("请输入不超过 6 位的字母或数字密码", return error("请输入 1–6 位英文字母或数字密码");
"Use a password of 1–6 letters or digits",
"Пароль: 1–6 латинских букв или цифр");
if (!signal_connected_) if (!signal_connected_)
return error("尚未连接服务器,请稍后重试", return error("尚未连接服务器,请稍后重试");
"Not connected to the server; try again shortly",
"Нет соединения с сервером. Повторите позже");
using State = SlintUi::BatchOperation::State; using State = SlintUi::BatchOperation::State;
for (const auto& operation : ui_->batch_operations) for (const auto& operation : ui_->batch_operations)
if (operation.id == id && operation.action == "connect" && if (operation.id == id && operation.action == "connect" &&
(operation.state == State::Queued || operation.state == State::Running)) (operation.state == State::Queued || operation.state == State::Running))
return error("该设备正在连接,请稍候", return error("该设备正在连接,请稍候");
"This device is already connecting",
"Устройство уже подключается");
auto updated = ui_->gallery; auto updated = ui_->gallery;
updated.Add(id, Trim(group)); updated.Add(id, Trim(group));
if (!updated.Save(ui_->gallery_path)) if (!updated.Save(ui_->gallery_path))
return error("保存失败,请重试", "Could not save; try again", return error("保存失败,请重试");
"Не удалось сохранить");
ui_->gallery = std::move(updated); ui_->gallery = std::move(updated);
ui_->gallery_hidden_active_ids.erase(id); ui_->gallery_hidden_active_ids.erase(id);
if (auto props = FindRemoteSession(id)) { if (auto props = FindRemoteSession(id)) {
@@ -5070,9 +5413,7 @@ bool GuiApplication::ImportGalleryRecent(const std::string& input_group) {
updated.AddMany(ids, group); updated.AddMany(ids, group);
if (!updated.Save(ui_->gallery_path)) { if (!updated.Save(ui_->gallery_path)) {
ui_->wall->set_gallery_notice(UiText( ui_->wall->set_gallery_notice(UiText(
localization_language_index_ == 0 ? "保存失败,请重试" localization::Literal("保存失败,请重试", localization_language_index_)));
: localization_language_index_ == 2 ? "Не удалось сохранить"
: "Could not save; try again"));
return false; return false;
} }
ui_->gallery = std::move(updated); ui_->gallery = std::move(updated);
@@ -5152,9 +5493,7 @@ bool GuiApplication::SaveDeviceGroups(const std::string& new_group,
auto updated = ui_->gallery; auto updated = ui_->gallery;
if (!updated.SetGroups(id, names) || !updated.Save(ui_->gallery_path)) { if (!updated.SetGroups(id, names) || !updated.Save(ui_->gallery_path)) {
main->set_device_group_error(UiText( main->set_device_group_error(UiText(
localization_language_index_ == 0 ? "保存失败,请重试" localization::Literal("保存失败,请重试", localization_language_index_)));
: localization_language_index_ == 2 ? "Не удалось сохранить"
: "Could not save; try again"));
return false; return false;
} }
// Removing an empty group shifts indices. Retain the filter by name instead // Removing an empty group shifts indices. Retain the filter by name instead
@@ -5192,10 +5531,8 @@ void GuiApplication::GalleryAction(const std::string& action,
ui_->gallery_network_synced = {}; ui_->gallery_network_synced = {};
return; return;
} }
const auto tr = [this](const char* zh, const char* en, const char* ru) { const auto tr = [this](const char* zh) {
return UiText(localization_language_index_ == 0 ? zh return UiText(localization::Literal(zh, localization_language_index_).c_str());
: localization_language_index_ == 2 ? ru
: en);
}; };
if (action == "recent-open") { if (action == "recent-open") {
ReleaseControl(); ReleaseControl();
@@ -5209,6 +5546,7 @@ void GuiApplication::GalleryAction(const std::string& action,
std::set<std::string> seen; std::set<std::string> seen;
int available = 0; int available = 0;
for (const auto& [_, recent] : recent_connections_) { for (const auto& [_, recent] : recent_connections_) {
if (!recent.lan.device_id.empty()) continue;
if (!gallery::Workspace::ValidId(recent.remote_id) || if (!gallery::Workspace::ValidId(recent.remote_id) ||
!seen.insert(recent.remote_id).second) !seen.insert(recent.remote_id).second)
continue; continue;
@@ -5412,7 +5750,7 @@ void GuiApplication::GalleryAction(const std::string& action,
SyncGallery(); SyncGallery();
}; };
if (action == "file") { if (action == "file") {
OpenFileDialog(std::string(tr("选择分发文件", "Select a file", "Выберите файл")), OpenFileDialog(std::string(tr("选择分发文件")),
[enqueue](std::string file) { [enqueue](std::string file) {
if (!file.empty()) enqueue(file); if (!file.empty()) enqueue(file);
}); });
@@ -5689,16 +6027,14 @@ void GuiApplication::SyncGallery() {
ui_->wall->set_gallery_recent_available(available); ui_->wall->set_gallery_recent_available(available);
ui_->wall->set_gallery_recent_selected(selected); ui_->wall->set_gallery_recent_selected(selected);
} }
const auto tr = [this](const char* zh, const char* en, const auto tr = [this](const char* zh) -> std::string {
const char* ru) -> std::string { return localization::Literal(zh, localization_language_index_).c_str();
return localization_language_index_ == 0 ? zh
: localization_language_index_ == 2 ? ru
: en;
}; };
const auto name = [this](const std::string& id) { const auto name = [this](const std::string& id) {
for (const auto& [_, recent] : recent_connections_) for (const auto& [_, recent] : recent_connections_)
if (recent.remote_id == id) if (recent.lan.device_id.empty())
return settings_.RecentConnectionDisplayName(recent); if (recent.remote_id == id)
return settings_.RecentConnectionDisplayName(recent);
Thumbnail::RecentConnection device{}; Thumbnail::RecentConnection device{};
device.remote_id = id; device.remote_id = id;
if (auto props = FindRemoteSession(id)) { if (auto props = FindRemoteSession(id)) {
@@ -5716,7 +6052,7 @@ void GuiApplication::SyncGallery() {
{ {
std::shared_lock lock(remote_sessions_mutex_); std::shared_lock lock(remote_sessions_mutex_);
for (const auto& [id, props] : remote_sessions_) for (const auto& [id, props] : remote_sessions_)
if (props && !props->closing_ && if (props && !props->lan_session_ && !props->closing_ &&
props->connection_status_ == ConnectionStatus::Connected) props->connection_status_ == ConnectionStatus::Connected)
connected_ids.push_back(id); connected_ids.push_back(id);
} }
@@ -5732,10 +6068,10 @@ void GuiApplication::SyncGallery() {
device.id = UiText(FormatPeerId(id.c_str())); device.id = UiText(FormatPeerId(id.c_str()));
const auto mode = props->traversal_mode_.load(); const auto mode = props->traversal_mode_.load();
device.mode = UiText(mode == TraversalMode::P2P device.mode = UiText(mode == TraversalMode::P2P
? tr("直连", "Direct", "Напрямую") ? tr("直连")
: mode == TraversalMode::UnknownMode : mode == TraversalMode::UnknownMode
? tr("等待统计", "Awaiting stats", "Ждём данные") ? tr("等待统计")
: tr("中继", "Relay", "Ретрансляция")); : tr("中继"));
device.inbound = device.outbound = device.loss = "—"; device.inbound = device.outbound = device.loss = "—";
device.video_latency = "—"; device.video_latency = "—";
// Prefer the wall's own frame path. An independently displayed stream // Prefer the wall's own frame path. An independently displayed stream
@@ -5816,11 +6152,12 @@ void GuiApplication::SyncGallery() {
bool remember = operation.remember_password; bool remember = operation.remember_password;
if (!operation.password) if (!operation.password)
for (const auto& [_, recent] : recent_connections_) for (const auto& [_, recent] : recent_connections_)
if (recent.remote_id == operation.id) { if (recent.lan.device_id.empty())
password = recent.password; if (recent.remote_id == operation.id) {
remember = recent.remember_password; password = recent.password;
break; remember = recent.remember_password;
} break;
}
// Join itself determines availability. Background probes must never // Join itself determines availability. Background probes must never
// overwrite the interactive page's single pending presence probe. // overwrite the interactive page's single pending presence probe.
ConnectTo(operation.id, password.c_str(), remember, true, true); ConnectTo(operation.id, password.c_str(), remember, true, true);
@@ -5902,7 +6239,7 @@ void GuiApplication::SyncGallery() {
label += member.group; label += member.group;
} }
std::vector<slint::SharedString> groups{ std::vector<slint::SharedString> groups{
UiText(tr("全部设备", "All devices", "Все устройства"))}; UiText(tr("全部设备"))};
for (const auto& group : wall.groups) groups.push_back(UiText(group)); for (const auto& group : wall.groups) groups.push_back(UiText(group));
const auto old_groups = ui_->wall->get_gallery_groups(); const auto old_groups = ui_->wall->get_gallery_groups();
bool changed_groups = !old_groups || old_groups->row_count() != groups.size(); bool changed_groups = !old_groups || old_groups->row_count() != groups.size();
@@ -5970,6 +6307,7 @@ void GuiApplication::SyncGallery() {
tile.selected = wall.selected.count(id); tile.selected = wall.selected.count(id);
auto props = FindRemoteSession(id); auto props = FindRemoteSession(id);
for (const auto& [_, recent] : recent_connections_) { for (const auto& [_, recent] : recent_connections_) {
if (!recent.lan.device_id.empty()) continue;
if (recent.remote_id != id) continue; if (recent.remote_id != id) continue;
tile.host_name = UiText(recent.remote_host_name); tile.host_name = UiText(recent.remote_host_name);
break; break;
@@ -5985,8 +6323,8 @@ void GuiApplication::SyncGallery() {
tile.online = tile.connected || online.value_or(false); tile.online = tile.connected || online.value_or(false);
tile.presence_known = tile.connected || online.has_value(); tile.presence_known = tile.connected || online.has_value();
tile.status = UiText( tile.status = UiText(
tile.connected ? tr("等待画面", "Waiting for video", "Ожидание видео") tile.connected ? tr("等待画面")
: tr("未连接", "Not connected", "Нет соединения")); : tr("未连接"));
const bool queued = !tile.connected && std::any_of( const bool queued = !tile.connected && std::any_of(
ui_->batch_operations.begin(), ui_->batch_operations.end(), ui_->batch_operations.begin(), ui_->batch_operations.end(),
[&](const auto& operation) { [&](const auto& operation) {
@@ -5995,9 +6333,9 @@ void GuiApplication::SyncGallery() {
}); });
// A queued retry takes precedence over errors from the previous session. // A queued retry takes precedence over errors from the previous session.
if (queued) if (queued)
tile.status = UiText(tr("排队中", "Queued", "В очереди")); tile.status = UiText(tr("队列中"));
else if (props && props->connection_auth_.Get().password_required) else if (props && props->connection_auth_.Get().password_required)
tile.status = UiText(tr("需要密码", "Password required", "Нужен пароль")); tile.status = UiText(tr("需要密码"));
else if (props && !tile.connected) else if (props && !tile.connected)
tile.status = UiText(ConnectionStatusText( tile.status = UiText(ConnectionStatusText(
props->connection_status_.load(), localization_language_index_)); props->connection_status_.load(), localization_language_index_));
@@ -6025,8 +6363,8 @@ void GuiApplication::SyncGallery() {
tile.has_frame = true; tile.has_frame = true;
tile.stale = now - props->gallery_frame_at_ > 5s; tile.stale = now - props->gallery_frame_at_ > 5s;
tile.status = tile.status =
UiText(tile.stale ? tr("画面已停留", "Frame stale", "Кадр устарел") UiText(tile.stale ? tr("画面已停留")
: tr("只读预览", "View only", "Просмотр")); : tr("只读预览"));
} }
} }
return tile; return tile;
@@ -6205,6 +6543,9 @@ void GuiApplication::SyncGallery() {
} }
void GuiApplication::Cleanup() { void GuiApplication::Cleanup() {
BeginShutdown();
while (!ShutdownDrained())
std::this_thread::sleep_for(std::chrono::milliseconds(2));
remote_input_.Stop(); remote_input_.Stop();
#if _WIN32 #if _WIN32
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed); windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
@@ -6245,6 +6586,7 @@ void GuiApplication::Cleanup() {
keyboard_.ReleaseAllRemotePressedKeys("application_exit"); keyboard_.ReleaseAllRemotePressedKeys("application_exit");
devices_.DestroyDevices(); devices_.DestroyDevices();
privacy_.Shutdown(); privacy_.Shutdown();
HandleScreenLock();
devices_.DestroyFactories(); devices_.DestroyFactories();
CloseAllRemoteSessions(); CloseAllRemoteSessions();
WaitForSessionCleanup(); WaitForSessionCleanup();
@@ -1,116 +1,91 @@
/* /*
* @Author: DI JUNKUN * @Author: DI JUNKUN
* @Date: 2024-05-29 * @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/ */
#ifndef _LOCALIZATION_H_ #ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_ #define _LOCALIZATION_H_
#include <cstddef> #include <cstddef>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "localization_data.h" #include "localization_data.h"
#include "system_language.h"
#if _WIN32 #if _WIN32
#include <Windows.h> #include <Windows.h>
#endif #endif
namespace crossdesk { namespace crossdesk {
namespace localization { namespace localization {
struct LanguageOption { struct LanguageOption {
std::string code; std::string code;
std::string display_name; std::string display_name;
}; };
class LocalizedString { class LocalizedString {
public: public:
constexpr explicit LocalizedString(const char* key) : key_(key) {} constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const; const std::string& operator[](int language_index) const;
private: private:
const char* key_; const char* key_;
}; };
inline const std::vector<LanguageOption>& GetSupportedLanguages() { inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = { static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")}, {"zh-CN", "简体中文"}, {"en-US", "English"}, {"ru-RU", "Русский"},
{"en-US", "English"}, {"zh-Hant", "繁體中文"}, {"ja", "日本語"}, {"ko", "한국어"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}}; {"de", "Deutsch"}, {"fr", "Français"}, {"es", "Español"},
return kSupportedLanguages; {"pt-BR", "Português (Brasil)"}};
} return kSupportedLanguages;
}
namespace detail { namespace detail {
inline int ClampLanguageIndex(int language_index) { inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 && if (language_index == kSystemLanguage) return SystemLanguage();
language_index < static_cast<int>(GetSupportedLanguages().size())) { return language_index >= 0 && language_index < kSystemLanguage
return language_index; ? language_index : kEnglishLanguage;
}
return 0;
} }
using TranslationTable = using TranslationTable = std::unordered_map<std::string, std::vector<std::string>>;
std::unordered_map<std::string, inline const TranslationTable& LiteralTable() {
std::unordered_map<std::string, std::string>>; static const TranslationTable table = [] {
TranslationTable result;
inline std::unordered_map<std::string, std::string> MakeLocalizedValues( for (const auto& row : kTranslationRows)
const TranslationRow& row) { result.emplace(row.key, std::vector<std::string>(std::begin(row.text), std::end(row.text)));
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)}, return result;
{"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);
}
return table; return table;
} }
inline const std::string& GetLiteralText(const std::string& key, int language) {
inline const TranslationTable& GetTranslationTable() { static const std::string empty;
static const TranslationTable table = BuildTranslationTable(); const auto it = LiteralTable().find(key);
return table; 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_index) { inline const std::string& GetTranslatedText(const std::string& key, int language) {
static const std::string kEmptyText = ""; static const std::unordered_map<std::string, std::string> names = [] {
std::unordered_map<std::string, std::string> result;
const auto& table = GetTranslationTable(); for (const auto& row : kNamedTranslations) result.emplace(row.name, row.literal);
const auto key_it = table.find(key); return result;
if (key_it == table.end()) { }();
return kEmptyText; static const std::string empty;
} const auto it = names.find(key);
return it == names.end() ? empty : GetLiteralText(it->second, language);
const auto& localized_values = key_it->second; }
const std::string& language_code =
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code; } // namespace detail
const auto exact_it = localized_values.find(language_code); inline const std::string& Literal(const std::string& key, int language) {
if (exact_it != localized_values.end()) { return detail::GetLiteralText(key, language);
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& LocalizedString::operator[]( inline const std::string& LocalizedString::operator[](
int language_index) const { int language_index) const {
return detail::GetTranslatedText(key_, language_index); return detail::GetTranslatedText(key_, language_index);
@@ -130,11 +105,11 @@ inline std::string FormatDisplayLabel(size_t display_index,
return label; return label;
} }
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \ #define CROSSDESK_DECLARE_LOCALIZED_STRING(name) \
inline const LocalizedString name(#name); inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING) CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING #undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32 #if _WIN32
inline const wchar_t* GetWindowsLocalizedLabel( inline const wchar_t* GetWindowsLocalizedLabel(
int language_index, const char* key, const wchar_t* fallback, int language_index, const char* key, const wchar_t* fallback,
@@ -187,8 +162,8 @@ inline const wchar_t* GetExitProgramLabel(int language_index) {
cache); cache);
} }
#endif #endif
} // namespace localization } // namespace localization
} // namespace crossdesk } // namespace crossdesk
#endif #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
@@ -226,15 +226,11 @@ int ClipboardController::SendToPeers(const std::string &text) {
} }
} }
if (owner_.peer_) { lock.unlock();
const int ret = for (const auto& id : owner_.ConnectedControllers()) {
SendReliableDataFrame(owner_.peer_, text.data(), text.size(), const int ret = owner_.SendControllerData(id, text.data(), text.size(),
owner_.clipboard_label_.c_str()); owner_.clipboard_label_.c_str());
if (ret != 0) { if (ret != 0) return ret;
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
owner_.remote_id_display_, ret);
return ret;
}
} }
return 0; return 0;
@@ -43,18 +43,17 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
SpeakerCapturerFactory().Create()); SpeakerCapturerFactory().Create());
}, },
[this](unsigned char *data, size_t size, const char *) { [this](unsigned char *data, size_t size, const char *) {
if (!owner_.peer_) return;
MiniRtcAudioFrame frame{}; MiniRtcAudioFrame frame{};
frame.data = reinterpret_cast<const char *>(data); frame.data = reinterpret_cast<const char *>(data);
frame.size = size; frame.size = size;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_); for (const auto& id : owner_.ConnectedControllers())
std::shared_lock lock(owner_.connection_status_mutex_); owner_.WithController(
for (const auto& [id, status] : owner_.connection_status_) { id, [&](PeerPtr* peer, const std::string& wire) {
if (status == ConnectionStatus::Connected) frame.captured_timestamp = GetSystemTimeMicros(peer);
SendAudioFrameToPeer(owner_.peer_, &frame, return SendAudioFrameToPeer(peer, &frame,
owner_.audio_label_.c_str(), id.data(), owner_.audio_label_.c_str(),
id.size()); wire.data(), wire.size());
} });
}) {} }) {}
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); } SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
@@ -132,6 +131,10 @@ void SessionDeviceManager::Initialize() {
DeviceControllerFactory::Device::Keyboard)); DeviceControllerFactory::Device::Keyboard));
} }
int SessionDeviceManager::EnsureCaptureReady() {
return screen_capturer_ ? 0 : InitializeScreenCapturer();
}
int SessionDeviceManager::InitializeScreenCapturer() { int SessionDeviceManager::InitializeScreenCapturer() {
#ifdef __APPLE__ #ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) { if (!owner_.EnsureMacScreenRecordingPermission()) {
@@ -193,16 +196,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
.count() .count()
: 0; : 0;
std::vector<std::string> connected_remote_ids; const auto connected_remote_ids = owner_.ConnectedControllers();
{
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);
}
}
}
// Capture can still deliver frames while ICE is gathering or after // Capture can still deliver frames while ICE is gathering or after
// the final controller disconnects. Do not broadcast those frames to // the final controller disconnects. Do not broadcast those frames to
@@ -232,28 +226,27 @@ int SessionDeviceManager::InitializeScreenCapturer() {
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs; has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
const bool stream_changed = !last_video_frame_stream_id_.empty() && const bool stream_changed = !last_video_frame_stream_id_.empty() &&
last_video_frame_stream_id_ != stream_id; 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{}; MiniRtcVideoFrame frame{};
frame.data = reinterpret_cast<const char *>(data); frame.data = reinterpret_cast<const char *>(data);
frame.size = size; frame.size = size;
frame.width = width; frame.width = width;
frame.height = height; frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
frame.native_frame = native_frame; frame.native_frame = native_frame;
const auto send_started = std::chrono::steady_clock::now(); const auto send_started = std::chrono::steady_clock::now();
uint64_t send_errors = 0; uint64_t send_errors = 0;
for (const std::string &remote_id : connected_remote_ids) { for (const std::string &remote_id : connected_remote_ids) {
if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time, if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time,
stream_id)) continue; stream_id)) continue;
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(), if (owner_.WithController(
remote_id.data(), remote_id.size()) != 0) 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; ++send_errors;
} }
const auto send_us = const auto send_us =
@@ -54,6 +54,7 @@ public:
void DestroyFactories(); void DestroyFactories();
int InitializeScreenCapturer(); int InitializeScreenCapturer();
int EnsureCaptureReady();
int StartScreenCapturer(); int StartScreenCapturer();
int StopScreenCapturer(); int StopScreenCapturer();
bool ReleaseRemoteMouseButtons(); bool ReleaseRemoteMouseButtons();
@@ -94,13 +94,19 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
peer = props ? props->peer_ : owner_.peer_; peer = props ? props->peer_ : owner_.peer_;
events = props ? props->peer_events_ : owner_.peer_events_; events = props ? props->peer_events_ : owner_.peer_events_;
} }
if (!props && file.remote_id.rfind("lan/", 0) == 0) {
events = owner_.ControllerEvents(file.remote_id);
peer = events ? events->DirectPeer() : nullptr;
}
auto session = std::make_shared<file_transfer::Session>( auto session = std::make_shared<file_transfer::Session>(
[peer, events, file, props](const char*, const char* data, size_t size) { [peer, events, file, props](const char*, const char* data, size_t size) {
if (!peer || !events || (props && props->closing_)) return -1; if (!peer || !events || (props && props->closing_)) return -1;
auto callback = events->EnterCallback(); auto callback = events->EnterCallback();
if (!callback) return -1; if (!callback || !events->DataAllowed()) return -1;
return SendReliableDataFrameToPeer(peer, data, size, file.file_label.c_str(), const auto wire_id = events->WireId(file.remote_id);
file.remote_id.c_str(), file.remote_id.size()); return SendReliableDataFrameToPeer(
peer, data, size, file.file_label.c_str(),
wire_id.data(), wire_id.size());
}, file_transfer::Session::Progress{}); }, file_transfer::Session::Progress{});
state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0; state.file_send_rate_bps_ = 0; state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0; state.file_send_rate_bps_ = 0;
state.current_file_id_ = id; state.current_file_id_ = id;
@@ -313,6 +319,10 @@ void FileTransferManager::Receive(const char* data, size_t size,
peer = props ? props->peer_ : owner_.peer_; peer = props ? props->peer_ : owner_.peer_;
events = props ? props->peer_events_ : owner_.peer_events_; 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; if (!peer || !events || events.get() != handler || (props && props->closing_)) return;
std::shared_ptr<FileReceiver> receiver; std::shared_ptr<FileReceiver> receiver;
std::filesystem::path default_directory; std::filesystem::path default_directory;
@@ -333,10 +343,12 @@ void FileTransferManager::Receive(const char* data, size_t size,
}); });
entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) { entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) {
auto callback = events->EnterCallback(); auto callback = events->EnterCallback();
if (!callback) return -1; if (!callback || !events->DataAllowed()) return -1;
const auto bytes = EncodeFileTransferAck(ack); const auto bytes = EncodeFileTransferAck(ack);
return SendReliableDataFrameToPeer(peer, bytes.data(), bytes.size(), kFileFeedbackStream, const auto wire_id = events->WireId(remote);
remote.c_str(), remote.size()); return SendReliableDataFrameToPeer(
peer, bytes.data(), bytes.size(), kFileFeedbackStream,
wire_id.data(), wire_id.size());
}); });
} }
receiver = entry.session; receiver = entry.session;
@@ -21,6 +21,8 @@
#include "interactive_desktop.h" #include "interactive_desktop.h"
#include "service_host.h" #include "service_host.h"
#include "unattended_config.h" #include "unattended_config.h"
#include "platform/windows/input/windows_input_injector.h"
#include "platform/windows/input/windows_text_input.h"
#endif #endif
namespace crossdesk { namespace crossdesk {
@@ -275,11 +277,51 @@ InputOutcome KeyboardController::ApplyRemoteText(const std::string& remote_id,
std::lock_guard input_lock(remote_input_mutex_); std::lock_guard input_lock(remote_input_mutex_);
if (!owner_.IsAuthorizedController(remote_id)) return InputOutcome::ignored; if (!owner_.IsAuthorizedController(remote_id)) return InputOutcome::ignored;
if (!owner_.devices_.SupportsTextInput()) return InputOutcome::failed; 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)) { if (!owner_.devices_.SendTextCommand(text)) {
LOG_WARN("Failed to inject committed remote text"); LOG_WARN("Failed to inject committed remote text");
return InputOutcome::failed; return InputOutcome::failed;
} }
return InputOutcome::applied; return InputOutcome::applied;
#endif
} }
InputOutcome KeyboardController::ApplyRemoteState(const std::string& remote_id, InputOutcome KeyboardController::ApplyRemoteState(const std::string& remote_id,
@@ -461,11 +461,9 @@ int SettingsManager::Load() {
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/", std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
owner_.aes128_key_, owner_.aes128_iv_); owner_.aes128_key_, owner_.aes128_iv_);
owner_.language_button_value_ = localization::detail::ClampLanguageIndex( owner_.language_button_value_ = static_cast<int>(owner_.config_center_->GetLanguage());
static_cast<int>(owner_.config_center_->GetLanguage()));
owner_.video_encode_format_button_value_ = owner_.video_encode_format_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat()); static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted(); owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart(); owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon(); owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
@@ -484,7 +482,6 @@ int SettingsManager::Load() {
owner_.language_button_value_last_ = owner_.language_button_value_; owner_.language_button_value_last_ = owner_.language_button_value_;
owner_.video_encode_format_button_value_last_ = owner_.video_encode_format_button_value_last_ =
owner_.video_encode_format_button_value_; owner_.video_encode_format_button_value_;
owner_.enable_turn_last_ = owner_.enable_turn_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_; owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_; owner_.enable_autostart_last_ = owner_.enable_autostart_;
@@ -744,6 +741,8 @@ std::string SettingsManager::RecentConnectionDisplayName(
connection.remote_host_name != "unknown") { connection.remote_host_name != "unknown") {
return connection.remote_host_name; return connection.remote_host_name;
} }
if (!connection.lan.device_id.empty())
return connection.lan.address + ":" + std::to_string(connection.lan.port);
return connection.remote_id; return connection.remote_id;
} }
@@ -14,6 +14,7 @@
#include "platform.h" #include "platform.h"
#include "platform/video_renderer.h" #include "platform/video_renderer.h"
#include "rd_log.h" #include "rd_log.h"
#include "screen_lock.h"
#include "runtime/device_presence_request.h" #include "runtime/device_presence_request.h"
#include "runtime/gui_runtime.h" #include "runtime/gui_runtime.h"
@@ -26,6 +27,7 @@ constexpr auto kPresenceRetryInterval = std::chrono::seconds(1);
} // namespace } // namespace
bool GuiRuntime::HasActiveSession() { bool GuiRuntime::HasActiveSession() {
if (lan_ && !lan_->Quiescent()) return true;
if (stream_window_inited_) return true; if (stream_window_inited_) return true;
std::shared_lock lock(connection_status_mutex_); std::shared_lock lock(connection_status_mutex_);
return std::any_of(connection_status_.begin(), connection_status_.end(), return std::any_of(connection_status_.begin(), connection_status_.end(),
@@ -188,7 +190,8 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
void GuiRuntime::HandleServerControllerDisconnected( void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) { const std::string& remote_id, const char* reason) {
transfers_.Cancel(peer_events_.get(), remote_id); if (auto events = ControllerEvents(remote_id))
transfers_.Cancel(events.get(), remote_id);
preview_subscriptions_.Remove(remote_id); preview_subscriptions_.Remove(remote_id);
bool has_connected_controller = false; bool has_connected_controller = false;
@@ -196,6 +199,8 @@ void GuiRuntime::HandleServerControllerDisconnected(
{ {
std::unique_lock lock(connection_status_mutex_); std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id); connection_status_.erase(remote_id);
disconnect_lock_.Disconnected(remote_id,
config_center_->IsLockOnDisconnect());
remote_input_.Disconnect(remote_id); remote_input_.Disconnect(remote_id);
devices_.OnControllerConnectionsChanged(); devices_.OnControllerConnectionsChanged();
connection_host_names_.erase(remote_id); connection_host_names_.erase(remote_id);
@@ -243,11 +248,30 @@ void GuiRuntime::HandleServerControllerDisconnected(
#endif #endif
devices_.StopSpeakerCapturer(); devices_.StopSpeakerCapturer();
start_mouse_controller_ = false; start_mouse_controller_ = false;
start_keyboard_capturer_ = false; {
std::shared_lock lock(remote_sessions_mutex_);
start_keyboard_capturer_ = std::any_of(
remote_sessions_.begin(), remote_sessions_.end(), [](const auto& item) {
const auto& session = item.second;
return session && !session->closing_ &&
session->connection_established_ && !session->wall_managed_;
});
}
remote_client_id_.clear(); remote_client_id_.clear();
audio_capture_ = false; audio_capture_ = false;
} }
void GuiRuntime::HandleScreenLock() {
// Serialize request consumption with incoming admission so a reconnect
// cancels a pending lock before it can interrupt the new controller.
std::unique_lock lock(connection_status_mutex_);
if (!disconnect_lock_.TakeRequest(config_center_->IsLockOnDisconnect()))
return;
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
if (LockScreen()) LOG_INFO("Requested screen lock after last controller disconnected");
#endif
}
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id, int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char* password, const char* password,
bool remember_password) { bool remember_password) {
@@ -294,10 +318,15 @@ int GuiRuntime::ForceSignalReconnect() {
} }
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) { void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
if (!props || props->closing_.exchange(true)) return; if (!props) return;
if (props->lan_session_ && !props->lan_session_->closing) {
if (lan_) lan_->Close(props->lan_session_);
return;
}
if (props->closing_.exchange(true)) return;
MuteRemoteAudio(props); MuteRemoteAudio(props);
if (props->peer_events_) { if (props->peer_events_) {
props->peer_events_->Deactivate(); props->peer_events_->StopAccepting();
transfers_.Cancel(props->peer_events_.get()); transfers_.Cancel(props->peer_events_.get());
} }
props->connection_established_ = false; props->connection_established_ = false;
@@ -350,7 +379,8 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask( session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
[props, peer, frame_snapshot, video_width, video_height, platform, [props, peer, frame_snapshot, video_width, video_height, platform,
thumbnail = thumbnail_, queued_at]() mutable { thumbnail = thumbnail_, queued_at]() mutable {
if (peer) { if (props->peer_events_) props->peer_events_->Deactivate();
if (peer && !props->lan_session_) {
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_, LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
props->remote_id_); props->remote_id_);
LeaveConnection(peer, props->remote_id_.c_str()); LeaveConnection(peer, props->remote_id_.c_str());
@@ -359,11 +389,20 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
if (thumbnail && props->was_connected_) { if (thumbnail && props->was_connected_) {
const char* nv12 = frame_snapshot && !frame_snapshot->empty() const char* nv12 = frame_snapshot && !frame_snapshot->empty()
? reinterpret_cast<const char*>(frame_snapshot->data()) : nullptr; ? reinterpret_cast<const char*>(frame_snapshot->data()) : nullptr;
const auto direct = props->lan_session_;
const Thumbnail::LanTarget target =
direct ? Thumbnail::LanTarget{direct->target.address,
direct->target.device_id,
direct->target.port}
: Thumbnail::LanTarget{};
const auto record_id = direct
? Thumbnail::LanRecordId(target.device_id)
: props->remote_id_;
if (thumbnail->SaveToThumbnail( if (thumbnail->SaveToThumbnail(
nv12, video_width, video_height, props->remote_id_, nv12, video_width, video_height, record_id,
props->remote_host_name_, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "", props->remember_password_ ? props->remote_password_ : "",
platform) != 0) { platform, direct ? &target : nullptr) != 0) {
LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_); LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_);
} }
} }
@@ -375,6 +414,17 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
} }
void GuiRuntime::CloseServerController(const std::string& remote_id) { void GuiRuntime::CloseServerController(const std::string& remote_id) {
if (remote_id.rfind("lan/", 0) == 0) {
std::shared_ptr<lan::Session> session;
{
std::shared_lock lock(remote_sessions_mutex_);
if (auto it = lan_controllers_.find(remote_id);
it != lan_controllers_.end())
session = it->second.session;
}
if (session && lan_) lan_->Close(session);
return;
}
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id)) if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
return; return;
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask( controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
@@ -392,20 +442,17 @@ void GuiRuntime::CloseConnectionPeer() {
announcements_.Reset(); announcements_.Reset();
return; return;
} }
peer_events_->Deactivate(); peer_events_->StopAccepting();
transfers_.Cancel(peer_events_.get()); transfers_.Cancel(peer_events_.get());
announcements_.Reset(); announcements_.Reset();
std::vector<std::string> controllers; std::vector<std::string> controllers;
{ {
std::shared_lock lock(connection_status_mutex_); std::shared_lock lock(connection_status_mutex_);
for (const auto& [id, status] : connection_status_) controllers.push_back(id); for (const auto& [id, status] : connection_status_)
if (id.rfind("lan/", 0) != 0) controllers.push_back(id);
} }
for (const auto& id : controllers) for (const auto& id : controllers)
HandleServerControllerDisconnected(id, "peer_closed"); HandleServerControllerDisconnected(id, "peer_closed");
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
signal_connected_ = false; signal_connected_ = false;
device_presence_cache_.SetSignalConnected(false); device_presence_cache_.SetSignalConnected(false);
signal_status_ = SignalStatus::SignalClosed; signal_status_ = SignalStatus::SignalClosed;
@@ -418,6 +465,7 @@ void GuiRuntime::CloseConnectionPeer() {
// these finish before the shared peer is destroyed. // these finish before the shared peer is destroyed.
connection_peer_cleanup_ = session_cleanup_queue_.PostTask( connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable { [peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
handler->Deactivate();
LOG_INFO("[{}] Background connection peer cleanup", id); LOG_INFO("[{}] Background connection peer cleanup", id);
LeaveConnection(peer, id.c_str()); LeaveConnection(peer, id.c_str());
DestroyPeer(&peer); DestroyPeer(&peer);
@@ -551,6 +599,12 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
std::shared_lock lock(remote_sessions_mutex_); std::shared_lock lock(remote_sessions_mutex_);
auto it = remote_sessions_.find(remote_id); auto it = remote_sessions_.find(remote_id);
if (it == remote_sessions_.end()) { if (it == remote_sessions_.end()) {
if (remote_id.rfind("lan-", 0) == 0)
for (const auto& [_, props] : remote_sessions_)
if (props->lan_session_ && !props->closing_ &&
Thumbnail::LanRecordId(props->lan_session_->target.device_id) ==
remote_id)
return props;
return nullptr; return nullptr;
} }
return it->second; return it->second;
@@ -22,6 +22,7 @@ inline nlohmann::json BuildDevicePresenceRequest(
const auto append = [&](const std::string& id) { const auto append = [&](const std::string& id) {
// Recent connection keys contain Y/N plus display settings after the ID. // Recent connection keys contain Y/N plus display settings after the ID.
const std::string pure_id = id.substr(0, id.find_first_of("YN")); const std::string pure_id = id.substr(0, id.find_first_of("YN"));
if (pure_id.rfind("lan-", 0) == 0 || pure_id.rfind("lan/", 0) == 0) return;
if (!pure_id.empty() && if (!pure_id.empty() &&
std::find(devices.begin(), devices.end(), pure_id) == devices.end()) { std::find(devices.begin(), devices.end(), pure_id) == devices.end()) {
devices.push_back(pure_id); devices.push_back(pure_id);
@@ -0,0 +1,41 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-25
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DISCONNECT_LOCK_POLICY_H_
#define _DISCONNECT_LOCK_POLICY_H_
#include <string>
#include <unordered_set>
#include <utility>
namespace crossdesk {
// Guarded by GuiRuntime's connection_status_mutex_. Only incoming sessions
// that reached Connected can request a lock; terminal callbacks may repeat.
class DisconnectLockPolicy {
public:
void Connected(const std::string& remote_id) {
controllers_.insert(remote_id);
pending_ = false;
}
void Disconnected(const std::string& remote_id, bool enabled) {
if (controllers_.erase(remote_id) && controllers_.empty() && enabled)
pending_ = true;
}
bool TakeRequest(bool enabled) {
return std::exchange(pending_, false) && enabled && controllers_.empty();
}
private:
std::unordered_set<std::string> controllers_;
bool pending_ = false;
};
} // namespace crossdesk
#endif
+15 -7
View File
@@ -70,6 +70,7 @@ GuiRuntime::~GuiRuntime() {
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed); windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
if (windows_service_worker_) windows_service_worker_->Reset(); if (windows_service_worker_) windows_service_worker_->Reset();
#endif #endif
lan_.reset();
transfers_.Shutdown(); transfers_.Shutdown();
WaitForSessionCleanup(); WaitForSessionCleanup();
} }
@@ -292,7 +293,7 @@ int GuiRuntime::CreateConnectionPeer() {
LOG_INFO("Create peer [{}] instance failed", client_id_); LOG_INFO("Create peer [{}] instance failed", client_id_);
} }
if (0 == devices_.InitializeScreenCapturer()) { if (0 == devices_.EnsureCaptureReady()) {
const auto &displays = devices_.display_info_list(); const auto &displays = devices_.display_info_list();
for (size_t index = 0; index < displays.size(); ++index) { for (size_t index = 0; index < displays.size(); ++index) {
const std::string stream_id = MakeDisplayStreamId(index); const std::string stream_id = MakeDisplayStreamId(index);
@@ -334,10 +335,18 @@ void GuiRuntime::HandleRecentConnections() {
// Record a successful connection before a preview or host info arrives. // Record a successful connection before a preview or host info arrives.
// The close path still updates its final thumbnail. Remember password // The close path still updates its final thumbnail. Remember password
// controls only credential storage, not whether this device is listed. // controls only credential storage, not whether this device is listed.
const auto direct = props->lan_session_;
const Thumbnail::LanTarget target =
direct ? Thumbnail::LanTarget{direct->target.address,
direct->target.device_id,
direct->target.port}
: Thumbnail::LanTarget{};
const auto record_id =
direct ? Thumbnail::LanRecordId(target.device_id) : props->remote_id_;
if (thumbnail_->SaveToThumbnail( if (thumbnail_->SaveToThumbnail(
nullptr, 0, 0, props->remote_id_, host_name, nullptr, 0, 0, record_id, host_name,
props->remember_password_ ? props->remote_password_ : "", props->remember_password_ ? props->remote_password_ : "",
platform) == 0) platform, direct ? &target : nullptr) == 0)
reload_recent_connections_ = true; reload_recent_connections_ = true;
else else
LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_); LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_);
@@ -346,9 +355,7 @@ void GuiRuntime::HandleRecentConnections() {
if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) { if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) {
uint32_t now_time = SDL_GetTicks(); uint32_t now_time = SDL_GetTicks();
if (now_time - recent_connection_image_save_time_ >= 50) { if (now_time - recent_connection_image_save_time_ >= 50) {
int ret = thumbnail_->LoadThumbnail(recent_connections_, int ret = thumbnail_->LoadThumbnail(recent_connections_);
&recent_connection_image_width_,
&recent_connection_image_height_);
if (!ret) { if (!ret) {
LOG_INFO("Load recent connection thumbnails"); LOG_INFO("Load recent connection thumbnails");
} }
@@ -356,7 +363,8 @@ void GuiRuntime::HandleRecentConnections() {
recent_connection_ids_.clear(); recent_connection_ids_.clear();
for (const auto &conn : recent_connections_) { for (const auto &conn : recent_connections_) {
recent_connection_ids_.push_back(conn.first); if (conn.second.lan.device_id.empty())
recent_connection_ids_.push_back(conn.first);
} }
need_to_send_recent_connections_ = true; need_to_send_recent_connections_ = true;
} }
@@ -19,8 +19,10 @@
#include "features/input/keyboard_controller.h" #include "features/input/keyboard_controller.h"
#include "features/input/remote_input_dispatcher.h" #include "features/input/remote_input_dispatcher.h"
#include "features/settings/settings_manager.h" #include "features/settings/settings_manager.h"
#include "network/lan/lan_session_manager.h"
#include "privacy_controller.h" #include "privacy_controller.h"
#include "runtime/gui_state.h" #include "runtime/gui_state.h"
#include "runtime/disconnect_lock_policy.h"
#include "runtime/announcement_inbox.h" #include "runtime/announcement_inbox.h"
#include "runtime/peer_event_handler.h" #include "runtime/peer_event_handler.h"
#include "runtime/privacy_session_policy.h" #include "runtime/privacy_session_policy.h"
@@ -50,6 +52,35 @@ class GuiRuntime : protected gui_detail::GuiState {
static void SdlCaptureAudioIn(void* userdata, uint8_t* stream, int len); static void SdlCaptureAudioIn(void* userdata, uint8_t* stream, int len);
static void SdlCaptureAudioOut(void* userdata, uint8_t* stream, int len); static void SdlCaptureAudioOut(void* userdata, uint8_t* stream, int len);
void InitializeLan();
void ApplyLanSettings();
bool LanAvailable() const;
void TickLan();
void BeginShutdown();
bool ShutdownDrained();
void RegisterStreams(PeerPtr* peer, size_t display_count);
void ConnectLan(const std::string& address, unsigned port,
std::string password, bool remember_password = false);
int WithController(
const std::string& id,
const std::function<int(PeerPtr*, const std::string&)>& operation);
int SendControllerData(const std::string& id, const char* data, size_t size,
const char* label);
std::shared_ptr<PeerEventHandler> ControllerEvents(const std::string& id);
std::vector<std::string> ConnectedControllers();
std::unique_ptr<lan::SessionManager> lan_;
std::string lan_error_;
std::shared_ptr<lan::Session> lan_connecting_;
bool lan_stopping_ = false;
bool stopping_ = false;
std::chrono::steady_clock::time_point shutdown_report_at_{};
struct LanController {
std::shared_ptr<lan::Session> session;
std::shared_ptr<PeerEventHandler> events;
};
// Protected by remote_sessions_mutex_; snapshots acquire callback leases
// before releasing that lock, so removal cannot race a raw Peer use.
std::map<std::string, LanController> lan_controllers_;
int CreateConnectionPeer(); int CreateConnectionPeer();
int ConnectTo(const std::string& remote_id, const char* password, int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false, bool remember_password, bool bypass_presence_check = false,
@@ -72,6 +103,7 @@ class GuiRuntime : protected gui_detail::GuiState {
const char* reason); const char* reason);
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
void HandlePrivacy(); void HandlePrivacy();
void HandleScreenLock();
bool IsAuthorizedController(const std::string& remote_id); bool IsAuthorizedController(const std::string& remote_id);
void SetControllerPrivacySupport(const std::string& remote_id, bool supported); void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
// Caller holds connection_status_mutex_ exclusively. Turns privacy off when // Caller holds connection_status_mutex_ exclusively. Turns privacy off when
@@ -127,8 +159,10 @@ class GuiRuntime : protected gui_detail::GuiState {
bool unattended_host_ = false; bool unattended_host_ = false;
AnnouncementInbox announcements_; AnnouncementInbox announcements_;
PrivacySessionPolicy privacy_sessions_; PrivacySessionPolicy privacy_sessions_;
DisconnectLockPolicy disconnect_lock_;
std::mutex privacy_commands_mutex_; std::mutex privacy_commands_mutex_;
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_; std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
bool privacy_settings_changed_ = false;
uint32_t last_privacy_revision_ = 0; uint32_t last_privacy_revision_ = 0;
uint64_t last_privacy_status_tick_ = 0; uint64_t last_privacy_status_tick_ = 0;
ClipboardController clipboard_; ClipboardController clipboard_;
@@ -0,0 +1,236 @@
#include <display_stream_id.h>
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#if defined(__linux__) && !defined(__OHOS__)
#include "platform/linux/headless/headless_console.h"
#endif
namespace crossdesk {
void GuiRuntime::RegisterStreams(PeerPtr* peer, size_t displays) {
for (size_t i = 0; i < displays; ++i) {
const auto id = MakeDisplayStreamId(i);
AddVideoStream(peer, id.c_str());
}
AddAudioStream(peer, audio_label_.c_str());
AddDataStream(peer, data_label_.c_str(), false);
for (const auto& label :
{mouse_label_, keyboard_label_, control_data_label_, file_label_,
file_feedback_label_, clipboard_label_})
AddDataStream(peer, label.c_str(), true);
}
void GuiRuntime::InitializeLan() {
if (unattended_host_ || stopping_ || lan_ ||
!config_center_->IsIpDirectEnabled()) return;
#if defined(__linux__) && !defined(__OHOS__)
if (HeadlessConsole::Instance().active()) return;
#endif
lan_error_.clear();
lan::SessionManager::Hooks hooks;
hooks.error = [this](const std::string& error) {
if (!lan_stopping_ && !stopping_) lan_error_ = error;
};
hooks.prepare = [this](const std::shared_ptr<lan::Session>& session) {
if (stopping_ || lan_stopping_ || devices_.EnsureCaptureReady() != 0)
throw std::runtime_error("Capture initialization failed");
const size_t displays = devices_.display_info_list().size();
auto events = std::make_shared<PeerEventHandler>(*this);
events->BindDirect(session);
// Direct initialization must not depend on cloud login/configuration having
// successfully populated the managed peer's Params snapshot.
Params params{};
std::snprintf(params.log_path, sizeof(params.log_path), "%s",
dll_log_path_.c_str());
params.hardware_acceleration = true;
params.native_video_output = true;
params.av1_encoding = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1;
params.enable_srtp = true;
params.turn_mode = TurnMode::TurnDisabled;
params.video_content_type = VideoContentType::ScreenContent;
params.video_quality = VideoQuality::QualityHigh;
params.video_frame_rate = 60;
params.video_degradation_preference =
VideoDegradationPreference::MaintainResolution;
params.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer;
params.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
params.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
params.on_connection_status = PeerEventHandler::OnConnectionStatus;
params.on_net_status_report = PeerEventHandler::OnNetStatusReport;
params.user_id = session->local_id.c_str();
params.user_data = events.get();
if (!session->incoming) {
auto props = FindRemoteSession(session->key);
if (!props) throw std::runtime_error("LAN session cancelled");
props->peer_events_ = events;
props->local_id_ = session->local_id;
}
return [this, session, events, params, displays] {
lan::SessionManager::Prepared result;
result.callback_owner = events;
result.drain_callbacks = [events] { events->Deactivate(); };
result.peer = CreatePeer(&params);
events->SetDirectPeer(result.peer);
if (result.peer) RegisterStreams(result.peer, displays);
return result;
};
};
hooks.attached = [this](const std::shared_ptr<lan::Session>& session) {
auto events =
std::static_pointer_cast<PeerEventHandler>(session->callback_owner);
std::unique_lock lock(remote_sessions_mutex_);
if (session->incoming)
lan_controllers_[session->key] = {session, events};
else if (auto it = remote_sessions_.find(session->key);
it != remote_sessions_.end())
it->second->peer_ = session->peer;
};
hooks.connected = [this](const std::shared_ptr<lan::Session>& session) {
auto events =
std::static_pointer_cast<PeerEventHandler>(session->callback_owner);
events->ApplyConnectionStatus(ConnectionStatus::Connected, session->key);
events->FlushEarlyData();
};
hooks.closed = [this](const std::shared_ptr<lan::Session>& session) {
auto events =
std::static_pointer_cast<PeerEventHandler>(session->callback_owner);
bool attached = !session->incoming;
if (session->incoming) {
std::shared_lock lock(remote_sessions_mutex_);
attached = lan_controllers_.count(session->key) != 0;
}
if (events) {
if (attached)
events->ApplyConnectionStatus(ConnectionStatus::Closed, session->key);
events->StopAccepting();
transfers_.Cancel(events.get());
}
if (session->incoming) {
std::unique_lock lock(remote_sessions_mutex_);
lan_controllers_.erase(session->key);
} else if (auto props = FindRemoteSession(session->key))
CloseRemoteSession(props);
};
try {
lan_ = std::make_unique<lan::SessionManager>(
path_manager_->GetConfigPath() / "lan", std::move(hooks));
} catch (const std::exception& error) {
lan_error_ = error.what();
LOG_ERROR("LAN initialization failed: {}", error.what());
}
}
void GuiRuntime::ApplyLanSettings() {
if (config_center_->IsIpDirectEnabled()) {
InitializeLan();
} else {
lan_connecting_.reset();
lan_error_.clear();
if (lan_ && !lan_stopping_) {
lan_stopping_ = true;
lan_->Stop();
}
}
}
bool GuiRuntime::LanAvailable() const {
return lan_ && !lan_stopping_ && !stopping_;
}
void GuiRuntime::TickLan() {
if (lan_) lan_->Tick();
if (lan_ && lan_stopping_ && lan_->Quiescent()) {
// Drain async authentication, media preparation and callbacks before
// releasing the manager. A quick off/on waits here before rebinding.
lan_.reset();
lan_stopping_ = false;
InitializeLan();
}
}
void GuiRuntime::ConnectLan(const std::string& address, unsigned port,
std::string password, bool remember_password) {
if (!LanAvailable() || (lan_connecting_ && !lan_connecting_->closing))
return;
try {
auto props = std::make_shared<RemoteSession>();
props->remember_password_ = remember_password;
if (remember_password && lan::ValidPassword(password))
std::memcpy(props->remote_password_, password.data(), password.size());
auto session = lan_->Connect(address, port, std::move(password));
lan_connecting_ = session;
props->lan_session_ = session;
props->remote_id_ = session->key;
props->connection_status_ = ConnectionStatus::Connecting;
props->connection_auth_.Complete();
{
std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_[session->key] = props;
}
focused_remote_id_ = session->key;
show_connection_status_window_ = false;
lan_error_.clear();
} catch (const std::exception& error) {
lan_error_ = error.what();
}
}
std::shared_ptr<PeerEventHandler> GuiRuntime::ControllerEvents(
const std::string& id) {
std::shared_lock lock(remote_sessions_mutex_);
auto it = lan_controllers_.find(id);
return it == lan_controllers_.end()
? (id.rfind("lan/", 0) == 0 ? nullptr : peer_events_)
: it->second.events;
}
int GuiRuntime::WithController(
const std::string& id,
const std::function<int(PeerPtr*, const std::string&)>& operation) {
std::shared_lock lock(remote_sessions_mutex_);
auto it = lan_controllers_.find(id);
auto events = it == lan_controllers_.end() ? peer_events_ : it->second.events;
if (it == lan_controllers_.end() && id.rfind("lan/", 0) == 0) return -1;
PeerPtr* peer =
it == lan_controllers_.end() ? peer_ : it->second.session->peer;
if (!peer || !events || !events->DataAllowed()) return -1;
auto callback = events->EnterCallback();
if (!callback) return -1;
const auto wire_id = events->WireId(id);
lock.unlock();
return operation(peer, wire_id);
}
int GuiRuntime::SendControllerData(const std::string& id, const char* data,
size_t size, const char* label) {
return WithController(id, [&](PeerPtr* peer, const std::string& wire) {
return SendReliableDataFrameToPeer(peer, data, size, label, wire.data(),
wire.size());
});
}
std::vector<std::string> GuiRuntime::ConnectedControllers() {
std::vector<std::string> ids;
std::shared_lock lock(connection_status_mutex_);
ids.reserve(connection_status_.size());
for (const auto& [id, status] : connection_status_)
if (status == ConnectionStatus::Connected) ids.push_back(id);
return ids;
}
void GuiRuntime::BeginShutdown() {
if (stopping_) return;
stopping_ = true;
shutdown_report_at_ =
std::chrono::steady_clock::now() + std::chrono::seconds(5);
if (lan_) lan_->Stop();
CloseAllRemoteSessions();
}
bool GuiRuntime::ShutdownDrained() {
TickLan();
HandleSessionCleanup();
const bool drained =
(!lan_ || lan_->Quiescent()) && !connection_peer_cleanup_.valid() &&
session_cleanup_tasks_.empty() && controller_cleanup_tasks_.empty();
if (!drained && std::chrono::steady_clock::now() >= shutdown_report_at_) {
LOG_WARN(
"Shutdown waiting: LAN [{}], cloud peer={}, viewers={}, controllers={}",
lan_ ? lan_->PendingWork() : "none", connection_peer_cleanup_.valid(),
session_cleanup_tasks_.size(), controller_cleanup_tasks_.size());
shutdown_report_at_ =
std::chrono::steady_clock::now() + std::chrono::seconds(5);
}
return drained;
}
} // namespace crossdesk
@@ -84,17 +84,52 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
} // namespace } // namespace
bool PeerEventHandler::QueueEarlyData(const char* data, size_t size,
const char* label, size_t label_size) {
if (!direct_bound_) return false;
auto session = direct_.lock();
if (!session || session->closing) return true;
std::lock_guard lock(early_data_mutex_);
if (session->authorized) return false;
// Keep bounded reliable metadata until the manager commits Connected. Mouse,
// keyboard, clipboard and files never enter this pre-authorization queue.
if (std::string_view(label, label_size) != owner_.control_data_label_)
return true;
if (size > 8192 || early_data_.size() >= 16 ||
early_data_bytes_ + size > 65536) {
session->failed = true;
return true;
}
early_data_bytes_ += size;
early_data_.emplace_back(data, size);
return true;
}
void PeerEventHandler::FlushEarlyData() {
std::vector<std::string> queued;
{
std::lock_guard lock(early_data_mutex_);
queued.swap(early_data_);
early_data_bytes_ = 0;
}
const auto session = direct_.lock();
if (!session || !session->authorized || session->closing) return;
const auto& label = owner_.control_data_label_;
for (const auto& data : queued)
OnReceiveDataBuffer(data.data(), data.size(), session->remote_id.data(),
session->remote_id.size(), label.data(), label.size(),
this);
}
void PeerEventHandler::OnReceiveDataBuffer( void PeerEventHandler::OnReceiveDataBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size, const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) { const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data); auto *handler = static_cast<PeerEventHandler *>(user_data);
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback || handler->QueueEarlyData(data, size, src_id, src_id_size) ||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; !handler->DataAllowed())
if (!runtime) {
return; return;
} GuiRuntime* runtime = &handler->owner_;
std::string source_id = std::string(src_id, src_id_size); std::string source_id = std::string(src_id, src_id_size);
// The service-owned host runs as SYSTEM. Do not expose filesystem or // The service-owned host runs as SYSTEM. Do not expose filesystem or
@@ -105,11 +140,13 @@ void PeerEventHandler::OnReceiveDataBuffer(
source_id == runtime->clipboard_label_)) source_id == runtime->clipboard_label_))
return; return;
if (source_id == runtime->file_label_) { if (source_id == runtime->file_label_) {
runtime->transfers_.Receive(data, size, handler, std::string(user_id, user_id_size)); runtime->transfers_.Receive(data, size, handler,
handler->ConnectionKey(user_id, user_id_size));
return; return;
} else if (source_id == runtime->clipboard_label_) { } else if (source_id == runtime->clipboard_label_) {
if (size > 0) { if (size > 0) {
std::string remote_user_id(user_id, user_id_size); std::string remote_user_id =
handler->ConnectionKey(user_id, user_id_size);
auto props = auto props =
runtime->FindRemoteSession(remote_user_id); runtime->FindRemoteSession(remote_user_id);
if (props && !props->clipboard_allowed_.load()) { if (props && !props->clipboard_allowed_.load()) {
@@ -121,11 +158,15 @@ void PeerEventHandler::OnReceiveDataBuffer(
} }
return; return;
} else if (source_id == runtime->file_feedback_label_) { } else if (source_id == runtime->file_feedback_label_) {
runtime->transfers_.HandleAck(data, size, handler, std::string(user_id, user_id_size)); runtime->transfers_.HandleAck(
data, size, handler, handler->ConnectionKey(user_id, user_id_size));
return; return;
} }
const std::string remote_id(user_id, user_id_size); const std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
const std::string wire_id = handler->WireId(remote_id);
PeerPtr* bound_peer =
handler->IsDirect() ? handler->DirectPeer() : runtime->peer_;
std::string json_str(data, size); std::string json_str(data, size);
const auto preview_json = const auto preview_json =
json_str.find("\"deskhub_preview") != std::string::npos json_str.find("\"deskhub_preview") != std::string::npos
@@ -145,29 +186,31 @@ void PeerEventHandler::OnReceiveDataBuffer(
if (!previous || subscription->IsNewerThan(*previous)) { if (!previous || subscription->IsNewerThan(*previous)) {
if (subscription->fps > 0) if (subscription->fps > 0)
accepted = UpdateVideoSettings( accepted = UpdateVideoSettings(
runtime->peer_, remote_id.data(), remote_id.size(), bound_peer, wire_id.data(), wire_id.size(),
static_cast<VideoQuality>(subscription->quality), static_cast<VideoQuality>(subscription->quality),
subscription->frame_rate, subscription->frame_rate,
static_cast<VideoDegradationPreference>( static_cast<VideoDegradationPreference>(
subscription->EncoderPreference())) == 0; subscription->EncoderPreference())) == 0;
if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) != if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) !=
subscription->max_bitrate_bps.value_or(0))) subscription->max_bitrate_bps.value_or(0)))
accepted = UpdateVideoBitrateLimit( accepted = UpdateVideoBitrateLimit(
runtime->peer_, remote_id.data(), remote_id.size(), bound_peer, wire_id.data(), wire_id.size(),
subscription->max_bitrate_bps.value_or(0)) == 0; subscription->max_bitrate_bps.value_or(0)) == 0;
// Rebalancing bandwidth alone must not force every device to send a // Rebalancing bandwidth alone must not force every device to send a
// keyframe. The pause API requests its own keyframes on resume. // keyframe. The pause API requests its own keyframes on resume.
if (accepted && (!previous || previous->width != subscription->width || if (accepted && (!previous || previous->width != subscription->width ||
previous->height != subscription->height)) previous->height != subscription->height))
accepted = UpdateVideoResolution( accepted = UpdateVideoResolution(bound_peer, wire_id.data(),
runtime->peer_, remote_id.data(), remote_id.size(), wire_id.size(), subscription->width,
subscription->width, subscription->height) == 0; subscription->height) == 0;
if (accepted) if (accepted)
accepted = SetVideoSendPaused(runtime->peer_, remote_id.data(), accepted =
remote_id.size(), subscription->fps == 0) == 0; SetVideoSendPaused(bound_peer, wire_id.data(), wire_id.size(),
subscription->fps == 0) == 0;
if (accepted) if (accepted)
accepted = SetAudioSendPaused(runtime->peer_, remote_id.data(), accepted =
remote_id.size(), !subscription->audio) == 0; SetAudioSendPaused(bound_peer, wire_id.data(), wire_id.size(),
!subscription->audio) == 0;
if (accepted && subscription->audio) if (accepted && subscription->audio)
runtime->devices_.StartSpeakerCapturer(); runtime->devices_.StartSpeakerCapturer();
} }
@@ -178,9 +221,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
{"request_id", subscription->request_id}, {"request_id", subscription->request_id},
{"accepted", {"accepted",
accepted}}.dump(); accepted}}.dump();
SendReliableDataFrameToPeer(runtime->peer_, reply.data(), reply.size(), SendReliableDataFrameToPeer(bound_peer, reply.data(), reply.size(),
runtime->control_data_label_.c_str(), runtime->control_data_label_.c_str(),
remote_id.data(), remote_id.size()); wire_id.data(), wire_id.size());
return; return;
} }
if (preview_json.is_object() && if (preview_json.is_object() &&
@@ -253,7 +296,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
const auto& settings = remote_action.vs; const auto& settings = remote_action.vs;
remote_action.vs.accepted = remote_action.vs.accepted =
UpdateVideoSettings( UpdateVideoSettings(
runtime->peer_, remote_id.data(), remote_id.size(), bound_peer, wire_id.data(), wire_id.size(),
static_cast<VideoQuality>(settings.quality), settings.frame_rate, static_cast<VideoQuality>(settings.quality), settings.frame_rate,
static_cast<VideoDegradationPreference>(settings.preference)) == 0; static_cast<VideoDegradationPreference>(settings.preference)) == 0;
LOG_INFO("Video settings: peer={} source={} request={} quality={} fps={} " LOG_INFO("Video settings: peer={} source={} request={} quality={} fps={} "
@@ -262,9 +305,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
settings.frame_rate, settings.preference, remote_action.vs.accepted); settings.frame_rate, settings.preference, remote_action.vs.accepted);
remote_action.type = ControlType::video_settings_status; remote_action.type = ControlType::video_settings_status;
const auto message = remote_action.to_json(); const auto message = remote_action.to_json();
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(), SendReliableDataFrameToPeer(bound_peer, message.data(), message.size(),
runtime->control_data_label_.c_str(), runtime->control_data_label_.c_str(),
remote_id.data(), remote_id.size()); wire_id.data(), wire_id.size());
return; return;
} }
if (remote_action.type == ControlType::privacy_status) { if (remote_action.type == ControlType::privacy_status) {
@@ -273,6 +316,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
std::lock_guard lock(props->privacy_status_mutex_); std::lock_guard lock(props->privacy_status_mutex_);
if (!props->privacy_status_received_ || if (!props->privacy_status_received_ ||
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) { static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
if (props->privacy_status_received_ &&
static_cast<int32_t>(remote_action.ps.local_unlock_revision -
props->privacy_status_.local_unlock_revision) > 0)
props->privacy_unlock_notice_ = true;
if (remote_action.ps.state == PrivacyState::on)
props->privacy_unlock_notice_ = false;
props->privacy_status_ = remote_action.ps; props->privacy_status_ = remote_action.ps;
props->privacy_status_received_ = true; props->privacy_status_received_ = true;
props->privacy_status_tick_ = SDL_GetTicks(); props->privacy_status_tick_ = SDL_GetTicks();
@@ -407,8 +456,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
props->video_settings_request_tick_ = SDL_GetTicks(); props->video_settings_request_tick_ = SDL_GetTicks();
settings.vs = props->video_settings_; settings.vs = props->video_settings_;
const auto message = settings.to_json(); const auto message = settings.to_json();
SendReliableDataFrame(props->peer_, message.data(), message.size(), SendReliableDataFrame(
props->control_data_label_.c_str()); handler->IsDirect() ? handler->DirectPeer() : props->peer_,
message.data(), message.size(),
props->control_data_label_.c_str());
} }
} }
// client mode // client mode
@@ -456,8 +507,8 @@ void PeerEventHandler::OnReceiveDataBuffer(
// command arrives after it (e.g. when unmuting an inactive window). // command arrives after it (e.g. when unmuting an inactive window).
const auto subscription = runtime->preview_subscriptions_.Get(remote_id); const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
const bool paused = subscription ? !subscription->audio : !remote_action.a; const bool paused = subscription ? !subscription->audio : !remote_action.a;
const int ret = SetAudioSendPaused(runtime->peer_, remote_id.data(), const int ret = SetAudioSendPaused(bound_peer, wire_id.data(),
remote_id.size(), paused); wire_id.size(), paused);
if (remote_action.a) if (remote_action.a)
runtime->devices_.StartSpeakerCapturer(); runtime->devices_.StartSpeakerCapturer();
else if (ret != 0) else if (ret != 0)
@@ -49,6 +49,22 @@ constexpr HostPlatform kClientPlatform = HostPlatform::Unknown;
} // namespace } // namespace
std::string PeerEventHandler::ConnectionKey(const char* id, size_t size) const {
if (auto session = direct_.lock()) return session->key;
return direct_bound_ ? std::string{} : std::string(id, size);
}
std::string PeerEventHandler::WireId(const std::string& key) const {
if (auto session = direct_.lock()) return session->remote_id;
return key;
}
bool PeerEventHandler::DataAllowed() const {
if (!direct_bound_) return true;
auto session = direct_.lock();
return session && session->authorized && !session->closing;
}
void PeerEventHandler::SendClientInfo(PeerPtr* peer, void PeerEventHandler::SendClientInfo(PeerPtr* peer,
const std::string& client_id) { const std::string& client_id) {
if (!peer) { if (!peer) {
@@ -74,8 +90,8 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = &handler->owner_;
if (!runtime || !message || size == 0) { if (!message || size == 0) {
return; return;
} }
std::string s(message, size); std::string s(message, size);
@@ -179,10 +195,7 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = &handler->owner_;
if (!runtime) {
return;
}
std::string client_id(user_id, user_id_size); std::string client_id(user_id, user_id_size);
if (client_id == runtime->client_id_) { if (client_id == runtime->client_id_) {
@@ -251,17 +264,30 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
} }
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char* user_id, const char* user_id, size_t size,
const size_t user_id_size, void* context) {
void* user_data) { auto* handler = static_cast<PeerEventHandler*>(context);
if (!handler) return;
if (handler->direct_bound_) {
auto callback = handler->EnterCallback();
if (callback)
if (auto session = handler->direct_.lock()) session->Status(status);
return;
}
DispatchConnectionStatus(status, user_id, size, context);
}
void PeerEventHandler::DispatchConnectionStatus(ConnectionStatus status,
const char* user_id,
const size_t user_id_size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = &handler->owner_;
if (!runtime) return;
std::string remote_id(user_id, user_id_size); std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
if (status == ConnectionStatus::Connected) runtime->transfers_.Connected(handler, remote_id); if (status == ConnectionStatus::Connected) runtime->transfers_.Connected(handler, remote_id);
else if (status == ConnectionStatus::Disconnected || status == ConnectionStatus::Failed || else if (status == ConnectionStatus::Disconnected || status == ConnectionStatus::Failed ||
status == ConnectionStatus::Closed) runtime->transfers_.Cancel(handler, remote_id); status == ConnectionStatus::Closed) runtime->transfers_.Cancel(handler, remote_id);
@@ -432,6 +458,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{ {
std::unique_lock lock(runtime->connection_status_mutex_); std::unique_lock lock(runtime->connection_status_mutex_);
if (status == ConnectionStatus::Connected) { if (status == ConnectionStatus::Connected) {
runtime->disconnect_lock_.Connected(remote_id);
// Local privacy requires explicit desktop capability. Host info may // Local privacy requires explicit desktop capability. Host info may
// arrive before Connected; consume that saved capability here. // arrive before Connected; consume that saved capability here.
const bool automatic_enable = runtime->privacy_sessions_.Connected( const bool automatic_enable = runtime->privacy_sessions_.Connected(
@@ -474,8 +501,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
} }
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
const int ret = const int ret =
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(), runtime->SendControllerData(remote_id, msg.data(), msg.size(),
runtime->control_data_label_.c_str()); runtime->control_data_label_.c_str());
if (ret != 0) { if (ret != 0) {
LOG_WARN("[{}] host information send failed, ret={}", remote_id, LOG_WARN("[{}] host information send failed, ret={}", remote_id,
ret); ret);
@@ -516,21 +543,12 @@ void PeerEventHandler::OnNetStatusReport(
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = &handler->owner_;
if (!runtime) {
return;
}
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) { if (const char* at_pos = handler->IsDirect() ? nullptr : strchr(client_id, '@');
std::string id, password; at_pos && strchr(user_id, '-') == nullptr) {
const char* at_pos = strchr(client_id, '@'); const std::string id(client_id, at_pos - client_id);
if (at_pos == nullptr) { const std::string password(at_pos + 1);
id = client_id;
password.clear();
} else {
id.assign(client_id, at_pos - client_id);
password = at_pos + 1;
}
bool is_self_hosted = runtime->config_center_->IsSelfHosted(); bool is_self_hosted = runtime->config_center_->IsSelfHosted();
@@ -578,7 +596,7 @@ void PeerEventHandler::OnNetStatusReport(
#endif #endif
} }
std::string remote_id(user_id, user_id_size); std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
auto props = runtime->FindRemoteSession(remote_id); auto props = runtime->FindRemoteSession(remote_id);
if (!props) { if (!props) {
bool report_codecs = false; bool report_codecs = false;
@@ -597,9 +615,8 @@ void PeerEventHandler::OnNetStatusReport(
net_traffic_stats->video_encoder); net_traffic_stats->video_encoder);
const auto message = action.to_json(); const auto message = action.to_json();
// Report only to this controller, after releasing the status lock. // Report only to this controller, after releasing the status lock.
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(), runtime->SendControllerData(remote_id, message.data(), message.size(),
runtime->control_data_label_.c_str(), runtime->control_data_label_.c_str());
remote_id.data(), remote_id.size());
} }
return; return;
} }
@@ -8,7 +8,10 @@
#define _PEER_EVENT_HANDLER_H_ #define _PEER_EVENT_HANDLER_H_
#include <cstddef> #include <cstddef>
#include <memory>
#include <mutex>
#include <string> #include <string>
#include <vector>
#include "minirtc.h" #include "minirtc.h"
#include "runtime/session_lifecycle.h" #include "runtime/session_lifecycle.h"
@@ -16,12 +19,30 @@
namespace crossdesk { namespace crossdesk {
class GuiRuntime; class GuiRuntime;
namespace lan {
struct Session;
}
// Adapts MiniRTC C callbacks to the GUI runtime. // Adapts MiniRTC C callbacks to the GUI runtime.
class PeerEventHandler { class PeerEventHandler {
public: public:
explicit PeerEventHandler(GuiRuntime &owner); explicit PeerEventHandler(GuiRuntime &owner);
void BindDirect(const std::shared_ptr<lan::Session>& session) {
direct_ = session;
direct_bound_ = true;
}
std::string ConnectionKey(const char* id, size_t size) const;
std::string WireId(const std::string& key) const;
bool DataAllowed() const;
bool IsDirect() const { return direct_bound_; }
void SetDirectPeer(PeerPtr* peer) { direct_peer_ = peer; }
PeerPtr* DirectPeer() const { return direct_peer_; }
void FlushEarlyData();
void ApplyConnectionStatus(ConnectionStatus status, const std::string& id) {
DispatchConnectionStatus(status, id.data(), id.size(), this);
}
void StopAccepting() { callbacks_.StopAccepting(); }
bool IsActive() const { return callbacks_.IsActive(); } bool IsActive() const { return callbacks_.IsActive(); }
auto EnterCallback() { return callbacks_.Enter(); } auto EnterCallback() { return callbacks_.Enter(); }
void Deactivate() { callbacks_.Close(); } void Deactivate() { callbacks_.Close(); }
@@ -51,12 +72,22 @@ public:
void *user_data); void *user_data);
private: private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id); static void DispatchConnectionStatus(ConnectionStatus status, const char* id,
size_t size, void* context);
static void SendClientInfo(PeerPtr* peer, const std::string& client_id);
bool QueueEarlyData(const char* data, size_t size, const char* label,
size_t label_size);
std::weak_ptr<lan::Session> direct_;
bool direct_bound_ = false;
PeerPtr* direct_peer_ = nullptr;
std::mutex early_data_mutex_;
std::vector<std::string> early_data_;
size_t early_data_bytes_ = 0;
GuiRuntime &owner_; GuiRuntime& owner_;
gui_detail::SessionCallbackGate callbacks_; gui_detail::SessionCallbackGate callbacks_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif
@@ -38,20 +38,21 @@ void PeerEventHandler::OnReceiveVideoBuffer(
auto* handler = static_cast<PeerEventHandler*>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback || !handler->DataAllowed()) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = &handler->owner_;
if (!runtime || !video_frame) { if (!video_frame) {
return; return;
} }
std::string remote_id(user_id, user_id_size); std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
auto props = runtime->FindRemoteSession(remote_id); auto props = runtime->FindRemoteSession(remote_id);
if (!props) return; if (!props) return;
if (props->connection_established_) { if (props->connection_established_) {
const auto timing_now = VideoLatencyFrame::Clock::now(); const auto timing_now = VideoLatencyFrame::Clock::now();
// Pair the clocks before preview conversion so its cost is included. // Pair the clocks before preview conversion so its cost is included.
const auto local_now_us = GetSystemTimeMicros(props->peer_); const auto local_now_us = GetSystemTimeMicros(
handler->IsDirect() ? handler->DirectPeer() : props->peer_);
const auto* received_native = GetNativeVideoFrame(video_frame); const auto* received_native = GetNativeVideoFrame(video_frame);
props->received_video_stats_.Record( props->received_video_stats_.Record(
received_native ? received_native->width : video_frame->width, received_native ? received_native->width : video_frame->width,
@@ -274,15 +275,15 @@ void PeerEventHandler::OnReceiveAudioBuffer(
auto* handler = static_cast<PeerEventHandler*>(user_data); auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return; if (!handler) return;
auto callback = handler->EnterCallback(); auto callback = handler->EnterCallback();
if (!callback) return; if (!callback || !handler->DataAllowed()) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr; GuiRuntime* runtime = &handler->owner_;
if (!runtime || runtime->unattended_host_) { if (runtime->unattended_host_) {
return; return;
} }
std::lock_guard delivery_lock(runtime->audio_delivery_mutex_); std::lock_guard delivery_lock(runtime->audio_delivery_mutex_);
const auto session = const auto session =
runtime->FindRemoteSession(std::string(user_id, user_id_size)); runtime->FindRemoteSession(handler->ConnectionKey(user_id, user_id_size));
if (!session || session->closing_ || !session->audio_allowed_.load()) return; if (!session || session->closing_ || !session->audio_allowed_.load()) return;
runtime->audio_buffer_fresh_ = true; runtime->audio_buffer_fresh_ = true;
@@ -10,11 +10,16 @@
#include <shared_mutex> #include <shared_mutex>
#include "rd_log.h" #include "rd_log.h"
#include "localization.h"
#include "runtime/gui_runtime.h" #include "runtime/gui_runtime.h"
namespace crossdesk { namespace crossdesk {
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) { bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
if (remote_id.rfind("lan/", 0) == 0) {
const auto events = ControllerEvents(remote_id);
if (!events || !events->DataAllowed()) return false;
}
// Connections appear here only after the existing password/signalling/RTC // Connections appear here only after the existing password/signalling/RTC
// admission path. Outgoing/viewer sessions live in remote_sessions_ instead. // admission path. Outgoing/viewer sessions live in remote_sessions_ instead.
std::shared_lock lock(connection_status_mutex_); std::shared_lock lock(connection_status_mutex_);
@@ -67,6 +72,14 @@ void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
} }
void GuiRuntime::HandlePrivacy() { void GuiRuntime::HandlePrivacy() {
const auto sync_text = [this] {
const int language = localization::detail::ClampLanguageIndex(
static_cast<int>(config_center_->GetLanguage()));
privacy_.SetText({localization::privacy_screen_unlock_hint[language],
localization::privacy_screen_locked_hint[language],
config_center_->IsPrivacyLocalUnlockAllowed()});
};
sync_text();
std::deque<std::pair<std::string, PrivacyCommand>> commands; std::deque<std::pair<std::string, PrivacyCommand>> commands;
{ {
std::lock_guard lock(privacy_commands_mutex_); std::lock_guard lock(privacy_commands_mutex_);
@@ -83,6 +96,16 @@ void GuiRuntime::HandlePrivacy() {
if (command.flag == PrivacyCommandFlag::enable) { if (command.flag == PrivacyCommandFlag::enable) {
if (privacy_sessions_.CanEnable()) if (privacy_sessions_.CanEnable())
privacy_.Enable(command.block_local_input); privacy_.Enable(command.block_local_input);
} else if (command.flag == PrivacyCommandFlag::lock) {
if (privacy_sessions_.CanEnable()) {
if (config_center_->SetPrivacyLocalUnlock(false) == 0) {
privacy_settings_changed_ = true;
sync_text();
privacy_.Enable(true, true);
} else {
privacy_.RejectCommand("Could not save the local privacy unlock setting; try again");
}
}
} else if (command.flag == PrivacyCommandFlag::disable) { } else if (command.flag == PrivacyCommandFlag::disable) {
privacy_.Disable(); privacy_.Disable();
} }
@@ -109,9 +132,9 @@ void GuiRuntime::HandlePrivacy() {
"A connected controller does not support privacy screen"); "A connected controller does not support privacy screen");
} }
} }
if (!peer_ || (last_privacy_status_tick_ != 0 && if (last_privacy_status_tick_ != 0 &&
status.revision == last_privacy_revision_ && status.revision == last_privacy_revision_ &&
now - last_privacy_status_tick_ < 1000)) now - last_privacy_status_tick_ < 1000)
return; return;
RemoteAction action{}; RemoteAction action{};
action.type = ControlType::privacy_status; action.type = ControlType::privacy_status;
@@ -119,9 +142,8 @@ void GuiRuntime::HandlePrivacy() {
const auto message = action.to_json(); const auto message = action.to_json();
bool sent = true; bool sent = true;
for (const auto& id : controllers) { for (const auto& id : controllers) {
const int result = SendReliableDataFrameToPeer( const int result = SendControllerData(id, message.data(), message.size(),
peer_, message.data(), message.size(), control_data_label_.c_str(), control_data_label_.c_str());
id.data(), id.size());
if (result != 0) { if (result != 0) {
sent = false; sent = false;
LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result); LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result);
@@ -31,7 +31,12 @@
#include "runtime/received_video_stats.h" #include "runtime/received_video_stats.h"
#include "runtime/connection_auth_state.h" #include "runtime/connection_auth_state.h"
namespace crossdesk { class PeerEventHandler; } namespace crossdesk {
class PeerEventHandler;
namespace lan {
struct Session;
}
} // namespace crossdesk
namespace crossdesk::gui_detail { namespace crossdesk::gui_detail {
@@ -105,6 +110,7 @@ struct RemoteSession {
// Decoder-thread scratch memory, never shared with the UI. // Decoder-thread scratch memory, never shared with the UI.
std::mutex gallery_conversion_mutex_; std::mutex gallery_conversion_mutex_;
std::vector<unsigned char> gallery_nv12_, gallery_scaled_, gallery_scratch_; std::vector<unsigned char> gallery_nv12_, gallery_scaled_, gallery_scratch_;
std::shared_ptr<lan::Session> lan_session_;
Params params_; Params params_;
PeerPtr* peer_ = nullptr; PeerPtr* peer_ = nullptr;
std::shared_ptr<PeerEventHandler> peer_events_; std::shared_ptr<PeerEventHandler> peer_events_;
@@ -187,6 +193,8 @@ struct RemoteSession {
std::mutex privacy_status_mutex_; std::mutex privacy_status_mutex_;
PrivacyStatus privacy_status_{}; PrivacyStatus privacy_status_{};
bool privacy_status_received_ = false; bool privacy_status_received_ = false;
bool privacy_unlock_notice_ = false;
bool privacy_desktop_available_ = true;
bool privacy_command_pending_ = false; bool privacy_command_pending_ = false;
uint64_t privacy_command_tick_ = 0; uint64_t privacy_command_tick_ = 0;
uint32_t privacy_request_revision_ = 0; uint32_t privacy_request_revision_ = 0;
@@ -59,8 +59,6 @@ struct RecentConnectionsState {
recent_connections_; recent_connections_;
std::vector<std::string> recent_connection_ids_; std::vector<std::string> recent_connection_ids_;
std::vector<std::string> gallery_presence_ids_; std::vector<std::string> gallery_presence_ids_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0; uint32_t recent_connection_image_save_time_ = 0;
DevicePresenceCache device_presence_cache_; DevicePresenceCache device_presence_cache_;
std::atomic<bool> need_to_send_recent_connections_{true}; std::atomic<bool> need_to_send_recent_connections_{true};
@@ -150,13 +148,11 @@ struct UserSettingsState {
char connection_login_identity_[17] = ""; char connection_login_identity_[17] = "";
int language_button_value_ = 0; int language_button_value_ = 0;
int video_encode_format_button_value_ = 0; int video_encode_format_button_value_ = 0;
bool enable_turn_ = true;
char signal_server_ip_[256] = "api.crossdesk.cn"; char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099"; char signal_server_port_[6] = "9099";
bool enable_self_hosted_ = false; bool enable_self_hosted_ = false;
int language_button_value_last_ = 0; int language_button_value_last_ = 0;
int video_encode_format_button_value_last_ = 0; int video_encode_format_button_value_last_ = 0;
bool enable_turn_last_ = true;
bool enable_self_hosted_last_ = false; bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false; bool enable_autostart_ = false;
bool enable_autostart_last_ = false; bool enable_autostart_last_ = false;
@@ -37,6 +37,11 @@ void SessionCallbackGate::EndCallback() {
} }
} }
void SessionCallbackGate::StopAccepting() {
std::lock_guard lock(mutex_);
accepting_callbacks_ = false;
}
void SessionCallbackGate::Close() { void SessionCallbackGate::Close() {
std::unique_lock lock(mutex_); std::unique_lock lock(mutex_);
accepting_callbacks_ = false; accepting_callbacks_ = false;
@@ -39,6 +39,7 @@ class SessionCallbackGate {
CallbackGuard Enter(); CallbackGuard Enter();
// Called by the session owner, never from within this session's callbacks. // Called by the session owner, never from within this session's callbacks.
void StopAccepting();
void Close(); void Close();
private: private:
+26 -25
View File
@@ -1,9 +1,10 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { ScrollView, TextEdit } from "std-widgets.slint"; import { ScrollView, TextEdit } from "std-widgets.slint";
import { SettingsDialog, SettingsButton } from "settings_controls.slint"; import { SettingsDialog, SettingsButton } from "settings_controls.slint";
import { WorkbenchStyle } from "workbench_controls.slint"; import { WorkbenchStyle } from "workbench_controls.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { GalleryStrings } from "device_strings.slint"; import { GalleryStrings } from "device_strings.slint";
import { FontAwesomeIcons } from "common.slint"; import { FontAwesomeIcons, ControlStyle } from "common.slint";
import "../assets/fonts/message_spacing.ttf"; import "../assets/fonts/message_spacing.ttf";
export enum AboutPage { overview, components, component, document, source } export enum AboutPage { overview, components, component, document, source }
@@ -18,19 +19,19 @@ export struct AboutComponent {
} }
export global AboutStrings { export global AboutStrings {
out property <string> software-license: GalleryStrings.tr("软件许可", "Software license", "Лицензия программы"); out property <string> software-license: GalleryStrings.texts[130];
out property <string> components: GalleryStrings.tr("开源组件", "Open-source components", "Компоненты с открытым кодом"); out property <string> components: GalleryStrings.texts[131];
out property <string> source: GalleryStrings.tr("源码与构建说明", "Source code and build instructions", "Исходный код и сборка"); out property <string> source: GalleryStrings.texts[132];
out property <string> notices: GalleryStrings.tr("许可与版权声明", "Licenses and copyright notices", "Лицензии и авторские права"); out property <string> notices: GalleryStrings.texts[133];
out property <string> view-source: GalleryStrings.tr("查看源码", "View source code", "Открыть исходный код"); out property <string> view-source: GalleryStrings.texts[134];
out property <string> back: GalleryStrings.tr("返回", "Back", "Назад"); out property <string> back: GalleryStrings.texts[135];
} }
export component AboutNavigationGroup inherits Rectangle { export component AboutNavigationGroup inherits FlatSurface {
height: rows.preferred-height + 2px; height: rows.preferred-height + 2px;
background: WorkbenchStyle.surface; background: WorkbenchStyle.surface;
border-radius: UiRadius.control; border-radius: UiRadius.control;
border-width: 1px; border-color: WorkbenchStyle.separator; outline-color: UiSurface.edge;
clip: true; clip: true;
rows := VerticalLayout { rows := VerticalLayout {
x: 1px; y: 1px; width: parent.width - 2px; x: 1px; y: 1px; width: parent.width - 2px;
@@ -41,13 +42,13 @@ export component AboutNavigationGroup inherits Rectangle {
export component AboutRowSeparator inherits Rectangle { export component AboutRowSeparator inherits Rectangle {
height: 1px; height: 1px;
Rectangle { SurfaceSeparator {
x: 14px; width: parent.width - 30px; height: 1px; x: 14px; width: parent.width - 30px; height: 1px;
background: WorkbenchStyle.separator; outline-color: UiSurface.separator;
} }
} }
export component AboutNavigationRow inherits Rectangle { export component AboutNavigationRow inherits FlatSurface {
in property <string> title; in property <string> title;
in property <string> subtitle; in property <string> subtitle;
in property <string> icon; in property <string> icon;
@@ -60,7 +61,7 @@ export component AboutNavigationRow inherits Rectangle {
background: touch.pressed ? WorkbenchStyle.pressed-surface background: touch.pressed ? WorkbenchStyle.pressed-surface
: touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface; : touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface;
border-radius: root.grouped ? 0px : UiRadius.control; border-radius: root.grouped ? 0px : UiRadius.control;
border-width: root.grouped ? 0px : 1px; border-color: WorkbenchStyle.separator; outline-enabled: !root.grouped; outline-color: UiSurface.edge;
accessible-role: button; accessible-role: button;
accessible-label: root.title + (root.subtitle != "" ? ", " + root.subtitle : "") accessible-label: root.title + (root.subtitle != "" ? ", " + root.subtitle : "")
+ (root.detail != "" ? ", " + root.detail : ""); + (root.detail != "" ? ", " + root.detail : "");
@@ -69,10 +70,10 @@ export component AboutNavigationRow inherits Rectangle {
x: 12px; y: (parent.height - self.height) / 2; x: 12px; y: (parent.height - self.height) / 2;
width: 28px; height: 28px; width: 28px; height: 28px;
border-radius: UiRadius.control; border-radius: UiRadius.control;
background: WorkbenchStyle.field-surface; background: ControlStyle.surface;
Text { Text {
text: root.icon; font-family: "Font Awesome 6 Free"; font-weight: 900; text: root.icon; font-family: "Font Awesome 6 Free"; font-weight: 900;
font-size: 13px; color: WorkbenchStyle.muted; font-size: 13px; color: ControlStyle.icon;
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
accessible-role: none; accessible-role: none;
} }
@@ -136,7 +137,7 @@ export component AboutPanel inherits SettingsDialog {
if root.page == AboutPage.components: VerticalLayout { if root.page == AboutPage.components: VerticalLayout {
padding: 0px; spacing: 10px; padding: 0px; spacing: 10px;
Text { Text {
text: GalleryStrings.tr("以下列出本版本的主要开源组件。各组件遵循各自的开源许可。", "Main open-source components used in this build. Each component is covered by its own license.", "Основные открытые компоненты этой сборки. Каждый распространяется по своей лицензии."); text: GalleryStrings.texts[136];
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
} }
Rectangle { Rectangle {
@@ -145,9 +146,9 @@ export component AboutPanel inherits SettingsDialog {
notice := VerticalLayout { notice := VerticalLayout {
x: 16px; y: 16px; width: parent.width - 32px; x: 16px; y: 16px; width: parent.width - 32px;
height: self.preferred-height; spacing: 12px; height: self.preferred-height; spacing: 12px;
Text { text: GalleryStrings.tr("OpenFEC 告知", "OpenFEC notice", "Уведомление OpenFEC"); color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; } Text { text: GalleryStrings.texts[137]; color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; }
Text { Text {
text: GalleryStrings.tr("本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。", "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.", "Приложение использует OpenFEC для прямого исправления ошибок по лицензии CeCILL-C 1.0 с ограниченной гарантией и ответственностью. Подробности — в тексте лицензии."); text: GalleryStrings.texts[138];
color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap; color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap;
font-family: "CrossDesk Message Spacing"; font-family: "CrossDesk Message Spacing";
} }
@@ -157,7 +158,7 @@ export component AboutPanel inherits SettingsDialog {
font-family: "CrossDesk Message Spacing"; font-family: "CrossDesk Message Spacing";
} }
if root.openfec-index >= 0: AboutNavigationRow { if root.openfec-index >= 0: AboutNavigationRow {
title: GalleryStrings.tr("OpenFEC 许可与源码", "OpenFEC license and source", "Лицензия и исходный код OpenFEC"); title: GalleryStrings.texts[139];
clicked => { root.component-index = root.openfec-index; root.page = AboutPage.component; } clicked => { root.component-index = root.openfec-index; root.page = AboutPage.component; }
} }
} }
@@ -184,24 +185,24 @@ export component AboutPanel inherits SettingsDialog {
clicked => { root.document-index = index; root.page = AboutPage.document; } clicked => { root.document-index = index; root.page = AboutPage.document; }
} }
if root.selected.documents.length == 0: Text { if root.selected.documents.length == 0: Text {
text: GalleryStrings.tr("本组件未内置许可文本,请通过“查看源码”查阅许可与版权声明。", "License text is not bundled for this component. Use View source code to read its license and copyright notices.", "Текст лицензии не включён. Откройте исходный код для просмотра лицензии и авторских прав."); text: GalleryStrings.texts[140];
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
} }
} }
if root.page == AboutPage.source: VerticalLayout { if root.page == AboutPage.source: VerticalLayout {
padding: 0px; spacing: 12px; padding: 0px; spacing: 12px;
Text { Text {
text: root.revision != "" ? GalleryStrings.tr("源码版本:", "Source revision: ", "Ревизия исходного кода: ") + root.revision text: root.revision != "" ? GalleryStrings.texts[141] + root.revision
: GalleryStrings.tr("本版本源码信息暂不可用。", "Source revision is unavailable for this build.", "Ревизия исходного кода этой сборки недоступна."); : GalleryStrings.texts[142];
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
} }
AboutNavigationRow { title: AboutStrings.view-source; external: true; clicked => { root.open-link(root.source-url); } } AboutNavigationRow { title: AboutStrings.view-source; external: true; clicked => { root.open-link(root.source-url); } }
AboutNavigationRow { AboutNavigationRow {
title: GalleryStrings.tr("构建说明", "Build instructions", "Инструкции по сборке"); external: true; title: GalleryStrings.texts[143]; external: true;
clicked => { root.open-link(root.build-url); } clicked => { root.open-link(root.build-url); }
} }
AboutNavigationRow { AboutNavigationRow {
title: GalleryStrings.tr("项目主页", "Project website", "Сайт проекта"); external: true; title: GalleryStrings.texts[144]; external: true;
clicked => { root.open-link("https://crossdesk.cn"); } clicked => { root.open-link("https://crossdesk.cn"); }
} }
} }
@@ -226,7 +227,7 @@ export component AboutPanel inherits SettingsDialog {
footer := VerticalLayout { footer := VerticalLayout {
x: 24px; y: parent.height - self.height - 24px; x: 24px; y: parent.height - self.height - 24px;
width: parent.width - 48px; height: self.preferred-height; spacing: 12px; width: parent.width - 48px; height: self.preferred-height; spacing: 12px;
Rectangle { height: 1px; background: WorkbenchStyle.separator; } SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; }
HorizontalLayout { HorizontalLayout {
alignment: end; alignment: end;
SettingsButton { text: AboutStrings.back; clicked => { root.back(); } } SettingsButton { text: AboutStrings.back; clicked => { root.back(); } }
+171 -87
View File
@@ -1,17 +1,74 @@
import { FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
// The legacy client uses ImGui::StyleColorsLight(). Keep the structural line
// colors in one place so the Slint renderer produces the same visual weight. // Keep codec names readable in narrow status panels without changing row height.
export global ImGuiLineStyle { export component ScrollingText inherits Rectangle {
out property <color> border: #0000004d; in property <string> text;
out property <color> separator: #6363639e; in property <color> text-color;
out property <color> divider: #0000007a; in property <length> font-size: 11px;
out property <color> control-separator: #b2b2b2; in property <int> font-weight: 400;
in property <bool> align-right: false;
private property <length> distance: max(0px, label.preferred-width - root.width);
private property <float> phase: 0;
min-width: 0px;
clip: true;
accessible-role: AccessibleRole.text;
accessible-label: root.text;
changed text => { root.phase = 0; }
changed width => { root.phase = 0; }
Timer {
interval: 16ms;
running: root.visible && root.width > 0px && root.distance > 0px;
// 30 logical pixels/second, with a one-second pause at each end.
triggered => { root.phase = mod(root.phase + 0.48, root.distance / 1px + 60); }
}
label := Text {
x: root.distance > 0px ? -min(root.distance, max(0, root.phase - 30) * 1px)
: root.align-right ? root.width - self.preferred-width : 0px;
width: self.preferred-width; height: root.height;
text: root.text; color: root.text-color;
font-size: root.font-size; font-weight: root.font-weight;
wrap: no-wrap; vertical-alignment: center;
}
} }
export global FloatingBarStyle { export global FloatingBarStyle {
out property <length> corner-radius: UiRadius.menu; out property <length> corner-radius: UiRadius.menu;
} }
// Fields and selection controls share a muted fill.
export global ControlStyle {
out property <color> surface: #dce6f3;
out property <color> hover: #cedef1;
out property <color> pressed: #bfd2ec;
out property <color> selected: #bcd4f7;
out property <color> selected-hover: #adcaf1;
out property <color> selected-pressed: #99bce8;
out property <color> selected-text: #1d4e9e;
out property <color> disabled: #f0f2f5;
out property <color> icon: #52657f;
out property <color> input-surface: #dee7f2;
out property <color> input-focused: #e5ecf5;
out property <color> input-placeholder: #566a84;
// These tints over white match the field fills for standard LineEdit.
out property <color> input-tint: #235eaa26;
out property <color> input-focus-tint: #235eaa1e;
}
// Action buttons use a light surface and a crisp edge, distinct from fields.
export global ButtonStyle {
out property <color> surface: #ffffff;
out property <color> hover: #f0f5ff;
out property <color> pressed: #dfe9fa;
out property <color> border: #b8c7db;
out property <color> hover-border: #7e9fd1;
out property <color> pressed-border: #6488bf;
out property <color> disabled-border: #dce2eb;
out property <color> selected: #3564e3;
out property <color> selected-hover: #4574f0;
out property <color> selected-pressed: #254cc0;
}
// ImGui loads a 32px font atlas and applies per-window font scales. These // ImGui loads a 32px font atlas and applies per-window font scales. These
// logical sizes reproduce those visual tiers on Slint's HiDPI renderer. // logical sizes reproduce those visual tiers on Slint's HiDPI renderer.
export global ImGuiFontStyle { export global ImGuiFontStyle {
@@ -69,10 +126,10 @@ export component IconButton inherits Rectangle {
in property <bool> enabled: true; in property <bool> enabled: true;
in property <bool> danger: false; in property <bool> danger: false;
in property <length> icon-size: 16px; in property <length> icon-size: 16px;
in property <color> icon-color: #24272d; in property <color> icon-color: ControlStyle.icon;
in property <color> normal-background: transparent; in property <color> normal-background: transparent;
in property <color> hover-background: #edf0f5; in property <color> hover-background: ControlStyle.hover;
in property <color> pressed-background: #d9dde5; in property <color> pressed-background: ControlStyle.pressed;
in property <bool> slashed: false; in property <bool> slashed: false;
callback clicked; callback clicked;
@@ -138,8 +195,8 @@ export component ImageButton inherits Rectangle {
width: 30px; width: 30px;
height: 30px; height: 30px;
background: touch.pressed ? (danger ? #d92d20 : #d9dde5) background: touch.pressed ? (danger ? #d92d20 : ControlStyle.pressed)
: touch.has-hover ? (danger ? #f04438 : #edf0f5) : touch.has-hover ? (danger ? #f04438 : ControlStyle.hover)
: transparent; : transparent;
border-radius: UiRadius.control; border-radius: UiRadius.control;
@@ -174,14 +231,14 @@ export component ImageButton inherits Rectangle {
} }
} }
export component CompactComboBox inherits Rectangle { export component CompactComboBox inherits FlatSurface {
in property <[string]> model; in property <[string]> model;
in-out property <int> current-index: 0; in-out property <int> current-index: 0;
in property <bool> enabled: true; in property <bool> enabled: true;
in property <bool> subtle: false; in property <bool> subtle: false;
in property <bool> show-arrow: true; in property <bool> show-arrow: true;
in property <color> normal-border-color: root.subtle ? #dfe6ef : #bfc2c7; in property <color> normal-outline-color: ButtonStyle.border;
in property <color> hover-border-color: root.normal-border-color; in property <color> hover-outline-color: ButtonStyle.hover-border;
property <bool> popup-animation-enabled: false; property <bool> popup-animation-enabled: false;
property <float> popup-progress: 0.0; property <float> popup-progress: 0.0;
callback selected(int); callback selected(int);
@@ -205,43 +262,45 @@ export component CompactComboBox inherits Rectangle {
width: 73px; width: 73px;
height: 24px; height: 24px;
background: root.enabled ? (root.subtle ? #f8fafc : white) : root.subtle ? #f1f3f6 : #f1f1f1; background: !root.enabled ? ControlStyle.disabled
border-width: 1px; : combo-touch.pressed ? ButtonStyle.pressed
border-color: root.enabled && combo-touch.has-hover ? root.hover-border-color : root.normal-border-color; : combo-popup.is-open || combo-touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface;
outline-focused: root.enabled && combo-popup.is-open;
outline-color: !root.enabled ? ButtonStyle.disabled-border
: combo-popup.is-open ? UiSurface.focus
: combo-touch.pressed ? ButtonStyle.pressed-border
: combo-touch.has-hover ? root.hover-outline-color : root.normal-outline-color;
border-radius: UiRadius.control; border-radius: UiRadius.control;
animate background, outline-color { duration: 120ms; }
accessible-role: combobox;
accessible-enabled: root.enabled;
accessible-expandable: true;
accessible-expanded: combo-popup.is-open;
accessible-value: root.model[root.current-index];
Text { Text {
x: root.subtle ? 10px : 5px; x: root.subtle ? 10px : 8px;
width: parent.width - (root.show-arrow ? (root.subtle ? 36px : 24px) : (root.subtle ? 20px : 10px)); width: parent.width - self.x - (root.show-arrow ? 28px : self.x);
height: parent.height;
text: root.model[root.current-index]; text: root.model[root.current-index];
color: root.enabled ? #202124 : #8b8f95; color: root.enabled ? #202124 : #8b8f95;
font-size: root.subtle ? 12px : ImGuiFontStyle.body; font-size: root.subtle ? 12px : ImGuiFontStyle.body;
overflow: elide; overflow: elide;
vertical-alignment: center; vertical-alignment: center;
accessible-role: none;
} }
if root.show-arrow: Rectangle { if root.show-arrow: Text {
x: parent.width - (root.subtle ? 27px : 21px); x: parent.width - 26px;
width: 20px; width: 18px; height: parent.height;
height: parent.height - 2px; // Switch glyphs so the software renderer also shows the open state.
y: 1px; text: combo-popup.is-open ? FontAwesomeIcons.angle-up : FontAwesomeIcons.angle-down;
background: root.subtle ? transparent : root.enabled ? #9bc9f8 : #d7d9dc; color: !root.enabled ? #98a2b3 : combo-popup.is-open ? UiSurface.focus : ControlStyle.icon;
border-top-right-radius: UiRadius.control - 1px; font-family: "Font Awesome 6 Free";
border-bottom-right-radius: UiRadius.control - 1px; font-weight: 900;
Text { font-size: 10px;
text: FontAwesomeIcons.angle-down; horizontal-alignment: center;
color: root.subtle ? #79879a : #202124; vertical-alignment: center;
font-family: "Font Awesome 6 Free"; accessible-role: none;
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
transform-rotation: combo-popup.is-open ? 180deg : 0deg;
animate transform-rotation {
duration: 140ms;
easing: ease-out;
enabled: combo-popup.is-open;
}
}
} }
combo-touch := TouchArea { combo-touch := TouchArea {
enabled: root.enabled; mouse-cursor: pointer; enabled: root.enabled; mouse-cursor: pointer;
@@ -269,12 +328,12 @@ export component CompactComboBox inherits Rectangle {
combo-popup := PopupWindow { combo-popup := PopupWindow {
x: 0px; x: 0px;
y: root.height; y: root.height + 4px;
width: root.width; width: root.width;
height: root.model.length * 23px; height: root.model.length * 23px + 8px;
close-policy: close-on-click-outside; close-policy: close-on-click-outside;
Rectangle { FlatSurface {
// Keep the reveal anchored to the popup's top edge while the // Keep the reveal anchored to the popup's top edge while the
// content settles downward into its final position. // content settles downward into its final position.
y: (1.0 - root.popup-progress) * -8px; y: (1.0 - root.popup-progress) * -8px;
@@ -282,18 +341,33 @@ export component CompactComboBox inherits Rectangle {
height: parent.height * root.popup-progress; height: parent.height * root.popup-progress;
opacity: root.popup-progress; opacity: root.popup-progress;
background: white; background: white;
border-width: 1px; outline-color: root.normal-outline-color;
border-color: root.normal-border-color;
border-radius: UiRadius.menu; border-radius: UiRadius.menu;
clip: true; clip: true;
for choice[index] in root.model: Rectangle { for choice[index] in root.model: Rectangle {
y: index * 23px; x: 4px; y: 4px + index * 23px;
width: parent.width; width: parent.width - 8px;
height: 23px; height: 23px;
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white; border-radius: UiRadius.control;
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } background: index == root.current-index
? (option-touch.pressed ? ButtonStyle.pressed : ButtonStyle.hover)
: option-touch.pressed ? ButtonStyle.pressed : option-touch.has-hover ? ButtonStyle.hover : white;
Text {
x: 6px; width: parent.width - 28px; height: parent.height; text: choice;
color: index == root.current-index ? ControlStyle.selected-text : #202124;
font-weight: index == root.current-index ? 600 : 400;
font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center;
}
if index == root.current-index: Text {
x: parent.width - 20px; width: 14px; height: parent.height;
text: FontAwesomeIcons.check; color: UiSurface.focus;
font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px;
horizontal-alignment: center; vertical-alignment: center;
accessible-role: none;
}
option-touch := TouchArea { option-touch := TouchArea {
enabled: root.enabled; enabled: root.enabled;
mouse-cursor: pointer;
clicked => { clicked => {
popup-open-timer.running = false; popup-open-timer.running = false;
root.popup-animation-enabled = false; root.popup-animation-enabled = false;
@@ -306,12 +380,12 @@ export component CompactComboBox inherits Rectangle {
} }
} }
} }
Rectangle { FlatSurface {
width: parent.width; width: parent.width;
height: parent.height; height: parent.height;
background: transparent; background: transparent;
border-width: 1px; clip: true;
border-color: root.normal-border-color; outline-color: root.normal-outline-color;
border-radius: UiRadius.menu; border-radius: UiRadius.menu;
z: 2; z: 2;
} }
@@ -319,19 +393,20 @@ export component CompactComboBox inherits Rectangle {
} }
} }
export component ImGuiCheckBox inherits Rectangle { export component ImGuiCheckBox inherits FlatSurface {
in-out property <bool> checked; in-out property <bool> checked;
in property <bool> enabled: true; in property <bool> enabled: true;
width: 21px; width: 21px;
height: 21px; height: 21px;
background: root.enabled ? white : #f1f1f1; background: !root.enabled ? ControlStyle.disabled
border-width: 1px; : root.checked ? (check-touch.pressed ? #254cc0 : check-touch.has-hover ? #4574f0 : #3564e3)
border-color: check-touch.has-hover && root.enabled ? #8cbef2 : #b9bdc2; : check-touch.pressed ? ControlStyle.pressed : check-touch.has-hover ? ControlStyle.hover : ControlStyle.surface;
outline-color: check-touch.has-hover && root.enabled ? UiSurface.focus : UiSurface.edge;
border-radius: 4px; border-radius: 4px;
if root.checked: Text { if root.checked: Text {
text: FontAwesomeIcons.check; text: FontAwesomeIcons.check;
color: root.enabled ? #2e86de : #8b8f95; color: root.enabled ? white : #8b8f95;
font-family: "Font Awesome 6 Free"; font-family: "Font Awesome 6 Free";
font-weight: 900; font-weight: 900;
font-size: 16px; font-size: 16px;
@@ -368,7 +443,7 @@ export component MiniToggleSwitch inherits Rectangle {
} }
} }
export component CompactButton inherits Rectangle { export component CompactButton inherits FlatSurface {
in property <string> text; in property <string> text;
in property <length> text-size: ImGuiFontStyle.body; in property <length> text-size: ImGuiFontStyle.body;
in property <bool> primary: false; in property <bool> primary: false;
@@ -378,13 +453,17 @@ export component CompactButton inherits Rectangle {
width: 34px; width: 34px;
height: 24px; height: 24px;
background: !root.enabled ? (root.quiet ? transparent : #eceef0) background: !root.enabled ? (root.quiet ? transparent : ControlStyle.disabled)
: button-touch.pressed ? (root.primary ? #82b8f2 : #d8dadd) : button-touch.pressed ? (root.primary ? #82b8f2 : ButtonStyle.pressed)
: button-touch.has-hover ? (root.primary ? #b8d9fb : #f0f1f2) : button-touch.has-hover ? (root.primary ? #b8d9fb : ButtonStyle.hover)
: root.primary ? #a8d0fa : root.quiet ? transparent : #e2e4e7; : root.primary ? #a8d0fa : root.quiet ? transparent : ButtonStyle.surface;
border-width: 1px; outline-enabled: !root.quiet;
border-color: root.primary ? #9bc3ed : root.quiet ? transparent : #c8cbd0; outline-color: !root.enabled ? ButtonStyle.disabled-border
: root.primary ? UiSurface.focus
: button-touch.pressed ? ButtonStyle.pressed-border
: button-touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border;
border-radius: UiRadius.control; border-radius: UiRadius.control;
animate background, outline-color { duration: 100ms; }
Text { Text {
text: root.text; text: root.text;
color: root.enabled ? #24272d : #8e9298; color: root.enabled ? #24272d : #8e9298;
@@ -393,27 +472,30 @@ export component CompactButton inherits Rectangle {
horizontal-alignment: center; horizontal-alignment: center;
vertical-alignment: center; vertical-alignment: center;
} }
button-touch := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } } button-touch := TouchArea { enabled: root.enabled; mouse-cursor: pointer; clicked => { root.clicked(); } }
} }
export component PrimaryButton inherits Rectangle { export component PrimaryButton inherits FlatSurface {
in property <string> text; in property <string> text;
in property <length> text-size: ImGuiFontStyle.body; in property <length> text-size: 12px;
in property <bool> enabled: true; in property <bool> enabled: true;
callback clicked; callback clicked;
min-width: 72px; min-width: 72px;
height: 30px; height: 32px;
background: !root.enabled ? #b8c1d1 background: !root.enabled ? #b8c1d1
: touch.pressed ? #1849a9 : touch.pressed ? #254cc0
: touch.has-hover ? #2970d6 : touch.has-hover ? #4574f0
: #2463c7; : #3564e3;
outline-enabled: false;
border-radius: UiRadius.control; border-radius: UiRadius.control;
animate background { duration: 100ms; }
Text { Text {
text: root.text; text: root.text;
color: white; color: white;
font-size: root.text-size; font-size: root.text-size;
font-weight: 600;
horizontal-alignment: center; horizontal-alignment: center;
vertical-alignment: center; vertical-alignment: center;
} }
@@ -424,32 +506,35 @@ export component PrimaryButton inherits Rectangle {
} }
} }
export component SecondaryButton inherits Rectangle { export component SecondaryButton inherits FlatSurface {
in property <string> text; in property <string> text;
in property <length> text-size: ImGuiFontStyle.body; in property <length> text-size: 12px;
in property <bool> enabled: true; in property <bool> enabled: true;
callback clicked; callback clicked;
min-width: 72px; min-width: 72px;
height: 30px; height: 32px;
background: !root.enabled ? #f1f2f4 background: !root.enabled ? ControlStyle.disabled
: touch.pressed ? #dfe3e9 : touch.pressed ? ButtonStyle.pressed
: touch.has-hover ? #f3f4f6 : white; : touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface;
border-width: 1px; outline-color: !root.enabled ? ButtonStyle.disabled-border
border-color: #c9ced7; : touch.pressed ? ButtonStyle.pressed-border
: touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border;
border-radius: UiRadius.control; border-radius: UiRadius.control;
clip: true; animate background, outline-color { duration: 100ms; }
Text { Text {
text: root.text; text: root.text;
color: root.enabled ? #24272d : #949aa5; color: root.enabled ? #303f55 : #949aa5;
font-size: root.text-size; font-size: root.text-size;
font-weight: 500;
horizontal-alignment: center; horizontal-alignment: center;
vertical-alignment: center; vertical-alignment: center;
} }
touch := TouchArea { touch := TouchArea {
enabled: root.enabled; enabled: root.enabled;
mouse-cursor: pointer;
clicked => { root.clicked(); } clicked => { root.clicked(); }
} }
} }
@@ -495,9 +580,8 @@ export component SectionTitle inherits Text {
vertical-alignment: center; vertical-alignment: center;
} }
export component DialogSurface inherits Rectangle { export component DialogSurface inherits FlatSurface {
background: white; background: white;
border-width: 1px; outline-color: UiSurface.edge;
border-color: ImGuiLineStyle.border;
border-radius: UiRadius.surface; border-radius: UiRadius.surface;
} }
@@ -35,12 +35,12 @@ export component DeviceCardActions inherits Rectangle {
spacing: 0px; spacing: 0px;
CardAction { CardAction {
icon: FontAwesomeIcons.pen; icon: FontAwesomeIcons.pen;
tooltip: GalleryStrings.tr("修改名称", "Rename", "Переименовать"); tooltip: GalleryStrings.texts[35];
clicked => { root.edit-alias(); } clicked => { root.edit-alias(); }
} }
CardAction { CardAction {
icon: FontAwesomeIcons.tags; icon: FontAwesomeIcons.tags;
tooltip: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"); tooltip: GalleryStrings.texts[30];
clicked => { root.edit-groups(); } clicked => { root.edit-groups(); }
} }
CardAction { CardAction {
@@ -51,7 +51,7 @@ export component DeviceCardActions inherits Rectangle {
CardAction { CardAction {
icon: root.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long; icon: root.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long;
icon-size: 13px; icon-size: 13px;
tooltip: root.connected ? GalleryStrings.tr("断开", "Disconnect", "Отключить") : GalleryStrings.tr("连接", "Connect", "Подключить"); tooltip: root.connected ? GalleryStrings.texts[36] : GalleryStrings.texts[37];
clicked => { clicked => {
if root.connected { root.disconnect-device(); } if root.connected { root.disconnect-device(); }
else { root.connect-device(); } else { root.connect-device(); }
@@ -1,7 +1,8 @@
import { FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
import { RoundedLineEdit } from "rounded_line_edit.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint";
import { CompactButton, ImGuiCheckBox } from "common.slint"; import { CompactButton, ImGuiCheckBox, ControlStyle } from "common.slint";
import { GalleryStrings } from "device_strings.slint"; import { GalleryStrings } from "device_strings.slint";
export struct DeviceGroupOption { export struct DeviceGroupOption {
@@ -20,17 +21,17 @@ export component DeviceGroupDialog inherits Rectangle {
callback cancel(); callback cancel();
background: #00000040; background: #00000040;
TouchArea {} TouchArea {}
dialog := Rectangle { dialog := FlatSurface {
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
width: min(360px, parent.width - 40px); width: min(360px, parent.width - 40px);
height: 208px + group-list.height + (root.error == "" ? 0px : 32px); height: 208px + group-list.height + (root.error == "" ? 0px : 32px);
background: white; border-radius: UiRadius.surface; background: white; border-radius: UiRadius.surface;
border-width: 1px; border-color: #b2b2b2; outline-color: UiSurface.edge;
TouchArea {} TouchArea {}
Text { Text {
x: 18px; y: 16px; height: 24px; x: 18px; y: 16px; height: 24px;
text: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"); text: GalleryStrings.texts[30];
color: #24272d; font-size: 17px; font-weight: 600; color: #24272d; font-size: 17px; font-weight: 600;
} }
Text { Text {
@@ -41,10 +42,10 @@ export component DeviceGroupDialog inherits Rectangle {
x: 18px; y: 67px; width: parent.width - 36px; height: 16px; x: 18px; y: 67px; width: parent.width - 36px; height: 16px;
text: "ID " + root.device-id; color: #858c97; font-size: 11px; text: "ID " + root.device-id; color: #858c97; font-size: 11px;
} }
group-list := Rectangle { group-list := FlatSurface {
x: 18px; y: 95px; width: parent.width - 36px; x: 18px; y: 95px; width: parent.width - 36px;
height: min(5, max(1, root.options.length)) * 32px + 2px; height: min(5, max(1, root.options.length)) * 32px + 2px;
background: #fbfcfd; border-width: 1px; border-color: #e1e5eb; border-radius: UiRadius.surface; background: #fbfcfd; outline-color: UiSurface.edge; border-radius: UiRadius.surface;
clip: true; clip: true;
ScrollView { ScrollView {
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px; x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
@@ -52,16 +53,21 @@ export component DeviceGroupDialog inherits Rectangle {
VerticalLayout { VerticalLayout {
spacing: 0px; spacing: 0px;
for option[index] in root.options: Rectangle { for option[index] in root.options: Rectangle {
private property <bool> selected: option.selected && (option.name != "" || root.new-group == "");
height: 32px; height: 32px;
background: row-touch.has-hover ? #eef4fc : transparent; background: self.selected
? (row-touch.pressed ? ControlStyle.selected-pressed : row-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected)
: row-touch.pressed ? ControlStyle.pressed : row-touch.has-hover ? ControlStyle.hover : transparent;
ImGuiCheckBox { ImGuiCheckBox {
x: 9px; y: 6px; width: 20px; height: 20px; x: 9px; y: 6px; width: 20px; height: 20px;
checked: option.selected && (option.name != "" || root.new-group == ""); checked: parent.selected;
} }
Text { Text {
x: 39px; width: parent.width - 48px; height: parent.height; x: 39px; width: parent.width - 48px; height: parent.height;
text: option.name == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : option.name; text: option.name == "" ? GalleryStrings.texts[31] : option.name;
color: #30343b; font-size: 12px; overflow: elide; vertical-alignment: center; color: parent.selected ? ControlStyle.selected-text : #30343b;
font-weight: parent.selected ? 600 : 400;
font-size: 12px; overflow: elide; vertical-alignment: center;
} }
row-touch := TouchArea { clicked => { root.toggle-group(option.name); } } row-touch := TouchArea { clicked => { root.toggle-group(option.name); } }
} }
@@ -75,17 +81,17 @@ export component DeviceGroupDialog inherits Rectangle {
border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface; border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface;
border-color: white; border-color: white;
} }
Rectangle { FlatSurface {
width: parent.width; height: parent.height; width: parent.width; height: parent.height;
border-width: 1px; border-radius: UiRadius.surface; border-radius: UiRadius.surface;
border-color: #e1e5eb; outline-color: UiSurface.edge;
} }
} }
RoundedLineEdit { RoundedLineEdit {
x: 18px; y: group-list.y + group-list.height + 12px; x: 18px; y: group-list.y + group-list.height + 12px;
width: parent.width - 36px; height: 30px; width: parent.width - 36px; height: 30px;
text <=> root.new-group; text <=> root.new-group;
placeholder-text: GalleryStrings.tr("新建分组(可选)", "New group (optional)", "Новая группа (необязательно)"); placeholder-text: GalleryStrings.texts[32];
} }
if root.error != "": Text { if root.error != "": Text {
x: 18px; y: group-list.y + group-list.height + 48px; x: 18px; y: group-list.y + group-list.height + 48px;
@@ -96,8 +102,8 @@ export component DeviceGroupDialog inherits Rectangle {
x: 18px; y: parent.height - 46px; width: parent.width - 36px; height: 28px; x: 18px; y: parent.height - 46px; width: parent.width - 36px; height: 28px;
spacing: 10px; spacing: 10px;
Rectangle { horizontal-stretch: 1; } Rectangle { horizontal-stretch: 1; }
CompactButton { width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.cancel(); } } CompactButton { width: 72px; height: 28px; text: GalleryStrings.texts[33]; clicked => { root.cancel(); } }
CompactButton { width: 88px; height: 28px; primary: true; text: GalleryStrings.tr("保存", "Save", "Сохранить"); clicked => { root.save(root.new-group); } } CompactButton { width: 88px; height: 28px; primary: true; text: GalleryStrings.texts[34]; clicked => { root.save(root.new-group); } }
} }
} }
} }
@@ -1,6 +1,32 @@
export global GalleryStrings { export global GalleryStrings {
in-out property <int> language: 0; in-out property <int> language: 0;
in-out property <[string]> texts;
in-out property <string> retry-prefix;
in-out property <string> retry-suffix;
public pure function retry-after(seconds: int) -> string {
return self.retry-prefix + seconds + self.retry-suffix;
}
// Compatibility for embedding callers with their own three-language text.
public pure function tr(zh: string, en: string, ru: string) -> string { public pure function tr(zh: string, en: string, ru: string) -> string {
return self.language == 0 ? zh : self.language == 2 ? ru : en; return self.language == 0 ? zh : self.language == 2 ? ru : en;
} }
public pure function device-count(count: int) -> string {
if self.language == 0 { return count + " 台设备"; }
if self.language == 3 { return count + " 臺裝置"; }
if self.language == 4 { return count + " 台"; }
if self.language == 5 { return count + "개 기기"; }
if self.language == 6 { return count + (count == 1 ? " Gerät" : " Geräte"); }
if self.language == 7 { return count + (count == 1 ? " appareil" : " appareils"); }
if self.language == 8 { return count + (count == 1 ? " dispositivo" : " dispositivos"); }
if self.language == 9 { return count + (count == 1 ? " dispositivo" : " dispositivos"); }
if self.language == 2 {
let last-two = mod(count, 100);
let last = mod(count, 10);
if last-two >= 11 && last-two <= 14 { return count + " устройств"; }
if last == 1 { return count + " устройство"; }
if last >= 2 && last <= 4 { return count + " устройства"; }
return count + " устройств";
}
return count + (count == 1 ? " device" : " devices");
}
} }
@@ -1,3 +1,4 @@
import { SurfaceSeparator, UiSurface } from "ui_surface.slint";
import { FileTransferEntry } from "stream_window.slint"; import { FileTransferEntry } from "stream_window.slint";
import { DialogSurface, FontAwesomeIcons, IconButton, ImGuiFontStyle } from "common.slint"; import { DialogSurface, FontAwesomeIcons, IconButton, ImGuiFontStyle } from "common.slint";
import { WorkbenchStyle, WorkbenchButton } from "workbench_controls.slint"; import { WorkbenchStyle, WorkbenchButton } from "workbench_controls.slint";
@@ -26,7 +27,7 @@ export component FileTransferNotification inherits Window {
DialogSurface { DialogSurface {
width: parent.width; height: parent.height; width: parent.width; height: parent.height;
background: WorkbenchStyle.surface; background: WorkbenchStyle.surface;
border-color: WorkbenchStyle.separator; outline-color: UiSurface.edge;
Text { Text {
x: 16px; y: 8px; x: 16px; y: 8px;
width: parent.width - 64px - (root.remote-id == "" ? 0px : 108px); width: parent.width - 64px - (root.remote-id == "" ? 0px : 108px);
@@ -50,9 +51,9 @@ export component FileTransferNotification inherits Window {
pressed-background: WorkbenchStyle.pressed-surface; pressed-background: WorkbenchStyle.pressed-surface;
clicked => { root.dismissed(); } clicked => { root.dismissed(); }
} }
Rectangle { SurfaceSeparator {
x: 16px; y: 40px; width: parent.width - 32px; height: 1px; x: 16px; y: 40px; width: parent.width - 32px; height: 1px;
background: WorkbenchStyle.separator; outline-color: UiSurface.separator;
} }
Text { Text {
x: 16px; y: 47px; width: parent.width - 32px; height: 18px; x: 16px; y: 47px; width: parent.width - 32px; height: 18px;
@@ -96,7 +97,7 @@ export component FileTransferNotification inherits Window {
text: root.cancel-text; text: root.cancel-text;
text-size: ImGuiFontStyle.base; text-size: ImGuiFontStyle.base;
text-color: WorkbenchStyle.muted; text-color: WorkbenchStyle.muted;
normal-border-color: WorkbenchStyle.border; normal-outline-color: UiSurface.edge;
clicked => { root.cancel-requested(); } clicked => { root.cancel-requested(); }
} }
} }
@@ -1,5 +1,6 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { CompactButton, FontAwesomeIcons } from "common.slint"; import { CompactButton, FontAwesomeIcons, ControlStyle } from "common.slint";
export struct GalleryMenuItem { label: string, enabled: bool } export struct GalleryMenuItem { label: string, enabled: bool }
@@ -88,12 +89,11 @@ export component GalleryMenuButton inherits Rectangle {
color: root.enabled ? #62666d : #8e9298; color: root.enabled ? #62666d : #8e9298;
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
} }
if root.compact-trigger: Rectangle { if root.compact-trigger: FlatSurface {
background: more-touch.pressed || popup.is-open ? #e5edfc : more-touch.has-hover ? white : #fffffff0; background: more-touch.pressed || popup.is-open ? ControlStyle.selected : more-touch.has-hover ? ControlStyle.hover : white;
border-radius: UiRadius.menu; border-radius: UiRadius.menu;
border-width: 1px; outline-color: trigger-focus.has-focus ? UiSurface.focus : UiSurface.edge;
border-color: trigger-focus.has-focus ? #3564e3 : #ffffff80; animate background, outline-color { duration: 120ms; }
animate background, border-color { duration: 120ms; }
for dot in 3: Rectangle { for dot in 3: Rectangle {
x: parent.width / 2 - 6px + dot * 5px; x: parent.width / 2 - 6px + dot * 5px;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
@@ -117,11 +117,12 @@ export component GalleryMenuButton inherits Rectangle {
forward-focus: popup-focus; forward-focus: popup-focus;
popup-focus := FocusScope { popup-focus := FocusScope {
key-pressed(event) => { return root.handle-key(event); } key-pressed(event) => { return root.handle-key(event); }
Rectangle { FlatSurface {
background: white; border-width: 1px; border-color: #c9cdd2; border-radius: UiRadius.menu; background: white; outline-color: UiSurface.edge; border-radius: UiRadius.menu;
if root.destructive-index > 0: Rectangle { clip: true;
if root.destructive-index > 0: SurfaceSeparator {
x: 10px; y: root.menu-padding + root.destructive-index * root.row-height + (root.divider-spacing - 1px) / 2; x: 10px; y: root.menu-padding + root.destructive-index * root.row-height + (root.divider-spacing - 1px) / 2;
width: parent.width - 20px; height: 1px; background: #e8edf4; width: parent.width - 20px; height: 1px; outline-color: UiSurface.separator;
} }
for item[index] in root.items: Rectangle { for item[index] in root.items: Rectangle {
private property <bool> destructive: index == root.destructive-index; private property <bool> destructive: index == root.destructive-index;
@@ -132,7 +133,9 @@ export component GalleryMenuButton inherits Rectangle {
+ (root.destructive-index > 0 && index >= root.destructive-index ? root.divider-spacing : 0px); + (root.destructive-index > 0 && index >= root.destructive-index ? root.divider-spacing : 0px);
width: parent.width - root.menu-padding * 2; height: root.row-height; width: parent.width - root.menu-padding * 2; height: root.row-height;
border-radius: UiRadius.control; border-radius: UiRadius.control;
background: item.enabled && index == root.current-index ? (self.destructive ? #fff0f0 : #edf4fc) : transparent; background: item.enabled && index == root.current-index
? (self.destructive ? (item-touch.pressed ? #f6babb : #fbe0e0)
: item-touch.pressed ? ControlStyle.pressed : ControlStyle.hover) : transparent;
accessible-role: button; accessible-label: item.label; accessible-enabled: item.enabled; accessible-role: button; accessible-label: item.label; accessible-enabled: item.enabled;
accessible-action-default => { root.choose(index); } accessible-action-default => { root.choose(index); }
if self.has-icon: Text { if self.has-icon: Text {
@@ -148,7 +151,7 @@ export component GalleryMenuButton inherits Rectangle {
text: item.label; color: parent.foreground; text: item.label; color: parent.foreground;
font-size: root.compact-menu ? 11px : 12px; vertical-alignment: center; font-size: root.compact-menu ? 11px : 12px; vertical-alignment: center;
} }
TouchArea { item-touch := TouchArea {
enabled: item.enabled; enabled: item.enabled;
mouse-cursor: pointer; mouse-cursor: pointer;
changed has-hover => { if self.has-hover { root.current-index = index; } } changed has-hover => { if self.has-hover { root.current-index = index; } }
@@ -1,6 +1,7 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { GalleryStrings } from "device_strings.slint"; import { GalleryStrings } from "device_strings.slint";
import { CompactButton } from "common.slint"; import { CompactButton, ScrollingText } from "common.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
export struct GalleryNetworkDevice { export struct GalleryNetworkDevice {
@@ -64,10 +65,10 @@ component NetworkRow inherits Rectangle {
encryption-text := Text { encryption-text := Text {
x: 6px; width: parent.width - 12px; height: parent.height; x: 6px; width: parent.width - 12px; height: parent.height;
text: !root.device.encryption-known text: !root.device.encryption-known
? GalleryStrings.tr("待确认", "Unknown", "Неизвестно") ? GalleryStrings.texts[38]
: root.device.encryption-enabled : root.device.encryption-enabled
? GalleryStrings.tr("已加密", "Encrypted", "Защищено") ? GalleryStrings.texts[39]
: GalleryStrings.tr("未加密", "Unencrypted", "Без шифр."); : GalleryStrings.texts[40];
color: !root.device.encryption-known ? #7a8490 color: !root.device.encryption-known ? #7a8490
: root.device.encryption-enabled ? #18794e : #8a6428; : root.device.encryption-enabled ? #18794e : #8a6428;
font-size: 10px; horizontal-alignment: center; font-size: 10px; horizontal-alignment: center;
@@ -82,7 +83,7 @@ component NetworkRow inherits Rectangle {
Text { Text {
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px; y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width; width: parent.width;
text: root.heading ? GalleryStrings.tr("画面", "Video", "Видео") : root.device.resolution; text: root.heading ? GalleryStrings.texts[41] : root.device.resolution;
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400; font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
color: root.heading ? #6b7785 : #293746; color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide; vertical-alignment: center; overflow: elide;
@@ -97,7 +98,7 @@ component NetworkRow inherits Rectangle {
width: max(100px, root.width * 0.18); width: max(100px, root.width * 0.18);
if root.heading: Text { if root.heading: Text {
x: 0px; width: parent.width; x: 0px; width: parent.width;
text: GalleryStrings.tr("收发速率", "Transfer rate", "Скорость"); text: GalleryStrings.texts[42];
font-size: 11px; font-weight: 600; color: #6b7785; font-size: 11px; font-weight: 600; color: #6b7785;
vertical-alignment: center; overflow: elide; vertical-alignment: center; overflow: elide;
} }
@@ -125,14 +126,14 @@ component NetworkRow inherits Rectangle {
Text { Text {
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px; y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width; width: parent.width;
text: root.heading ? GalleryStrings.tr("视频延时", "Video latency", "Задержка видео") : root.device.video-latency; text: root.heading ? GalleryStrings.texts[43] : root.device.video-latency;
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400; font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
color: root.heading ? #6b7785 : #293746; color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide; vertical-alignment: center; overflow: elide;
} }
if !root.heading: Text { if !root.heading: Text {
y: 30px; height: 17px; width: parent.width; y: 30px; height: 17px; width: parent.width;
text: GalleryStrings.tr("丢包 ", "Loss ", "Потери ") + root.device.loss; text: GalleryStrings.texts[44] + root.device.loss;
font-size: 11px; color: #7a8490; font-size: 11px; color: #7a8490;
vertical-alignment: center; overflow: elide; vertical-alignment: center; overflow: elide;
} }
@@ -152,18 +153,26 @@ component NetworkRow inherits Rectangle {
spacing: 12px; spacing: 12px;
Rectangle { Rectangle {
width: (parent.width - 12px) / 2; width: (parent.width - 12px) / 2;
Text { encoder-label := Text {
width: parent.width; height: parent.height; height: parent.height; width: self.preferred-width;
text: GalleryStrings.tr("被控端编码器 ", "Remote encoder ", "Удалённый кодер ") + root.device.encoder; text: GalleryStrings.texts[45];
font-size: 10px; color: #586f87; vertical-alignment: center; overflow: elide; font-size: 10px; color: #586f87; vertical-alignment: center;
}
ScrollingText {
x: encoder-label.width; width: max(0px, parent.width - self.x); height: parent.height;
text: root.device.encoder; font-size: 10px; text-color: #586f87;
} }
} }
Rectangle { Rectangle {
width: (parent.width - 12px) / 2; width: (parent.width - 12px) / 2;
Text { decoder-label := Text {
width: parent.width; height: parent.height; height: parent.height; width: self.preferred-width;
text: GalleryStrings.tr("控制端解码器 ", "Local decoder ", "Локальный декодер ") + root.device.decoder; text: GalleryStrings.texts[46];
font-size: 10px; color: #586f87; vertical-alignment: center; overflow: elide; font-size: 10px; color: #586f87; vertical-alignment: center;
}
ScrollingText {
x: decoder-label.width; width: max(0px, parent.width - self.x); height: parent.height;
text: root.device.decoder; font-size: 10px; text-color: #586f87;
} }
} }
} }
@@ -173,11 +182,11 @@ component NetworkEmptyState inherits Rectangle {
VerticalLayout { VerticalLayout {
alignment: center; spacing: 8px; alignment: center; spacing: 8px;
Text { Text {
text: GalleryStrings.tr("暂无已连接设备", "No connected devices", "Нет подключённых устройств"); text: GalleryStrings.texts[47];
font-size: 13px; color: #596878; horizontal-alignment: center; font-size: 13px; color: #596878; horizontal-alignment: center;
} }
Text { Text {
text: GalleryStrings.tr("建立连接后,网络和画面信息将在此显示", "Network and video details appear here once connected", "После подключения здесь появятся данные сети и видео"); text: GalleryStrings.texts[48];
font-size: 11px; color: #8a95a1; horizontal-alignment: center; font-size: 11px; color: #8a95a1; horizontal-alignment: center;
} }
} }
@@ -190,13 +199,13 @@ export component GalleryNetworkDialog inherits Rectangle {
callback close(); callback close();
background: #00000042; background: #00000042;
TouchArea { clicked => { root.close(); } } TouchArea { clicked => { root.close(); } }
Rectangle { FlatSurface {
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
width: min(760px, parent.width - 32px); width: min(760px, parent.width - 32px);
height: min(120px + root.list-height, parent.height - 32px); height: min(120px + root.list-height, parent.height - 32px);
background: white; border-radius: UiRadius.surface; background: white; border-radius: UiRadius.surface;
border-width: 1px; border-color: #c6cdd5; outline-color: UiSurface.edge;
TouchArea {} TouchArea {}
FocusScope { FocusScope {
init => { self.focus(); } init => { self.focus(); }
@@ -207,25 +216,25 @@ export component GalleryNetworkDialog inherits Rectangle {
} }
Text { Text {
x: 20px; y: 17px; width: parent.width - 120px; height: 24px; x: 20px; y: 17px; width: parent.width - 120px; height: 24px;
text: GalleryStrings.tr("设备网络状态", "Device network status", "Сеть устройств"); text: GalleryStrings.texts[49];
font-size: 17px; font-weight: 600; color: #293746; font-size: 17px; font-weight: 600; color: #293746;
} }
CompactButton { CompactButton {
border-radius: UiRadius.control; border-radius: UiRadius.control;
x: parent.width - 80px; y: 17px; width: 60px; height: 26px; x: parent.width - 80px; y: 17px; width: 60px; height: 26px;
text: GalleryStrings.tr("关闭", "Close", "Закрыть"); clicked => { root.close(); } text: GalleryStrings.texts[50]; clicked => { root.close(); }
} }
Text { Text {
x: 20px; y: 48px; width: parent.width - 40px; height: 18px; x: 20px; y: 48px; width: parent.width - 40px; height: 18px;
text: GalleryStrings.tr("已连接设备 ", "Connected devices ", "Подключённые устройства ") + root.devices.length text: GalleryStrings.texts[51] + root.devices.length
+ GalleryStrings.tr(" 台", "", ""); + GalleryStrings.texts[52];
font-size: 11px; color: #7a8490; overflow: elide; font-size: 11px; color: #7a8490; overflow: elide;
} }
NetworkRow { NetworkRow {
x: 12px; y: 78px; width: parent.width - 38px; height: 30px; x: 12px; y: 78px; width: parent.width - 38px; height: 30px;
heading: true; background: #f3f5f8; border-radius: UiRadius.control; heading: true; background: #f3f5f8; border-radius: UiRadius.control;
device: { device: {
name: GalleryStrings.tr("设备 / ID", "Device / ID", "Устройство / ID"), name: GalleryStrings.texts[53],
}; };
} }
list := ScrollView { list := ScrollView {
@@ -237,7 +246,7 @@ export component GalleryNetworkDialog inherits Rectangle {
for device[index] in root.devices: NetworkRow { for device[index] in root.devices: NetworkRow {
y: index * root.row-height; width: list.viewport-width; height: root.row-height; y: index * root.row-height; width: list.viewport-width; height: root.row-height;
device: device; background: mod(index, 2) == 0 ? white : #f8fafc; device: device; background: mod(index, 2) == 0 ? white : #f8fafc;
Rectangle { y: parent.height - 1px; height: 1px; background: #edf0f4; } SurfaceSeparator { y: parent.height - 1px; height: 1px; outline-color: UiSurface.separator; }
} }
} }
if root.devices.length == 0: NetworkEmptyState { if root.devices.length == 0: NetworkEmptyState {
@@ -9,31 +9,31 @@ export component GallerySettings inherits Rectangle {
in property <string> error; in property <string> error;
SettingsGroup { SettingsGroup {
y: 0px; width: parent.width; y: 0px; width: parent.width;
title: GalleryStrings.tr("画面传输", "Video transmission", "Передача видео"); rows: 3; title: GalleryStrings.texts[145]; rows: 3;
SettingsRow { SettingsRow {
label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Разрешение"); label: GalleryStrings.texts[146];
label-reserve: self.width - 112px; label-reserve: self.width - 112px;
SettingsSelect { SettingsSelect {
border-radius: UiRadius.control; border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px; x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.resolution-index; current-index <=> root.resolution-index;
model: [GalleryStrings.tr("自动", "Auto", "Авто"), model: [GalleryStrings.texts[147],
"320 × 180", "640 × 360", "1280 × 720", "1920 × 1080", "2560 × 1440", "3840 × 2160", "320 × 180", "640 × 360", "1280 × 720", "1920 × 1080", "2560 × 1440", "3840 × 2160",
GalleryStrings.tr("原始分辨率", "Original resolution", "Исходное разрешение")]; GalleryStrings.texts[148]];
} }
} }
SettingsRow { SettingsRow {
label: GalleryStrings.tr("帧率", "Frame rate", "Частота кадров"); label: GalleryStrings.texts[149];
label-reserve: self.width - 112px; label-reserve: self.width - 112px;
SettingsSelect { SettingsSelect {
border-radius: UiRadius.control; border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px; x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.frame-rate-index; current-index <=> root.frame-rate-index;
model: [GalleryStrings.tr("跟随设备设置", "Follow device settings", "Настройки устройства"), "15 FPS", "30 FPS", "60 FPS"]; model: [GalleryStrings.texts[150], "15 FPS", "30 FPS", "60 FPS"];
} }
} }
SettingsRow { SettingsRow {
label: GalleryStrings.tr("视频总带宽", "Video budget", "Бюджет видео"); label: GalleryStrings.texts[151];
label-reserve: self.width - 112px; divider: false; label-reserve: self.width - 112px; divider: false;
SettingsSelect { SettingsSelect {
border-radius: UiRadius.control; border-radius: UiRadius.control;
+126 -121
View File
@@ -1,3 +1,4 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { GalleryStrings } from "device_strings.slint"; import { GalleryStrings } from "device_strings.slint";
import { RemoteCursor } from "remote_cursor.slint"; import { RemoteCursor } from "remote_cursor.slint";
@@ -6,7 +7,7 @@ import { GalleryMenuButton } from "gallery_menu_button.slint";
import { GroupFilter } from "group_filter.slint"; import { GroupFilter } from "group_filter.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
import { RoundedLineEdit } from "rounded_line_edit.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint";
import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint"; import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox, ControlStyle } from "common.slint";
component GalleryButton inherits CompactButton { component GalleryButton inherits CompactButton {
border-radius: UiRadius.control; border-radius: UiRadius.control;
@@ -155,7 +156,7 @@ export component GalleryView inherits Rectangle {
in property <int> count: 0; in property <int> count: 0;
in property <int> connected-count: 0; in property <int> connected-count: 0;
private property <string> connected-summary: root.connected-count + " / " + root.count private property <string> connected-summary: root.connected-count + " / " + root.count
+ GalleryStrings.tr(" 台已连接", " connected", " подключено"); + GalleryStrings.texts[152];
callback network-open; callback network-open;
in property <int> selected-count: 0; in property <int> selected-count: 0;
in property <bool> selected-audio-available: false; in property <bool> selected-audio-available: false;
@@ -289,17 +290,17 @@ export component GalleryView inherits Rectangle {
selected(index) => { root.action("group", index); } selected(index) => { root.action("group", index); }
} }
Text { Text {
text: root.count + GalleryStrings.tr(" 台设备", " devices", " устройств"); text: GalleryStrings.device-count(root.count);
min-width: 0px; overflow: elide; font-size: 11px; color: #858c96; vertical-alignment: center; min-width: 0px; overflow: elide; font-size: 11px; color: #858c96; vertical-alignment: center;
} }
Rectangle { horizontal-stretch: 1; min-width: 0px; } Rectangle { horizontal-stretch: 1; min-width: 0px; }
add-menu := GalleryMenuButton { add-menu := GalleryMenuButton {
horizontal-padding: root.toolbar-button-padding; horizontal-padding: root.toolbar-button-padding;
width: self.preferred-button-width; width: self.preferred-button-width;
text: GalleryStrings.tr("添加", "Add", "Добавить"); menu-width: 232px; text: GalleryStrings.texts[153]; menu-width: 232px;
items: [ items: [
{label: GalleryStrings.tr("通过设备 ID 添加", "Add by device ID", "Добавить по ID устройства"), enabled: true}, {label: GalleryStrings.texts[154], enabled: true},
{label: GalleryStrings.tr("从近期连接添加", "Add from recent", "Добавить из недавних"), enabled: true}, {label: GalleryStrings.texts[155], enabled: true},
]; ];
opened => { root.device-tooltip-id = ""; root.action("release", ""); } opened => { root.device-tooltip-id = ""; root.action("release", ""); }
selected(index) => { selected(index) => {
@@ -315,12 +316,12 @@ export component GalleryView inherits Rectangle {
horizontal-stretch: 1; min-width: 0px; horizontal-stretch: 1; min-width: 0px;
Text { Text {
x: 0px; y: 0px; width: parent.width; height: root.hidden-selected > 0 ? 13px : parent.height; x: 0px; y: 0px; width: parent.width; height: root.hidden-selected > 0 ? 13px : parent.height;
text: GalleryStrings.tr("已选 ", "Selected ", "Выбрано ") + root.selected-count + GalleryStrings.tr(" 台", "", ""); text: GalleryStrings.texts[156] + root.selected-count + GalleryStrings.texts[52];
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide; color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
} }
if root.hidden-selected > 0: Text { if root.hidden-selected > 0: Text {
y: 14px; width: parent.width; height: 10px; y: 14px; width: parent.width; height: 10px;
text: GalleryStrings.tr("跨页 ", "Other pages ", "Вне страницы ") + root.hidden-selected; text: GalleryStrings.texts[157] + root.hidden-selected;
color: #80858d; font-size: 10px; overflow: elide; color: #80858d; font-size: 10px; overflow: elide;
} }
} }
@@ -328,27 +329,27 @@ export component GalleryView inherits Rectangle {
horizontal-padding: root.toolbar-button-padding; horizontal-padding: root.toolbar-button-padding;
enabled: root.count > 0; enabled: root.count > 0;
text: root.count > 0 && root.selected-count == root.count text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор") ? GalleryStrings.texts[158]
: GalleryStrings.tr("全选", "Select all", "Выбрать все"); : GalleryStrings.texts[159];
alternate-text: root.count > 0 && root.selected-count == root.count alternate-text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("全选", "Select all", "Выбрать все") ? GalleryStrings.texts[159]
: GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор"); : GalleryStrings.texts[158];
clicked => { root.action("all-select", ""); } clicked => { root.action("all-select", ""); }
} }
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.tr("连接", "Connect", "Связь"); clicked => { root.action("connect", ""); } } GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.texts[37]; clicked => { root.action("connect", ""); } }
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.tr("断开", "Close", "Отключить"); clicked => { root.confirm-action("disconnect", self.text); } } GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.texts[36]; clicked => { root.confirm-action("disconnect", self.text); } }
batch-menu := GalleryMenuButton { batch-menu := GalleryMenuButton {
horizontal-padding: root.toolbar-button-padding; horizontal-padding: root.toolbar-button-padding;
width: self.preferred-button-width; enabled: root.selected-count > 0; width: self.preferred-button-width; enabled: root.selected-count > 0;
text: GalleryStrings.tr("更多", "More", "Ещё"); text: GalleryStrings.texts[68];
items: [ items: [
{label: root.selected-audio-muted ? GalleryStrings.tr("取消静音", "Unmute", "Со звуком") : GalleryStrings.tr("静音", "Mute", "Без звука"), enabled: root.selected-audio-available}, {label: root.selected-audio-muted ? GalleryStrings.texts[160] : GalleryStrings.texts[161], enabled: root.selected-audio-available},
{label: GalleryStrings.tr("分发文件", "Send file", "Отправить файл"), enabled: root.selected-count > 0}, {label: GalleryStrings.texts[162], enabled: root.selected-count > 0},
]; ];
opened => { root.device-tooltip-id = ""; root.action("release", ""); } opened => { root.device-tooltip-id = ""; root.action("release", ""); }
selected(index) => { selected(index) => {
if index == 0 { root.action(root.selected-audio-muted ? "unmute" : "mute", ""); } if index == 0 { root.action(root.selected-audio-muted ? "unmute" : "mute", ""); }
else { root.confirm-action("file", GalleryStrings.tr("分发文件", "Send file", "Отправить файл")); } else { root.confirm-action("file", GalleryStrings.texts[162]); }
} }
} }
} }
@@ -356,8 +357,8 @@ export component GalleryView inherits Rectangle {
x: parent.width - self.width; y: 0px; x: parent.width - self.width; y: 0px;
horizontal-padding: root.toolbar-button-padding; horizontal-padding: root.toolbar-button-padding;
primary: root.batch; primary: root.batch;
alternate-text: GalleryStrings.tr("选择", "Select", "Выбрать"); alternate-text: GalleryStrings.texts[163];
text: root.batch ? GalleryStrings.tr("完成", "Done", "Готово") : GalleryStrings.tr("选择", "Select", "Выбрать"); text: root.batch ? GalleryStrings.texts[164] : GalleryStrings.texts[163];
clicked => { root.batch = !root.batch; root.action("selection", ""); } clicked => { root.batch = !root.batch; root.action("selection", ""); }
} }
} }
@@ -404,7 +405,7 @@ export component GalleryView inherits Rectangle {
function show-hint() { function show-hint() {
if self.has-hover && !self.pressed && (self.edge-x != 0 || self.edge-y != 0) { if self.has-hover && !self.pressed && (self.edge-x != 0 || self.edge-y != 0) {
root.device-tooltip-id = "resize-grid"; root.device-tooltip-id = "resize-grid";
root.device-tooltip-text = GalleryStrings.tr("拖动表格分隔线,同步调整所有设备大小", "Drag a table divider to resize all devices", "Потяните за разделитель таблицы, чтобы изменить размер всех устройств"); root.device-tooltip-text = GalleryStrings.texts[165];
root.device-tooltip-x = grid.x + self.mouse-x; root.device-tooltip-x = grid.x + self.mouse-x;
root.device-tooltip-y = grid.y + self.mouse-y + 16px; root.device-tooltip-y = grid.y + self.mouse-y + 16px;
} else if root.device-tooltip-id == "resize-grid" { root.device-tooltip-id = ""; } } else if root.device-tooltip-id == "resize-grid" { root.device-tooltip-id = ""; }
@@ -460,8 +461,8 @@ export component GalleryView inherits Rectangle {
else if root.drag-source != "" { root.move-device-drag(self.mouse-x, self.mouse-y); } else if root.drag-source != "" { root.move-device-drag(self.mouse-x, self.mouse-y); }
} }
for tile[index] in root.tiles: tile-card := Rectangle { for tile[index] in root.tiles: tile-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") private property <string> presence: tile.online ? GalleryStrings.texts[166]
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); : tile.presence-known ? GalleryStrings.texts[167] : GalleryStrings.texts[100];
private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence;
accessible-role: button; accessible-label: self.details; accessible-role: button; accessible-label: self.details;
accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); } accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
@@ -473,7 +474,7 @@ export component GalleryView inherits Rectangle {
height: root.card-height; height: root.card-height;
opacity: root.dragging && root.drag-source == tile.id ? 0.45 : 1; opacity: root.dragging && root.drag-source == tile.id ? 0.45 : 1;
background: !tile.online ? DeviceCardStyle.offline-background background: !tile.online ? DeviceCardStyle.offline-background
: tile.selected ? #eaf3ff : root.batch && tile-touch.has-hover ? #f3f8fe : white; : tile.selected ? ControlStyle.selected : root.batch && tile-touch.has-hover ? ControlStyle.hover : white;
Rectangle { Rectangle {
x: root.card-inset; y: self.x; x: root.card-inset; y: self.x;
width: parent.width - self.x * 2; width: parent.width - self.x * 2;
@@ -491,12 +492,13 @@ export component GalleryView inherits Rectangle {
width: parent.width - self.x - (tile-touch.has-hover && !root.batch ? DeviceCardStyle.toolbar-width + 12px : root.batch ? 35px : 9px); width: parent.width - self.x - (tile-touch.has-hover && !root.batch ? DeviceCardStyle.toolbar-width + 12px : root.batch ? 35px : 9px);
height: 24px; text: tile.name; color: tile.online ? #24272d : DeviceCardStyle.offline-text; font-size: 12px; overflow: elide; vertical-alignment: center; height: 24px; text: tile.name; color: tile.online ? #24272d : DeviceCardStyle.offline-text; font-size: 12px; overflow: elide; vertical-alignment: center;
} }
if root.batch && !root.simplified: Rectangle { if root.batch && !root.simplified: FlatSurface {
x: parent.width - 27px; y: parent.height - 27px; x: parent.width - 27px; y: parent.height - 27px;
width: 18px; height: 18px; border-radius: 4px; border-width: 1px; width: 18px; height: 18px; border-radius: 4px;
background: tile.selected ? (tile-touch.pressed ? #5c9fdf : tile-touch.has-hover ? #659fe0 : #6dabea) : tile-touch.has-hover ? #f3f8fe : white; background: tile.selected ? (tile-touch.pressed ? #254cc0 : tile-touch.has-hover ? #4574f0 : #3564e3)
border-color: tile.selected ? #6dabea : tile-touch.has-hover ? #76aeea : #b7bec8; : tile-touch.pressed ? ControlStyle.pressed : tile-touch.has-hover ? ControlStyle.hover : ControlStyle.surface;
animate background, border-color { duration: 120ms; } outline-color: tile.selected ? UiSurface.focus : tile-touch.has-hover ? UiSurface.hover : UiSurface.edge;
animate background, outline-color { duration: 120ms; }
if tile.selected: Text { if tile.selected: Text {
text: FontAwesomeIcons.check; color: white; text: FontAwesomeIcons.check; color: white;
font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 11px; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 11px;
@@ -520,7 +522,7 @@ export component GalleryView inherits Rectangle {
changed over-actions => { if self.has-hover { self.show-details(); } } changed over-actions => { if self.has-hover { self.show-details(); } }
if tile-touch.has-hover && !root.dragging && !root.resizing && !root.batch && !root.simplified: DeviceCardActions { if tile-touch.has-hover && !root.dragging && !root.resizing && !root.batch && !root.simplified: DeviceCardActions {
x: parent.width - self.width; y: parent.height - 23px; x: parent.width - self.width; y: parent.height - 23px;
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub"); remove-label: GalleryStrings.texts[168];
connected: tile.connected; connected: tile.connected;
edit-alias => { root.action("release", ""); root.edit-alias(tile.id, tile.name); } edit-alias => { root.action("release", ""); root.edit-alias(tile.id, tile.name); }
edit-groups => { root.edit-groups(tile.id, tile.name); } edit-groups => { root.edit-groups(tile.id, tile.name); }
@@ -531,32 +533,32 @@ export component GalleryView inherits Rectangle {
} }
} }
// Draw each table line once, continuously across occupied and empty cells. // Draw each table line once, continuously across occupied and empty cells.
for column in root.columns + 1: Rectangle { for column in root.columns + 1: SurfaceSeparator {
visible: root.count > 0; visible: root.count > 0;
x: min(column * root.card-width, grid.table-width - 1px); x: min(column * root.card-width, grid.table-width - 1px);
y: 0px; y: 0px;
width: 1px; height: grid.table-height; width: 1px; height: grid.table-height;
background: root.resizing || (grid.has-hover && grid.edge-x != 0 && grid.column-line == column) vertical: true; outline-color: root.resizing || (grid.has-hover && grid.edge-x != 0 && grid.column-line == column)
? #76aeea : #c9cdd2; ? UiSurface.focus : UiSurface.separator;
} }
for row in root.rows + 1: Rectangle { for row in root.rows + 1: SurfaceSeparator {
visible: root.count > 0; visible: root.count > 0;
x: 0px; x: 0px;
y: min(row * root.card-height, grid.table-height - 1px); y: min(row * root.card-height, grid.table-height - 1px);
width: grid.table-width; height: 1px; width: grid.table-width; height: 1px;
background: root.resizing || (grid.has-hover && grid.edge-y != 0 && grid.row-line == row) outline-color: root.resizing || (grid.has-hover && grid.edge-y != 0 && grid.row-line == row)
? #76aeea : #c9cdd2; ? UiSurface.focus : UiSurface.separator;
} }
if root.drag-target >= 0: Rectangle { if root.drag-target >= 0: FlatSurface {
x: mod(root.drag-target, root.columns) * root.card-width + 1px; x: mod(root.drag-target, root.columns) * root.card-width + 1px;
y: floor(root.drag-target / root.columns) * root.card-height + 1px; y: floor(root.drag-target / root.columns) * root.card-height + 1px;
width: root.card-width - 2px; height: root.card-height - 2px; width: root.card-width - 2px; height: root.card-height - 2px;
background: #76aeea26; border-width: 2px; border-color: #4a94de; background: #76aeea26; outline-color: UiSurface.focus;
} }
if root.count == 0: Text { if root.count == 0: Text {
text: root.simplified text: root.simplified
? GalleryStrings.tr("暂无设备,退出精简模式后可添加", "No devices. Exit compact mode to add devices", "Нет устройств. Выйдите из компактного режима, чтобы добавить их") ? GalleryStrings.texts[169]
: GalleryStrings.tr("添加设备,建立你的桌面墙", "Add devices to build your DeskHub", "Добавьте устройства в DeskHub"); : GalleryStrings.texts[170];
color: #808080; horizontal-alignment: center; vertical-alignment: center; color: #808080; horizontal-alignment: center; vertical-alignment: center;
} }
} }
@@ -566,7 +568,7 @@ export component GalleryView inherits Rectangle {
height: footer.y - 12px - self.y; height: footer.y - 12px - self.y;
HorizontalLayout { HorizontalLayout {
x: 0px; y: 0px; width: parent.width - root.view-controls-space; height: 24px; spacing: 8px; x: 0px; y: 0px; width: parent.width - root.view-controls-space; height: 24px; spacing: 8px;
GalleryButton { width: 52px; text: GalleryStrings.tr("返回", "Back", "Назад"); clicked => { root.action("focus", ""); } } GalleryButton { width: 52px; text: GalleryStrings.texts[135]; clicked => { root.action("focus", ""); } }
Rectangle { Rectangle {
horizontal-stretch: 1; min-width: 0px; horizontal-stretch: 1; min-width: 0px;
Text { x: 0px; width: parent.width; height: parent.height; text: root.focused.name; color: root.focused.online ? #303943 : DeviceCardStyle.offline-text; font-size: 12px; font-weight: 600; overflow: elide; horizontal-alignment: left; vertical-alignment: center; } Text { x: 0px; width: parent.width; height: parent.height; text: root.focused.name; color: root.focused.online ? #303943 : DeviceCardStyle.offline-text; font-size: 12px; font-weight: 600; overflow: elide; horizontal-alignment: left; vertical-alignment: center; }
@@ -577,10 +579,10 @@ export component GalleryView inherits Rectangle {
background: root.controlling ? #e7f1fd : #f0f2f5; background: root.controlling ? #e7f1fd : #f0f2f5;
focus-state := Text { focus-state := Text {
x: 8px; width: parent.width - 16px; height: parent.height; x: 8px; width: parent.width - 16px; height: parent.height;
text: root.group-controlling ? GalleryStrings.tr("群控中", "Group control", "Групповое") text: root.group-controlling ? GalleryStrings.texts[171]
: root.controlling ? GalleryStrings.tr("正在操控", "Controlling", "Управление") : root.controlling ? GalleryStrings.texts[172]
: root.focused.connected && root.focused.has-frame && !root.focused.stale : root.focused.connected && root.focused.has-frame && !root.focused.stale
? GalleryStrings.tr("只读预览", "View only", "Просмотр") : root.focused.status; ? GalleryStrings.texts[173] : root.focused.status;
font-size: 10px; color: root.controlling ? #3e77b3 : #6b7480; vertical-alignment: center; overflow: elide; font-size: 10px; color: root.controlling ? #3e77b3 : #6b7480; vertical-alignment: center; overflow: elide;
} }
} }
@@ -588,20 +590,20 @@ export component GalleryView inherits Rectangle {
horizontal-padding: root.toolbar-button-padding; horizontal-padding: root.toolbar-button-padding;
primary: root.group-controlling; primary: root.group-controlling;
enabled: root.group-controlling || (root.focused.connected && root.focused.has-frame && !root.focused.stale); enabled: root.group-controlling || (root.focused.connected && root.focused.has-frame && !root.focused.stale);
text: root.group-controlling ? GalleryStrings.tr("关闭群控", "Stop group", "Откл. группу") : GalleryStrings.tr("开启群控", "Group control", "Групп. ввод"); text: root.group-controlling ? GalleryStrings.texts[174] : GalleryStrings.texts[175];
alternate-text: root.group-controlling ? GalleryStrings.tr("开启群控", "Group control", "Групп. ввод") : GalleryStrings.tr("关闭群控", "Stop group", "Откл. группу"); alternate-text: root.group-controlling ? GalleryStrings.texts[175] : GalleryStrings.texts[174];
accessible-role: button; accessible-role: button;
accessible-label: self.text + GalleryStrings.tr(":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах"); accessible-label: self.text + GalleryStrings.texts[176];
accessible-action-default => { self.clicked(); } accessible-action-default => { self.clicked(); }
clicked => { root.action(root.group-controlling ? "group-release" : "group-control", root.focused.id); } clicked => { root.action(root.group-controlling ? "group-release" : "group-control", root.focused.id); }
} }
GalleryButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.tr("释放操控", "Release", "Отпустить") : GalleryStrings.tr("接管操控", "Control", "Управлять")) : GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } } GalleryButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.texts[177] : GalleryStrings.texts[178]) : GalleryStrings.texts[37]; clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } }
} }
Rectangle { FlatSurface {
x: 0px; y: rail.visible ? focus-divider.y + focus-divider.height : 36px; x: 0px; y: rail.visible ? focus-divider.y + focus-divider.height : 36px;
width: parent.width; height: parent.height - self.y; width: parent.width; height: parent.height - self.y;
background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: UiRadius.surface; background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: UiRadius.surface;
border-width: root.controlling ? 2px : 1px; border-color: root.controlling ? #76aeea : root.focused.online ? #d5dbe3 : DeviceCardStyle.offline-border; outline-color: root.controlling ? UiSurface.focus : UiSurface.edge;
video := Rectangle { video := Rectangle {
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px; x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
background: root.focused.online ? #24272d : DeviceCardStyle.offline-preview; background: root.focused.online ? #24272d : DeviceCardStyle.offline-preview;
@@ -668,12 +670,12 @@ export component GalleryView inherits Rectangle {
// Collapse the focused tile and its gap without changing the page model. // Collapse the focused tile and its gap without changing the page model.
width: tile.id == root.focused.id ? 0px : root.rail-slot-width; width: tile.id == root.focused.id ? 0px : root.rail-slot-width;
height: root.rail-card-height; visible: tile.id != root.focused.id; height: root.rail-card-height; visible: tile.id != root.focused.id;
rail-card := Rectangle { rail-card := FlatSurface {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") private property <string> presence: tile.online ? GalleryStrings.texts[166]
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); : tile.presence-known ? GalleryStrings.texts[167] : GalleryStrings.texts[100];
private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence;
width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: UiRadius.surface; width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: UiRadius.surface;
background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; border-width: 1px; border-color: tile.online ? #d5dbe3 : DeviceCardStyle.offline-border; background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; outline-color: UiSurface.edge;
accessible-role: button; accessible-role: button;
accessible-label: self.details; accessible-label: self.details;
accessible-action-default => { root.action("focus", tile.id); } accessible-action-default => { root.action("focus", tile.id); }
@@ -714,9 +716,9 @@ export component GalleryView inherits Rectangle {
visible: root.focus-rail-visible; visible: root.focus-rail-visible;
x: 0px; y: rail.y + rail.height; width: parent.width; height: 12px; x: 0px; y: rail.y + rail.height; width: parent.width; height: 12px;
private property <bool> highlighted: divider-touch.has-hover || root.focus-resizing || divider-focus.has-focus; private property <bool> highlighted: divider-touch.has-hover || root.focus-resizing || divider-focus.has-focus;
Rectangle { SurfaceSeparator {
x: 0px; y: 5px; width: parent.width; height: 1px; x: 0px; y: 5px; width: parent.width; height: 1px;
background: focus-divider.highlighted ? #a6c9ed : #e6e9ed; outline-color: focus-divider.highlighted ? UiSurface.focus : UiSurface.separator;
} }
Rectangle { Rectangle {
x: (parent.width - self.width) / 2; y: 4px; width: 36px; height: 3px; border-radius: 1.5px; x: (parent.width - self.width) / 2; y: 4px; width: 36px; height: 3px; border-radius: 1.5px;
@@ -742,7 +744,7 @@ export component GalleryView inherits Rectangle {
function show-hint() { function show-hint() {
if self.has-hover && !root.focus-resizing { if self.has-hover && !root.focus-resizing {
root.device-tooltip-id = "resize-focus"; root.device-tooltip-id = "resize-focus";
root.device-tooltip-text = GalleryStrings.tr("上下拖动,调整缩略图与主画面大小", "Drag up or down to resize the thumbnails and main preview", "Потяните вверх или вниз, чтобы изменить размер миниатюр и основного экрана"); root.device-tooltip-text = GalleryStrings.texts[179];
root.device-tooltip-x = focus-view.x + self.mouse-x; root.device-tooltip-x = focus-view.x + self.mouse-x;
root.device-tooltip-y = focus-view.y + focus-divider.y + self.mouse-y + 16px; root.device-tooltip-y = focus-view.y + focus-divider.y + self.mouse-y + 16px;
} else if root.device-tooltip-id == "resize-focus" { root.device-tooltip-id = ""; } } else if root.device-tooltip-id == "resize-focus" { root.device-tooltip-id = ""; }
@@ -775,7 +777,7 @@ export component GalleryView inherits Rectangle {
} }
Rectangle { Rectangle {
y: parent.height - 44px; height: 44px; background: #fafbfc; y: parent.height - 44px; height: 44px; background: #fafbfc;
Rectangle { y: 0px; width: parent.width; height: 1px; background: #e1e4e8; } SurfaceSeparator { y: 0px; width: parent.width; height: 1px; outline-color: UiSurface.separator; }
} }
// Connection status and pagination remain available in every selection state. // Connection status and pagination remain available in every selection state.
footer := Rectangle { footer := Rectangle {
@@ -786,15 +788,15 @@ export component GalleryView inherits Rectangle {
connection-count := Text { connection-count := Text {
x: 0px; width: min(self.preferred-width, parent.width - network-button.width - 12px); height: parent.height; x: 0px; width: min(self.preferred-width, parent.width - network-button.width - 12px); height: parent.height;
text: root.group-controlling text: root.group-controlling
? GalleryStrings.tr("群控中 · 所有已连接设备", "Group control · All connected devices", "Групповое управление · Все подключения") ? GalleryStrings.texts[180]
: root.connected-summary; : root.connected-summary;
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide; color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
} }
network-button := SecondaryButton { network-button := SecondaryButton {
x: connection-count.x + connection-count.width + 12px; x: connection-count.x + connection-count.width + 12px;
min-width: 64px; width: 64px; height: parent.height; width: 64px; height: parent.height;
border-color: #c8cbd0; border-radius: UiRadius.control; outline-color: UiSurface.edge; border-radius: UiRadius.control;
text: GalleryStrings.tr("网络状态", "Network", "Сеть"); text: GalleryStrings.texts[181];
accessible-role: button; accessible-role: button;
accessible-label: self.text; accessible-label: self.text;
accessible-action-default => { self.clicked(); } accessible-action-default => { self.clicked(); }
@@ -815,16 +817,17 @@ export component GalleryView inherits Rectangle {
compact-toggle := GalleryToolbarButton { compact-toggle := GalleryToolbarButton {
horizontal-padding: root.toolbar-button-padding; horizontal-padding: root.toolbar-button-padding;
primary: root.simplified; primary: root.simplified;
alternate-text: GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид"); alternate-text: GalleryStrings.texts[182];
text: root.simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид") text: root.simplified ? GalleryStrings.texts[182]
: GalleryStrings.tr("精简模式", "Compact", "Компактно"); : GalleryStrings.texts[183];
clicked => { root.toggle-simplified(); } clicked => { root.toggle-simplified(); }
} }
IconButton { IconButton {
width: 24px; height: 24px; width: 24px; height: 24px;
border-radius: UiRadius.control; border-radius: UiRadius.control;
normal-background: ControlStyle.surface;
icon: root.fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; icon: root.fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран"); tooltip: root.fullscreen ? GalleryStrings.texts[184] : GalleryStrings.texts[185];
clicked => { root.toggle-fullscreen(); } clicked => { root.toggle-fullscreen(); }
} }
} }
@@ -840,11 +843,11 @@ export component GalleryView inherits Rectangle {
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
} }
} }
if root.dragging: Rectangle { if root.dragging: FlatSurface {
x: clamp(grid.x + root.drag-x + 14px, 8px, parent.width - self.width - 8px); x: clamp(grid.x + root.drag-x + 14px, 8px, parent.width - self.width - 8px);
y: clamp(grid.y + root.drag-y + 14px, 8px, footer.y - self.height - 12px); y: clamp(grid.y + root.drag-y + 14px, 8px, footer.y - self.height - 12px);
width: 224px; height: 54px; z: 10; width: 224px; height: max(54px, drag-hint.y + drag-hint.height + 9px); z: 10;
background: #fffffff2; border-width: 1px; border-color: #76aeea; border-radius: UiRadius.surface; background: white; outline-color: UiSurface.focus; border-radius: UiRadius.surface;
Rectangle { Rectangle {
x: 8px; y: 9px; width: 64px; height: 36px; x: 8px; y: 9px; width: 64px; height: 36px;
background: root.dragged-tile.online ? #24272d : DeviceCardStyle.offline-preview; background: root.dragged-tile.online ? #24272d : DeviceCardStyle.offline-preview;
@@ -854,28 +857,28 @@ export component GalleryView inherits Rectangle {
} }
} }
Text { x: 80px; y: 8px; width: parent.width - 88px; height: 18px; text: root.dragged-tile.name; font-size: 12px; color: #303943; overflow: elide; } Text { x: 80px; y: 8px; width: parent.width - 88px; height: 18px; text: root.dragged-tile.name; font-size: 12px; color: #303943; overflow: elide; }
Text { drag-hint := Text {
x: 80px; y: 29px; width: parent.width - 88px; height: 16px; x: 80px; y: 29px; width: parent.width - 88px; height: max(16px, self.preferred-height);
text: root.drag-page-direction != 0 ? GalleryStrings.tr("停留翻页,松开取消", "Hold to turn page", "Удерживайте для перелистывания") text: root.drag-page-direction != 0 ? GalleryStrings.texts[186]
: root.drag-target >= 0 ? GalleryStrings.tr("松开鼠标交换位置", "Release to swap", "Отпустите для обмена") : root.drag-target >= 0 ? GalleryStrings.texts[187]
: GalleryStrings.tr("拖到另一台设备上", "Drag onto another device", "На другое устройство"); : GalleryStrings.texts[188];
font-size: 10px; color: #527ca8; overflow: elide; font-size: 10px; color: #527ca8; wrap: word-wrap;
} }
} }
if root.swap-save-failed: Rectangle { if root.swap-save-failed: FlatSurface {
x: (parent.width - self.width) / 2; y: footer.y - self.height - 14px; x: (parent.width - self.width) / 2; y: footer.y - self.height - 14px;
width: min(420px, parent.width - 32px); height: 36px; z: 10; width: min(420px, parent.width - 32px); height: 36px; z: 10;
background: #fff2f0; border-width: 1px; border-color: #e9b7b0; border-radius: UiRadius.surface; background: #fff2f0; outline-color: UiSurface.edge; border-radius: UiRadius.surface;
Text { x: 12px; y: 0px; width: parent.width - 24px; height: parent.height; font-size: 12px; color: #9d4339; vertical-alignment: center; Text { x: 12px; y: 0px; width: parent.width - 24px; height: parent.height; font-size: 12px; color: #9d4339; vertical-alignment: center;
text: GalleryStrings.tr("换位未保存,请重试", "Couldn't save positions. Please try again.", "Не удалось сохранить порядок. Повторите попытку."); overflow: elide; text: GalleryStrings.texts[189]; overflow: elide;
} }
} }
// Device details are rendered by MainWindow; resize hints stay local. // Device details are rendered by MainWindow; resize hints stay local.
if root.device-tooltip-visible && (root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus"): Rectangle { if root.device-tooltip-visible && (root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus"): FlatSurface {
x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px); x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px);
y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px); y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px);
width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px; width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px;
background: #22252bf2; border-width: 1px; border-color: #ffffff38; border-radius: UiRadius.control; background: #22252bf2; outline-color: UiSurface.edge; border-radius: UiRadius.control;
device-tooltip-label := Text { device-tooltip-label := Text {
x: 8px; y: 8px; width: parent.width - 16px; x: 8px; y: 8px; width: parent.width - 16px;
text: root.device-tooltip-text; color: white; font-size: 11px; wrap: word-wrap; text: root.device-tooltip-text; color: white; font-size: 11px; wrap: word-wrap;
@@ -884,45 +887,45 @@ export component GalleryView inherits Rectangle {
if root.adding: Rectangle { if root.adding: Rectangle {
background: #00000042; background: #00000042;
TouchArea {} TouchArea {}
add-dialog := Rectangle { add-dialog := FlatSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 400px; height: 306px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 400px; height: 306px;
background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2; background: white; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
function submit() { function submit() {
if root.add-device(device-id.text, password.text, group.text, remember.checked) { if root.add-device(device-id.text, password.text, group.text, remember.checked) {
password.text = ""; password.text = "";
root.adding = false; root.adding = false;
} }
} }
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("添加设备", "Add device", "Добавить устройство"); font-size: 16px; } Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.texts[190]; font-size: 16px; }
Text { x: 18px; y: 56px; width: 72px; height: 30px; text: GalleryStrings.tr("设备 ID", "Device ID", "ID"); vertical-alignment: center; font-size: 12px; } Text { x: 18px; y: 56px; width: 72px; height: 30px; text: GalleryStrings.texts[191]; vertical-alignment: center; font-size: 12px; }
device-id := RoundedLineEdit { device-id := RoundedLineEdit {
x: 96px; y: 56px; width: 286px; height: 30px; x: 96px; y: 56px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("9 位设备 ID", "9-digit device ID", "ID из 9 цифр"); placeholder-text: GalleryStrings.texts[192];
input-type: InputType.number; input-type: InputType.number;
accepted => { password.focus(); } accepted => { password.focus(); }
} }
Text { x: 18px; y: 100px; width: 72px; height: 30px; text: GalleryStrings.tr("密码", "Password", "Пароль"); vertical-alignment: center; font-size: 12px; } Text { x: 18px; y: 100px; width: 72px; height: 30px; text: GalleryStrings.texts[193]; vertical-alignment: center; font-size: 12px; }
password := RoundedLineEdit { password := RoundedLineEdit {
x: 96px; y: 100px; width: 286px; height: 30px; x: 96px; y: 100px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("设备连接密码", "Device password", "Пароль устройства"); placeholder-text: GalleryStrings.texts[194];
input-type: InputType.password; input-type: InputType.password;
accepted => { group.focus(); } accepted => { group.focus(); }
} }
Text { x: 18px; y: 144px; width: 72px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); vertical-alignment: center; font-size: 12px; } Text { x: 18px; y: 144px; width: 72px; height: 30px; text: GalleryStrings.texts[101]; vertical-alignment: center; font-size: 12px; }
group := RoundedLineEdit { group := RoundedLineEdit {
x: 96px; y: 144px; width: 286px; height: 30px; x: 96px; y: 144px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); placeholder-text: GalleryStrings.texts[195];
accepted => { add-dialog.submit(); } accepted => { add-dialog.submit(); }
} }
remember := ImGuiCheckBox { x: 96px; y: 188px; } remember := ImGuiCheckBox { x: 96px; y: 188px; }
Text { x: 123px; y: 188px; height: 21px; text: GalleryStrings.tr("记住密码", "Remember password", "Запомнить пароль"); font-size: 12px; vertical-alignment: center; Text { x: 123px; y: 188px; height: 21px; text: GalleryStrings.texts[196]; font-size: 12px; vertical-alignment: center;
TouchArea { clicked => { remember.checked = !remember.checked; } } TouchArea { clicked => { remember.checked = !remember.checked; } }
} }
Text { x: 18px; y: 216px; width: 364px; height: 28px; text: root.notice; color: #8e4740; font-size: 11px; wrap: word-wrap; overflow: elide; } Text { x: 18px; y: 216px; width: 364px; height: 28px; text: root.notice; color: #8e4740; font-size: 11px; wrap: word-wrap; overflow: elide; }
GalleryButton { x: 172px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } } GalleryButton { x: 136px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.texts[33]; clicked => { password.text = ""; root.adding = false; } }
GalleryButton { GalleryButton {
x: 254px; y: 260px; width: 128px; height: 28px; primary: true; x: 218px; y: 260px; width: 164px; height: 28px; primary: true;
text: GalleryStrings.tr("添加并连接", "Add and connect", "Подключить"); text: GalleryStrings.texts[197];
enabled: device-id.text != "" && password.text != ""; enabled: device-id.text != "" && password.text != "";
clicked => { add-dialog.submit(); } clicked => { add-dialog.submit(); }
} }
@@ -931,38 +934,40 @@ export component GalleryView inherits Rectangle {
if root.importing: Rectangle { if root.importing: Rectangle {
background: #00000042; background: #00000042;
TouchArea {} TouchArea {}
Rectangle { FlatSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px;
background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2; background: white; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("从最近连接添加", "Add from recent connections", "Добавить из недавних"); font-size: 16px; } Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.texts[155]; font-size: 16px; }
Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.tr("加入桌面墙后,可通过批量选择连接", "Add devices, then use Select to connect them.", "После добавления подключайтесь через выбор."); color: #62666d; font-size: 11px; } Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.texts[198]; color: #62666d; font-size: 11px; }
ImGuiCheckBox { x: 18px; y: 74px; enabled: root.recent-available > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-available; } ImGuiCheckBox { x: 18px; y: 74px; enabled: root.recent-available > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-available; }
Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.tr("全选", "Select all", "Выбрать все"); color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; } Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.texts[159]; color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; }
TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-available > 0; clicked => { root.action("recent-all", ""); } } TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-available > 0; clicked => { root.action("recent-all", ""); } }
Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.tr("待添加 ", "To add ", "К добавлению ") + root.recent-selected + " / " + root.recent-available; horizontal-alignment: right; vertical-alignment: center; font-size: 12px; color: #62666d; } Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.texts[199] + root.recent-selected + " / " + root.recent-available; horizontal-alignment: right; vertical-alignment: center; font-size: 12px; color: #62666d; }
Rectangle { FlatSurface {
x: 18px; y: 102px; width: 404px; height: 160px; x: 18px; y: 102px; width: 404px; height: 160px;
border-width: 1px; border-color: #d8dadd; border-radius: UiRadius.surface; clip: true; outline-color: UiSurface.edge; border-radius: UiRadius.surface; clip: true;
recent-list := ScrollView { recent-list := ScrollView {
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px; x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
viewport-width: self.width - 12px; viewport-width: self.width - 12px;
viewport-height: max(self.height, root.recent-devices.length * 44px); viewport-height: max(self.height, root.recent-devices.length * 44px);
for device[index] in root.recent-devices: Rectangle { for device[index] in root.recent-devices: Rectangle {
y: index * 44px; width: recent-list.viewport-width; height: 44px; y: index * 44px; width: recent-list.viewport-width; height: 44px;
background: device.added ? #f1f2f4 : device.selected ? #edf5fe : row-touch.has-hover ? #f4f5f6 : white; background: device.added ? ControlStyle.disabled
: device.selected ? (row-touch.pressed ? ControlStyle.selected-pressed : row-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected)
: row-touch.pressed ? ControlStyle.pressed : row-touch.has-hover ? ControlStyle.hover : white;
ImGuiCheckBox { x: 8px; y: 11px; enabled: !device.added; checked: device.added || device.selected; } ImGuiCheckBox { x: 8px; y: 11px; enabled: !device.added; checked: device.added || device.selected; }
Text { x: 36px; y: 3px; width: parent.width - 126px; height: 20px; text: device.name; color: device.added ? #92969e : #24272d; font-size: 12px; overflow: elide; vertical-alignment: center; } Text { x: 36px; y: 3px; width: parent.width - 126px; height: 20px; text: device.name; color: device.added ? #92969e : #24272d; font-size: 12px; overflow: elide; vertical-alignment: center; }
Text { x: 36px; y: 23px; width: parent.width - 126px; height: 16px; text: device.id; font-size: 11px; color: device.added ? #92969e : #808080; } Text { x: 36px; y: 23px; width: parent.width - 126px; height: 16px; text: device.id; font-size: 11px; color: device.added ? #92969e : #808080; }
Rectangle { x: parent.width - 74px; y: 19px; width: 6px; height: 6px; border-radius: 3px; background: !device.added && device.online ? #21b567 : #92969e; } Rectangle { x: parent.width - 74px; y: 19px; width: 6px; height: 6px; border-radius: 3px; background: !device.added && device.online ? #21b567 : #92969e; }
Text { Text {
x: parent.width - 61px; y: 12px; width: 57px; height: 20px; x: parent.width - 61px; y: 12px; width: 57px; height: 20px;
text: device.added ? GalleryStrings.tr("已添加", "Added", "Добавлено") : device.online ? GalleryStrings.tr("在线", "Online", "В сети") : GalleryStrings.tr("离线", "Offline", "Не в сети"); text: device.added ? GalleryStrings.texts[200] : device.online ? GalleryStrings.texts[166] : GalleryStrings.texts[167];
color: device.added ? #92969e : device.online ? #17854c : #808080; font-size: 11px; vertical-alignment: center; color: device.added ? #92969e : device.online ? #17854c : #808080; font-size: 11px; vertical-alignment: center;
} }
row-touch := TouchArea { enabled: !device.added; clicked => { root.action("recent-select", device.id); } } row-touch := TouchArea { enabled: !device.added; clicked => { root.action("recent-select", device.id); } }
} }
} }
if root.recent-devices.length == 0: Text { text: GalleryStrings.tr("暂无最近连接", "No recent connections", "Нет недавних подключений"); color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; } if root.recent-devices.length == 0: Text { text: GalleryStrings.texts[89]; color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
Rectangle { Rectangle {
x: -UiRadius.surface; y: -UiRadius.surface; x: -UiRadius.surface; y: -UiRadius.surface;
width: parent.width + 2 * UiRadius.surface; width: parent.width + 2 * UiRadius.surface;
@@ -970,39 +975,39 @@ export component GalleryView inherits Rectangle {
border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface; border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface;
border-color: white; border-color: white;
} }
Rectangle { FlatSurface {
width: parent.width; height: parent.height; width: parent.width; height: parent.height;
border-width: 1px; border-radius: UiRadius.surface; border-radius: UiRadius.surface;
border-color: #d8dadd; outline-color: UiSurface.edge;
} }
} }
Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); font-size: 12px; vertical-alignment: center; } Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.texts[101]; font-size: 12px; vertical-alignment: center; }
import-group := RoundedLineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); } import-group := RoundedLineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.texts[195]; }
Text { x: 18px; y: 309px; width: 404px; height: 20px; text: root.notice; font-size: 11px; color: #8e4740; overflow: elide; } Text { x: 18px; y: 309px; width: 404px; height: 20px; text: root.notice; font-size: 11px; color: #8e4740; overflow: elide; }
GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } } GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.texts[33]; clicked => { root.importing = false; } }
GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add selected", "Добавить"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } } GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.texts[201]; enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } }
} }
} }
if root.confirming: Rectangle { if root.confirming: Rectangle {
background: #00000042; TouchArea {} background: #00000042; TouchArea {}
Rectangle { FlatSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2;
width: 360px; height: confirmation-list.height + 118px; width: 360px; height: confirmation-list.height + 118px;
background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2; background: white; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
Text { Text {
x: 20px; y: 18px; width: parent.width - 132px; height: 24px; x: 20px; y: 18px; width: parent.width - 132px; height: 24px;
text: root.action-code == "disconnect" ? GalleryStrings.tr("断开连接", "Disconnect devices", "Отключение") : root.action-name; text: root.action-code == "disconnect" ? GalleryStrings.texts[96] : root.action-name;
color: #24272d; font-size: 16px; font-weight: 600; vertical-alignment: center; color: #24272d; font-size: 16px; font-weight: 600; vertical-alignment: center;
} }
Rectangle { Rectangle {
x: parent.width - 108px; y: 20px; width: 88px; height: 22px; x: parent.width - 108px; y: 20px; width: 88px; height: 22px;
background: #eff3f8; border-radius: UiRadius.surface; background: #eff3f8; border-radius: UiRadius.surface;
Text { text: root.selected-count + GalleryStrings.tr(" 台设备", " devices", " устр."); color: #626e7d; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } Text { text: GalleryStrings.device-count(root.selected-count); color: #626e7d; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; }
} }
confirmation-list := Rectangle { confirmation-list := FlatSurface {
x: 20px; y: 58px; width: parent.width - 40px; x: 20px; y: 58px; width: parent.width - 40px;
height: clamp(root.selected-devices.length * 52px + 2px, 54px, 210px); height: clamp(root.selected-devices.length * 52px + 2px, 54px, 210px);
background: #fcfdfe; border-width: 1px; border-color: #e2e6eb; border-radius: UiRadius.surface; clip: true; background: #fcfdfe; outline-color: UiSurface.edge; border-radius: UiRadius.surface; clip: true;
confirmation-scroll := ScrollView { confirmation-scroll := ScrollView {
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px; x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
viewport-width: self.width - 12px; viewport-width: self.width - 12px;
@@ -1016,16 +1021,16 @@ export component GalleryView inherits Rectangle {
width: GalleryStrings.language == 0 ? 68px : 104px; height: 20px; border-radius: 10px; width: GalleryStrings.language == 0 ? 68px : 104px; height: 20px; border-radius: 10px;
background: device.connected ? #eaf6ee : #f0f2f5; background: device.connected ? #eaf6ee : #f0f2f5;
Rectangle { x: 9px; y: 7px; width: 6px; height: 6px; border-radius: 3px; background: device.connected ? #27a762 : #a0a6b0; } Rectangle { x: 9px; y: 7px; width: 6px; height: 6px; border-radius: 3px; background: device.connected ? #27a762 : #a0a6b0; }
Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.tr("已连接", "Connected", "Подключено") : GalleryStrings.tr("未连接", "Not connected", "Нет связи"); color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; } Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.texts[202] : GalleryStrings.texts[203]; color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; }
} }
if index + 1 < root.selected-devices.length: Rectangle { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #edf0f3; } if index + 1 < root.selected-devices.length: SurfaceSeparator { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; outline-color: UiSurface.separator; }
} }
} }
} }
GalleryButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.confirming = false; } } GalleryButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.texts[33]; clicked => { root.confirming = false; } }
GalleryButton { GalleryButton {
x: parent.width - 126px; y: parent.height - 46px; width: 108px; height: 28px; primary: true; x: parent.width - 126px; y: parent.height - 46px; width: 108px; height: 28px; primary: true;
text: root.action-code == "disconnect" ? GalleryStrings.tr("确认断开", "Disconnect", "Отключить") : GalleryStrings.tr("确认执行", "Continue", "Продолжить"); text: root.action-code == "disconnect" ? GalleryStrings.texts[204] : GalleryStrings.texts[205];
clicked => { root.confirming = false; root.action(root.action-code, ""); } clicked => { root.confirming = false; root.action(root.action-code, ""); }
} }
} }
+53 -26
View File
@@ -1,16 +1,27 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
import { FontAwesomeIcons } from "common.slint"; import { FontAwesomeIcons, ControlStyle } from "common.slint";
// Shared by history and DeskHub; system choices precede the named groups. // Shared by history and DeskHub; system choices precede the named groups.
export component GroupFilter inherits Rectangle { export component GroupFilter inherits FlatSurface {
in property <[string]> model; in property <[string]> model;
in property <int> system-count: 2; in property <int> system-count: 2;
in property <bool> enabled: true; in property <bool> enabled: true;
in property <color> normal-border-color: #c9cdd2; // An embedded picker shares its enclosing field's outline and focus state.
in property <color> hover-border-color: root.normal-border-color; in property <bool> embedded: false;
in property <color> focus-border-color: #8db5df; in property <bool> show-indicator: true;
in property <length> popup-min-width: 0px;
in property <length> text-size: 12px;
in property <int> text-weight: 400;
in property <color> text-color: #30343b;
in property <color> normal-background: ControlStyle.surface;
in property <color> normal-outline-color: UiSurface.edge;
in property <color> hover-outline-color: UiSurface.hover;
in property <color> focus-outline-color: UiSurface.focus;
in-out property <int> current-index: 0; in-out property <int> current-index: 0;
out property <bool> has-focus: trigger-focus.has-focus || popup.is-open;
out property <bool> has-hover: root.enabled && trigger-touch.has-hover && !popup.is-open;
callback selected(int); callback selected(int);
private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count; private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count;
private property <length> list-height: root.model.length * 26px + (root.has-divider ? 6px : 0px); private property <length> list-height: root.model.length * 26px + (root.has-divider ? 6px : 0px);
@@ -25,12 +36,16 @@ export component GroupFilter inherits Rectangle {
} }
width: 155px; height: 28px; width: 155px; height: 28px;
background: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white; background: root.embedded ? (root.enabled && trigger-touch.has-hover ? ControlStyle.hover : transparent)
border-width: 1px; : !root.enabled ? ControlStyle.disabled
border-color: popup.is-open || trigger-focus.has-focus ? root.focus-border-color : trigger-touch.pressed || popup.is-open ? ControlStyle.pressed
: trigger-touch.has-hover ? root.hover-border-color : root.normal-border-color; : trigger-touch.has-hover ? ControlStyle.hover : root.normal-background;
outline-enabled: !root.embedded;
outline-focused: popup.is-open || trigger-focus.has-focus;
outline-color: popup.is-open || trigger-focus.has-focus ? root.focus-outline-color
: trigger-touch.has-hover ? root.hover-outline-color : root.normal-outline-color;
border-radius: UiRadius.control; border-radius: UiRadius.control;
animate background, border-color { duration: 100ms; easing: ease-out; } animate background, outline-color { duration: 100ms; easing: ease-out; }
forward-focus: trigger-focus; forward-focus: trigger-focus;
accessible-role: combobox; accessible-role: combobox;
accessible-enabled: root.enabled; accessible-enabled: root.enabled;
@@ -52,16 +67,18 @@ export component GroupFilter inherits Rectangle {
root.reveal-selection(); root.reveal-selection();
root.selected(root.current-index); root.selected(root.current-index);
} }
function open-menu() { public function open-menu() {
if root.enabled && root.model.length > 0 { if root.enabled && root.model.length > 0 {
popup-open-timer.running = false; popup-open-timer.running = false;
root.popup-animation-enabled = false; root.popup-animation-enabled = false;
root.popup-progress = 0; root.popup-progress = 0;
root.reveal-selection(); root.reveal-selection();
trigger-focus.focus();
popup.show(); popup.show();
popup-open-timer.running = true; popup-open-timer.running = true;
} }
} }
public function close-menu() { popup.close(); }
// Defer until the popup has its initial frame; animate its contents only, // Defer until the popup has its initial frame; animate its contents only,
// keeping the native popup bounds and scrolling geometry stable. // keeping the native popup bounds and scrolling geometry stable.
popup-open-timer := Timer { popup-open-timer := Timer {
@@ -87,14 +104,19 @@ export component GroupFilter inherits Rectangle {
return reject; return reject;
} }
Text { Text {
x: 10px; width: parent.width - 36px; height: parent.height; x: root.embedded ? 8px : 10px;
width: parent.width - (root.show-indicator
? (root.embedded ? 26px : 36px) : (root.embedded ? 16px : 20px));
height: parent.height;
text: root.model[root.current-index]; text: root.model[root.current-index];
color: root.enabled ? #30343b : #92979f; color: root.enabled ? root.text-color : #92979f;
font-size: 12px; overflow: elide; vertical-alignment: center; font-size: root.text-size; font-weight: root.text-weight;
overflow: elide; vertical-alignment: center;
accessible-role: none; accessible-role: none;
} }
Text { if root.show-indicator: Text {
x: parent.width - 25px; width: 16px; height: parent.height; x: parent.width - (root.embedded ? 16px : 25px);
width: root.embedded ? 10px : 16px; height: parent.height;
text: FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free"; text: FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free";
font-weight: 900; font-size: 10px; color: #858c96; font-weight: 900; font-size: 10px; color: #858c96;
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
@@ -107,22 +129,24 @@ export component GroupFilter inherits Rectangle {
key-pressed(event) => { return root.handle-key(event); } key-pressed(event) => { return root.handle-key(event); }
trigger-touch := TouchArea { trigger-touch := TouchArea {
enabled: root.enabled; enabled: root.enabled;
clicked => { trigger-focus.focus(); root.open-menu(); } mouse-cursor: pointer;
clicked => { root.open-menu(); }
} }
} }
popup := PopupWindow { popup := PopupWindow {
x: 0px; y: root.height + 4px; x: 0px; y: root.height + 4px;
width: root.width; height: root.visible-height + 8px; width: max(root.width, root.popup-min-width); height: root.visible-height + 8px;
close-policy: close-on-click-outside; close-policy: close-on-click-outside;
forward-focus: popup-focus; forward-focus: popup-focus;
popup-focus := FocusScope { popup-focus := FocusScope {
key-pressed(event) => { return root.handle-key(event); } key-pressed(event) => { return root.handle-key(event); }
Rectangle { FlatSurface {
y: (1 - root.popup-progress) * -4px; y: (1 - root.popup-progress) * -4px;
width: parent.width; height: parent.height; width: parent.width; height: parent.height;
opacity: root.popup-progress; opacity: root.popup-progress;
background: white; border-width: 1px; background: white;
border-color: root.normal-border-color; border-radius: UiRadius.menu; clip: true;
outline-color: root.normal-outline-color; border-radius: UiRadius.menu;
ScrollView { ScrollView {
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px; x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
viewport-width: self.width; viewport-width: self.width;
@@ -130,27 +154,30 @@ export component GroupFilter inherits Rectangle {
viewport-y <=> root.scroll-y; viewport-y <=> root.scroll-y;
Rectangle { Rectangle {
width: parent.width; height: root.list-height; width: parent.width; height: root.list-height;
if root.has-divider: Rectangle { if root.has-divider: SurfaceSeparator {
x: 6px; y: root.system-count * 26px + 3px; x: 6px; y: root.system-count * 26px + 3px;
width: parent.width - 12px; height: 1px; background: #e9ebee; width: parent.width - 12px; height: 1px; outline-color: UiSurface.separator;
} }
for choice[index] in root.model: Rectangle { for choice[index] in root.model: Rectangle {
y: index * 26px + (root.has-divider && index >= root.system-count ? 6px : 0px); y: index * 26px + (root.has-divider && index >= root.system-count ? 6px : 0px);
width: parent.width; height: 26px; border-radius: UiRadius.control; width: parent.width; height: 26px; border-radius: UiRadius.control;
background: index == root.current-index ? #edf4fc : option-touch.has-hover ? #f1f3f5 : transparent; background: index == root.current-index
? (option-touch.pressed ? ControlStyle.selected-pressed : option-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected)
: option-touch.pressed ? ControlStyle.pressed : option-touch.has-hover ? ControlStyle.hover : transparent;
animate background { duration: 100ms; easing: ease-out; } animate background { duration: 100ms; easing: ease-out; }
accessible-role: list-item; accessible-role: list-item;
accessible-label: choice; accessible-label: choice;
Text { Text {
x: 8px; width: parent.width - 34px; height: parent.height; x: 8px; width: parent.width - 34px; height: parent.height;
text: choice; color: #30343b; font-size: 12px; text: choice; color: index == root.current-index ? ControlStyle.selected-text : #30343b; font-size: 12px;
font-weight: index == root.current-index ? 600 : 400;
overflow: elide; vertical-alignment: center; overflow: elide; vertical-alignment: center;
accessible-role: none; accessible-role: none;
} }
if index == root.current-index: Text { if index == root.current-index: Text {
x: parent.width - 24px; width: 16px; height: parent.height; x: parent.width - 24px; width: 16px; height: parent.height;
text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free"; text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free";
font-weight: 900; font-size: 10px; color: #4e87c0; font-weight: 900; font-size: 10px; color: ControlStyle.selected-text;
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
accessible-role: none; accessible-role: none;
} }
@@ -1,9 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"> <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs> <circle cx="32" cy="32" r="31" fill="white"/>
<radialGradient id="edge">
<stop offset="95%" stop-color="white"/>
<stop offset="100%" stop-color="white" stop-opacity="0"/>
</radialGradient>
</defs>
<circle cx="32" cy="32" r="32" fill="url(#edge)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 139 B

+58
View File
@@ -0,0 +1,58 @@
import { GalleryStrings } from "device_strings.slint";
export global LanStrings {
// Short localized lines let the warning dialog add leading even though
// Slint Text does not expose a line-height property.
public pure function access-error-lines(code: string) -> [string] {
if code == "LanPortInUse" { return [
GalleryStrings.texts[0],
GalleryStrings.texts[1],
GalleryStrings.texts[2]
]; }
if code == "LanPortCheckFailed" { return [
GalleryStrings.texts[3],
GalleryStrings.texts[4],
GalleryStrings.texts[2]
]; }
if code == "LanPortPermissionDenied" { return [
GalleryStrings.texts[5],
GalleryStrings.texts[6],
GalleryStrings.texts[2]
]; }
if code == "Selected LAN interface is unavailable" { return [
GalleryStrings.texts[7],
GalleryStrings.texts[8],
GalleryStrings.texts[9]
]; }
// Keep all branches the same size for Slint's C++ model generator.
return ["", "", ""];
}
public pure function error-message(code: string) -> string {
let lines = LanStrings.access-error-lines(code);
if lines[0] != "" {
let separator = GalleryStrings.texts[10];
return lines[0] + separator + lines[1] + separator + lines[2];
}
if code == "" { return ""; }
if code == "InvalidCredentials" { return GalleryStrings.texts[11]; }
if code == "AuthenticationRateLimited" { return GalleryStrings.texts[12]; }
if code == "CredentialsChanged" { return GalleryStrings.texts[13]; }
if code == "Busy" { return GalleryStrings.texts[14]; }
if code == "LAN password must contain exactly 6 ASCII letters or digits" { return GalleryStrings.texts[15]; }
if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "IceConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" || code == "HeartbeatTimeout" || code == "SignalSendFailed" { return GalleryStrings.texts[16]; }
if code == "Enter a numeric IPv4 address or IPv4:port" || code == "Invalid LAN endpoint" { return GalleryStrings.texts[17]; }
if code == "Invalid LAN port" { return GalleryStrings.texts[18]; }
if code == "LAN password is not ready" { return GalleryStrings.texts[19]; }
if code == "LAN connection limit reached" { return GalleryStrings.texts[20]; }
if code == "UnsupportedNetworkPath" || code == "NetworkPolicyFailed" { return GalleryStrings.texts[21]; }
if code == "IncompatibleProtocol" || code == "RequiredCryptoUnsupported" || code == "TLS 1.3 is required for LAN connections" { return GalleryStrings.texts[22]; }
if code == "InvalidCertificate" || code == "FingerprintMissing" || code == "FingerprintMismatch" { return GalleryStrings.texts[23]; }
if code == "DtlsHandshakeFailed" || code == "SrtpInstallFailed" || code == "SrtpKeyExportFailed" || code == "SecureTransportFailed" { return GalleryStrings.texts[24]; }
if code == "InvalidSignal" || code == "InvalidLocalSignal" || code == "Invalid LAN protocol message" { return GalleryStrings.texts[25]; }
if code == "ListenerChanged" { return GalleryStrings.texts[26]; }
if code == "PeerPreparationFailed" { return GalleryStrings.texts[27]; }
if code == "Cannot copy text" { return GalleryStrings.texts[28]; }
// Keep diagnostic details for unexpected system errors, with a localized explanation.
return GalleryStrings.texts[29] + code;
}
}
File diff suppressed because it is too large Load Diff
+13 -11
View File
@@ -1,6 +1,7 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
import { IconButton, FontAwesomeIcons } from "common.slint"; import { IconButton, FontAwesomeIcons, ControlStyle } from "common.slint";
import { SettingsDialog, SettingsButton } from "settings_controls.slint"; import { SettingsDialog, SettingsButton } from "settings_controls.slint";
import { WorkbenchStyle } from "workbench_controls.slint"; import { WorkbenchStyle } from "workbench_controls.slint";
import "../assets/fonts/message_spacing.ttf"; import "../assets/fonts/message_spacing.ttf";
@@ -90,11 +91,11 @@ export component MessagePanel inherits SettingsDialog {
text: root.notice-text; font-size: 12px; wrap: word-wrap; color: #b3261e; text: root.notice-text; font-size: 12px; wrap: word-wrap; color: #b3261e;
} }
} }
message-content := Rectangle { message-content := FlatSurface {
x: 24px; y: root.content-top + (root.notice-text == "" ? 0px : notice.height + 12px); x: 24px; y: root.content-top + (root.notice-text == "" ? 0px : notice.height + 12px);
width: parent.width - 48px; height: parent.height - self.y - 24px; width: parent.width - 48px; height: parent.height - self.y - 24px;
background: WorkbenchStyle.surface; background: WorkbenchStyle.surface;
border-radius: UiRadius.surface; border-width: 1px; border-color: WorkbenchStyle.border; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
if root.items.length > 0 || root.selected-id != "": Rectangle { if root.items.length > 0 || root.selected-id != "": Rectangle {
x: 1px; y: 1px; width: content-divider.x - 1px; height: parent.height - 2px; x: 1px; y: 1px; width: content-divider.x - 1px; height: parent.height - 2px;
background: WorkbenchStyle.field-surface; background: WorkbenchStyle.field-surface;
@@ -112,19 +113,19 @@ export component MessagePanel inherits SettingsDialog {
announcement-entries := VerticalLayout { announcement-entries := VerticalLayout {
width: announcements-scroll.viewport-width; spacing: 10px; padding: 0px; width: announcements-scroll.viewport-width; spacing: 10px; padding: 0px;
alignment: start; alignment: start;
for item in root.items: Rectangle { for item in root.items: FlatSurface {
height: 64px; height: 64px;
background: root.selected-id == item.id ? #e8efff background: root.selected-id == item.id
? (entry-touch.pressed ? ControlStyle.selected-pressed : entry-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected)
: entry-touch.pressed ? WorkbenchStyle.pressed-surface : entry-touch.pressed ? WorkbenchStyle.pressed-surface
: entry-touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface; : entry-touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface;
border-radius: UiRadius.control; border-radius: UiRadius.control;
border-width: 1px; outline-color: root.selected-id == item.id ? UiSurface.focus : UiSurface.edge;
border-color: root.selected-id == item.id ? #b5c9f5 : WorkbenchStyle.border;
Text { Text {
x: 10px; y: 9px; width: parent.width - 30px; height: 20px; x: 10px; y: 9px; width: parent.width - 30px; height: 20px;
text: item.title; overflow: elide; font-size: 13px; text: item.title; overflow: elide; font-size: 13px;
font-weight: item.read ? 400 : 600; font-weight: item.read ? 400 : 600;
color: root.selected-id == item.id ? WorkbenchStyle.accent : WorkbenchStyle.text; color: root.selected-id == item.id ? ControlStyle.selected-text : WorkbenchStyle.text;
vertical-alignment: center; vertical-alignment: center;
} }
if !item.read: Rectangle { if !item.read: Rectangle {
@@ -154,10 +155,10 @@ export component MessagePanel inherits SettingsDialog {
color: WorkbenchStyle.muted; font-size: 13px; color: WorkbenchStyle.muted; font-size: 13px;
horizontal-alignment: center; vertical-alignment: center; horizontal-alignment: center; vertical-alignment: center;
} }
content-divider := Rectangle { content-divider := SurfaceSeparator {
x: announcement-list.x + announcement-list.width + 8px; x: announcement-list.x + announcement-list.width + 8px;
y: 1px; width: 1px; height: parent.height - 2px; y: 1px; width: 1px; height: parent.height - 2px;
background: WorkbenchStyle.separator; vertical: true; outline-color: UiSurface.separator;
visible: root.items.length > 0 || root.selected-id != ""; visible: root.items.length > 0 || root.selected-id != "";
accessible-role: none; accessible-role: none;
} }
@@ -199,13 +200,14 @@ export component MessagePanel inherits SettingsDialog {
x: parent.width - self.width; y: 0px; width: 28px; height: 28px; x: parent.width - self.width; y: 0px; width: 28px; height: 28px;
icon: FontAwesomeIcons.trash-can; icon-size: 12px; icon: FontAwesomeIcons.trash-can; icon-size: 12px;
icon-color: WorkbenchStyle.muted; danger: true; icon-color: WorkbenchStyle.muted; danger: true;
normal-background: ControlStyle.surface;
accessible-role: button; accessible-role: button;
accessible-label: MessageStrings.delete-message + " " + root.selected-title; accessible-label: MessageStrings.delete-message + " " + root.selected-title;
accessible-action-default => { root.request-delete(root.selected-id, root.selected-revision); } accessible-action-default => { root.request-delete(root.selected-id, root.selected-revision); }
clicked => { root.request-delete(root.selected-id, root.selected-revision); } clicked => { root.request-delete(root.selected-id, root.selected-revision); }
} }
} }
Rectangle { height: 1px; background: WorkbenchStyle.separator; } SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; }
StyledText { StyledText {
vertical-stretch: 0; vertical-stretch: 0;
text: root.selected-body; text: root.selected-body;
@@ -1,15 +1,17 @@
import { FlatSurface, UiSurface } from "ui_surface.slint";
import { LineEdit } from "std-widgets.slint"; import { LineEdit } from "std-widgets.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ControlStyle } from "common.slint";
// Retain the standard editor's selection, clipboard, clear button and input // Retain the standard editor's selection, clipboard, clear button and input
// method support. Only its outer frame is replaced by the shared shape. // method support. Its frame receives the shared field tint and rounded shape.
export component RoundedLineEdit inherits Rectangle { export component RoundedLineEdit inherits Rectangle {
in-out property <string> text; in-out property <string> text;
in property <string> placeholder-text; in property <string> placeholder-text;
in property <InputType> input-type: InputType.text; in property <InputType> input-type: InputType.text;
in property <bool> enabled: true; in property <bool> enabled: true;
in property <length> font-size: 12px; in property <length> font-size: 12px;
in property <color> normal-border-color: #dfe6ef; in property <color> normal-outline-color: UiSurface.edge;
in property <brush> surrounding-background: white; in property <brush> surrounding-background: white;
callback accepted(); callback accepted();
callback edited(); callback edited();
@@ -20,8 +22,8 @@ export component RoundedLineEdit inherits Rectangle {
forward-focus: editor; forward-focus: editor;
editor := LineEdit { editor := LineEdit {
x: -1px; y: -1px; x: -2px; y: -2px;
width: parent.width + 2px; height: parent.height + 2px; width: parent.width + 4px; height: parent.height + 4px;
text <=> root.text; text <=> root.text;
placeholder-text: root.placeholder-text; placeholder-text: root.placeholder-text;
input-type: root.input-type; input-type: root.input-type;
@@ -30,8 +32,15 @@ export component RoundedLineEdit inherits Rectangle {
accepted => { root.accepted(); } accepted => { root.accepted(); }
edited => { root.edited(); } edited => { root.edited(); }
} }
// Fluent's LineEdit paints its own background and exposes no fill override.
// A visual-only tint preserves the standard editor and its built-in actions.
Rectangle {
width: parent.width; height: parent.height;
background: !root.enabled ? transparent
: editor.has-focus ? ControlStyle.input-focus-tint : ControlStyle.input-tint;
}
// The software renderer clips children to rectangular bounds. Mask the // The software renderer clips children to rectangular bounds. Mask the
// standard editor's corners before drawing our rounded border on top. // standard editor's corners before drawing the focus outline on top.
Rectangle { Rectangle {
x: -root.border-radius; y: -root.border-radius; x: -root.border-radius; y: -root.border-radius;
width: parent.width + 2 * root.border-radius; width: parent.width + 2 * root.border-radius;
@@ -40,11 +49,12 @@ export component RoundedLineEdit inherits Rectangle {
border-radius: 2 * root.border-radius; border-radius: 2 * root.border-radius;
border-color: root.surrounding-background; border-color: root.surrounding-background;
} }
Rectangle { FlatSurface {
width: parent.width; height: parent.height; width: parent.width; height: parent.height;
border-radius: root.border-radius; border-radius: root.border-radius;
border-width: editor.has-focus ? 2px : 1px; clip: true;
border-color: editor.has-focus ? #3564e3 : root.normal-border-color; outline-color: editor.has-focus ? UiSurface.focus : root.normal-outline-color;
outline-focused: editor.has-focus;
background: transparent; background: transparent;
} }
} }
+17 -13
View File
@@ -1,6 +1,7 @@
import { FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint"; import { ScrollView } from "std-widgets.slint";
import { FontAwesomeIcons, ImGuiLineStyle, FloatingBarStyle } from "common.slint"; import { FontAwesomeIcons, FloatingBarStyle, ButtonStyle } from "common.slint";
export struct ControllerEntry { export struct ControllerEntry {
remote-id: string, remote-id: string,
@@ -19,7 +20,7 @@ export global ServerStrings {
in property <string> cancel: "取消"; in property <string> cancel: "取消";
} }
component SessionButton inherits Rectangle { component SessionButton inherits FlatSurface {
in property <string> text; in property <string> text;
in property <bool> danger: false; in property <bool> danger: false;
in property <bool> enabled: true; in property <bool> enabled: true;
@@ -29,10 +30,13 @@ component SessionButton inherits Rectangle {
width: label.preferred-width + 20px; width: label.preferred-width + 20px;
height: 28px; height: 28px;
border-radius: UiRadius.control; border-radius: UiRadius.control;
border-width: 1px; outline-enabled: !root.danger;
border-color: root.danger ? #d92d20 : #c9ced7; outline-color: root.pointer-feedback && touch.pressed ? ButtonStyle.pressed-border
: root.pointer-feedback && touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border;
background: root.danger ? (root.pointer-feedback && touch.pressed ? #b42318 : root.pointer-feedback && touch.has-hover ? #f04438 : #d92d20) background: root.danger ? (root.pointer-feedback && touch.pressed ? #b42318 : root.pointer-feedback && touch.has-hover ? #f04438 : #d92d20)
: (root.pointer-feedback && touch.pressed ? #dfe3e9 : root.pointer-feedback && touch.has-hover ? #f3f4f6 : white); : (root.pointer-feedback && touch.pressed ? ButtonStyle.pressed
: root.pointer-feedback && touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface);
animate background, outline-color { duration: 120ms; }
opacity: root.enabled ? 1 : 0.5; opacity: root.enabled ? 1 : 0.5;
accessible-role: button; accessible-role: button;
accessible-label: root.text; accessible-label: root.text;
@@ -72,9 +76,9 @@ component SessionButton inherits Rectangle {
// Restore hover feedback only after a fresh pointer move or press. // Restore hover feedback only after a fresh pointer move or press.
clicked => { root.pointer-feedback = false; root.keyboard-focus = false; keyboard.focus(); root.clicked(); } clicked => { root.pointer-feedback = false; root.keyboard-focus = false; keyboard.focus(); root.clicked(); }
} }
if root.enabled && keyboard.has-focus && root.keyboard-focus: Rectangle { if root.enabled && keyboard.has-focus && root.keyboard-focus: FlatSurface {
x: 2px; y: 2px; width: parent.width - 4px; height: parent.height - 4px; x: 2px; y: 2px; width: parent.width - 4px; height: parent.height - 4px;
border-radius: UiRadius.control - 2px; border-width: 1px; border-color: root.danger ? white : #2463c7; border-radius: UiRadius.control - 2px; outline-color: root.danger ? white : UiSurface.focus;
} }
} }
@@ -107,10 +111,10 @@ export component ServerStatusBar inherits Window {
return reject; return reject;
} }
} }
Rectangle { FlatSurface {
x: 0px; y: 0px; width: parent.width; height: parent.height; x: 0px; y: 0px; width: parent.width; height: parent.height;
background: touch.pressed ? #dfe3e9 : touch.has-hover ? #f3f4f6 : white; background: touch.pressed ? #dfe3e9 : touch.has-hover ? #f3f4f6 : white;
border-width: 1px; border-color: ImGuiLineStyle.border; outline-color: UiSurface.edge;
border-radius: FloatingBarStyle.corner-radius; border-radius: FloatingBarStyle.corner-radius;
border-top-right-radius: 0px; border-top-right-radius: 0px;
border-bottom-right-radius: 0px; border-bottom-right-radius: 0px;
@@ -170,9 +174,9 @@ export component ServerWindow inherits Window {
if event.text == Key.Escape { root.dismiss-panel(); return accept; } if event.text == Key.Escape { root.dismiss-panel(); return accept; }
return reject; return reject;
} }
Rectangle { FlatSurface {
width: root.width; height: root.height; width: root.width; height: root.height;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; background: white; outline-color: UiSurface.edge;
border-radius: UiRadius.surface; border-radius: UiRadius.surface;
Text { Text {
x: 10px; y: 10px; width: parent.width - 20px; height: 24px; x: 10px; y: 10px; width: parent.width - 20px; height: 24px;
@@ -192,9 +196,9 @@ export component ServerWindow inherits Window {
width: parent.width - (root.controllers.length > 1 ? disconnect.width + 4px : status.preferred-width + 8px); width: parent.width - (root.controllers.length > 1 ? disconnect.width + 4px : status.preferred-width + 8px);
height: parent.height; border-radius: UiRadius.control; height: parent.height; border-radius: UiRadius.control;
background: root.controllers.length > 1 && index == root.selected-controller ? #eef5fd : select-touch.has-hover ? #f3f4f6 : transparent; background: root.controllers.length > 1 && index == root.selected-controller ? #eef5fd : select-touch.has-hover ? #f3f4f6 : transparent;
if root.controllers.length > 1: Rectangle { if root.controllers.length > 1: FlatSurface {
x: 4px; y: (parent.height - 13px) / 2; width: 13px; height: 13px; x: 4px; y: (parent.height - 13px) / 2; width: 13px; height: 13px;
border-radius: 7px; border-width: 1px; border-color: index == root.selected-controller ? #2463c7 : #8c939e; border-radius: 7px; outline-color: index == root.selected-controller ? UiSurface.focus : UiSurface.edge;
if index == root.selected-controller: Rectangle { x: 3px; y: 3px; width: 7px; height: 7px; border-radius: 4px; background: #2463c7; } if index == root.selected-controller: Rectangle { x: 3px; y: 3px; width: 7px; height: 7px; border-radius: 4px; background: #2463c7; }
} }
name := Text { name := Text {
+22 -18
View File
@@ -1,26 +1,32 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons } from "common.slint"; import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons, ControlStyle, ButtonStyle } from "common.slint";
import { WorkbenchStyle } from "workbench_controls.slint"; import { WorkbenchStyle } from "workbench_controls.slint";
// Shared visual treatment for the global settings pages. // Shared visual treatment for the global settings pages.
export component SettingsButton inherits Rectangle { export component SettingsButton inherits FlatSurface {
in property <string> text; in property <string> text;
in property <image> icon; in property <image> icon;
in property <bool> primary: false; in property <bool> primary: false;
in property <bool> quiet: false; in property <bool> quiet: false;
in property <bool> align-left: false; in property <bool> align-left: false;
in property <bool> enabled: true; in property <bool> enabled: true;
out property <length> label-width: button-label.preferred-width + 2 * root.content-padding + (root.has-icon ? 24px : 0px);
private property <bool> has-icon: root.icon.width > 0; private property <bool> has-icon: root.icon.width > 0;
private property <length> content-padding: root.has-icon ? 8px : 12px; private property <length> content-padding: root.has-icon ? 8px : 12px;
callback clicked(); callback clicked();
width: 88px; height: 32px; border-radius: UiRadius.control; width: 88px; height: 32px; border-radius: UiRadius.control;
background: !root.enabled ? #f0f3f7 background: !root.enabled ? ControlStyle.disabled
: root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc) : root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc)
: touch.pressed ? #e9eef5 : touch.has-hover ? #f0f4fa : root.quiet ? transparent : white; : touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover
border-width: root.quiet || root.primary ? 0px : 1px; : root.quiet ? transparent : ButtonStyle.surface;
border-color: !root.enabled ? WorkbenchStyle.border outline-enabled: !root.quiet && !root.primary;
: touch.has-hover ? WorkbenchStyle.hover-border : WorkbenchStyle.control-border; outline-color: !root.enabled ? ButtonStyle.disabled-border
: touch.pressed ? ButtonStyle.pressed-border
: touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border;
animate background, outline-color { duration: 120ms; }
accessible-role: button; accessible-label: root.text; accessible-role: button; accessible-label: root.text;
accessible-enabled: root.enabled;
accessible-action-default => { if root.enabled { root.clicked(); } } accessible-action-default => { if root.enabled { root.clicked(); } }
Rectangle { Rectangle {
x: root.align-left ? root.content-padding : (parent.width - self.width) / 2; x: root.align-left ? root.content-padding : (parent.width - self.width) / 2;
@@ -59,10 +65,7 @@ export component SettingsDialog inherits DialogSurface {
callback action(); callback action();
callback close(); callback close();
background: #f8fafc; background: #f8fafc;
border-color: WorkbenchStyle.separator; outline-color: UiSurface.edge;
drop-shadow-color: #14294424;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 8px;
TouchArea { } TouchArea { }
HorizontalLayout { HorizontalLayout {
x: 24px; y: 18px; width: parent.width - 48px; height: 28px; x: 24px; y: 18px; width: parent.width - 48px; height: 28px;
@@ -97,6 +100,7 @@ export component SettingsDialog inherits DialogSurface {
width: 28px; height: 28px; width: 28px; height: 28px;
icon: FontAwesomeIcons.xmark; icon-size: 14px; icon: FontAwesomeIcons.xmark; icon-size: 14px;
icon-color: WorkbenchStyle.muted; icon-color: WorkbenchStyle.muted;
normal-background: ControlStyle.surface;
hover-background: WorkbenchStyle.hover-surface; hover-background: WorkbenchStyle.hover-surface;
pressed-background: WorkbenchStyle.pressed-surface; pressed-background: WorkbenchStyle.pressed-surface;
accessible-role: button; accessible-label: root.close-label; accessible-role: button; accessible-label: root.close-label;
@@ -109,9 +113,9 @@ export component SettingsDialog inherits DialogSurface {
export component SettingsSelect inherits CompactComboBox { export component SettingsSelect inherits CompactComboBox {
subtle: true; subtle: true;
show-arrow: false; show-arrow: true;
normal-border-color: WorkbenchStyle.control-border; normal-outline-color: UiSurface.edge;
hover-border-color: WorkbenchStyle.hover-border; hover-outline-color: UiSurface.hover;
height: 28px; height: 28px;
} }
@@ -123,9 +127,9 @@ export component SettingsGroup inherits Rectangle {
x: 2px; y: 0px; width: parent.width - 4px; height: 18px; text: root.title; x: 2px; y: 0px; width: parent.width - 4px; height: 18px; text: root.title;
font-size: 11px; font-weight: 600; color: #718096; vertical-alignment: center; font-size: 11px; font-weight: 600; color: #718096; vertical-alignment: center;
} }
Rectangle { FlatSurface {
y: 24px; width: parent.width; height: parent.height - self.y; y: 24px; width: parent.width; height: parent.height - self.y;
background: white; border-radius: UiRadius.surface; border-color: WorkbenchStyle.border; border-width: 1px; background: white; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
VerticalLayout { padding: 0px; spacing: 0px; @children } VerticalLayout { padding: 0px; spacing: 0px; @children }
} }
} }
@@ -141,8 +145,8 @@ export component SettingsRow inherits Rectangle {
text: root.label; color: root.enabled ? #34445a : #9ba5b3; text: root.label; color: root.enabled ? #34445a : #9ba5b3;
font-size: 12px; overflow: elide; vertical-alignment: center; font-size: 12px; overflow: elide; vertical-alignment: center;
} }
if root.divider: Rectangle { if root.divider: SurfaceSeparator {
x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #d9e1ec; x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; outline-color: UiSurface.separator;
} }
@children @children
} }
+154 -96
View File
@@ -1,7 +1,8 @@
import { SurfaceSeparator, FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ComboBox, ScrollView } from "std-widgets.slint"; import { ComboBox, ScrollView } from "std-widgets.slint";
import { RemoteCursor } from "remote_cursor.slint"; import { RemoteCursor } from "remote_cursor.slint";
import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint"; import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiFontStyle, ControlStyle, ButtonStyle, ScrollingText } from "common.slint";
export struct StreamTab { export struct StreamTab {
remote-id: string, remote-id: string,
@@ -25,6 +26,10 @@ export struct NetworkStatsRow {
} }
export global StreamStrings { export global StreamStrings {
in-out property <string> window-minimize: "Minimize";
in-out property <string> window-maximize: "Maximize";
in-out property <string> window-restore: "Restore window";
in-out property <string> window-close: "Close window";
in-out property <string> remote-update-title: "Remote update recommended"; in-out property <string> remote-update-title: "Remote update recommended";
in-out property <string> remote-update: "Update the remote device to the latest version for improved features and performance."; in-out property <string> remote-update: "Update the remote device to the latest version for improved features and performance.";
in-out property <string> remote-update-dismiss: "Got it"; in-out property <string> remote-update-dismiss: "Got it";
@@ -42,15 +47,19 @@ export global StreamStrings {
in-out property <string> select-display: "Select Display"; in-out property <string> select-display: "Select Display";
in-out property <string> send-shortcut: "Send Shortcut"; in-out property <string> send-shortcut: "Send Shortcut";
in-out property <string> privacy-unlocked-title: "Privacy screen unlocked";
in-out property <string> privacy-unlocked-message: "The remote privacy screen was unlocked using the local shortcut. Lock it again to restore privacy and disable local shortcut unlocking.";
in-out property <string> privacy-unlock-confirm: "Confirm";
in-out property <string> privacy-unlock-lock: "Lock";
in-out property <string> privacy-enable: "Enable privacy screen"; in-out property <string> privacy-enable: "Enable privacy screen";
in-out property <string> privacy-disable: "Turn privacy off / recover"; in-out property <string> privacy-disable: "Turn privacy off / recover";
in-out property <string> control-mouse: "Control Mouse"; in-out property <string> control-mouse: "Control Mouse";
in-out property <string> release-mouse: "Release Mouse"; in-out property <string> release-mouse: "Release Mouse";
in-out property <string> audio: "Audio Capture"; in-out property <string> audio: "Play audio";
in-out property <string> mute: "Mute"; in-out property <string> mute: "Mute";
in-out property <string> select-file: "Select File to Send"; in-out property <string> select-file: "Select File to Send";
in-out property <string> show-stats: "Show Net Traffic Stats"; in-out property <string> show-stats: "Show network status";
in-out property <string> hide-stats: "Hide Net Traffic Stats"; in-out property <string> hide-stats: "Hide network status";
in-out property <string> fullscreen: "Fullscreen"; in-out property <string> fullscreen: "Fullscreen";
in-out property <string> exit-fullscreen: "Exit fullscreen"; in-out property <string> exit-fullscreen: "Exit fullscreen";
in-out property <string> disconnect: "Disconnect"; in-out property <string> disconnect: "Disconnect";
@@ -64,7 +73,7 @@ export global StreamStrings {
in-out property <string> stats-out: "Out"; in-out property <string> stats-out: "Out";
in-out property <string> stats-loss-rate: "Loss Rate"; in-out property <string> stats-loss-rate: "Loss Rate";
in-out property <string> stats-resolution: "Res"; in-out property <string> stats-resolution: "Res";
in-out property <string> stats-video-latency: "Video delay"; in-out property <string> stats-video-latency: "Video latency";
in-out property <string> stats-connection-latency: "Connection RTT"; in-out property <string> stats-connection-latency: "Connection RTT";
in-out property <string> stats-connection-mode: "Mode"; in-out property <string> stats-connection-mode: "Mode";
in-out property <string> stats-encoder: "Remote encoder"; in-out property <string> stats-encoder: "Remote encoder";
@@ -80,14 +89,10 @@ struct ControlBarTooltip {
} }
// Shared surface treatment for the capsule and its detached detail cards. // Shared surface treatment for the capsule and its detached detail cards.
component StreamControlSurface inherits Rectangle { component StreamControlSurface inherits FlatSurface {
background: white; background: white;
border-width: 1px; outline-color: UiSurface.edge;
border-color: #dbe5f1;
border-radius: 16px; border-radius: 16px;
drop-shadow-color: #1d35571a;
drop-shadow-blur: 10px;
drop-shadow-offset-y: 2px;
} }
// Keep the compact 16px icons inside roomier circular targets. This remains // Keep the compact 16px icons inside roomier circular targets. This remains
@@ -97,6 +102,7 @@ component ControlBarButton inherits Rectangle {
in property <bool> show-logo: false; in property <bool> show-logo: false;
in property <bool> logo-pressed: false; in property <bool> logo-pressed: false;
in property <bool> logo-dragging: false; in property <bool> logo-dragging: false;
in property <bool> hand-cursor-supported: true;
in property <float> reveal-progress: 1; in property <float> reveal-progress: 1;
in property <string> badge: ""; in property <string> badge: "";
in property <string> tooltip: ""; in property <string> tooltip: "";
@@ -121,14 +127,14 @@ component ControlBarButton inherits Rectangle {
!root.show-logo && root.action-enabled && !root.muted && touch.pressed; !root.show-logo && root.action-enabled && !root.muted && touch.pressed;
// Share the fill with the slash cutout so every icon stays legible while // Share the fill with the slash cutout so every icon stays legible while
// transitioning between idle, hover and pressed states. // transitioning between idle, hover and pressed states.
private property <color> button-fill: root.muted ? #eef0f3 private property <color> button-fill: root.muted ? ControlStyle.disabled
: root.button-pressed ? (root.danger ? #7a271a : #65a5f2) : root.button-pressed ? (root.danger ? #7a271a : root.selected ? ControlStyle.selected-pressed : ControlStyle.pressed)
: root.button-hovered ? (root.danger ? (root.selected ? #912018 : #b42318) : root.button-hovered ? (root.danger ? (root.selected ? #912018 : #b42318)
: root.selected ? #86baff : #a7ceff) : root.selected ? ControlStyle.selected-hover : ControlStyle.hover)
: root.danger ? (root.selected ? #b42318 : #d92d20) : root.danger ? (root.selected ? #b42318 : #d92d20)
: root.selected ? #b3d5fd : #deecff; : root.selected ? ControlStyle.selected : ControlStyle.surface;
private property <color> button-foreground: root.muted ? #8a8f96 private property <color> button-foreground: root.muted ? #8a8f96
: root.danger ? white : #24272d; : root.danger ? white : root.selected ? ControlStyle.selected-text : #303f55;
animate button-fill, button-foreground { animate button-fill, button-foreground {
duration: 90ms; duration: 90ms;
easing: ease-out; easing: ease-out;
@@ -222,7 +228,7 @@ component ControlBarButton inherits Rectangle {
} }
touch := TouchArea { touch := TouchArea {
mouse-cursor: !root.action-enabled ? default : root.show-logo ? (root.logo-pressed ? grabbing : grab) : pointer; mouse-cursor: root.action-enabled && root.hand-cursor-supported ? pointer : default;
scroll-event(event) => { accept } scroll-event(event) => { accept }
pointer-event(event) => { pointer-event(event) => {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y); root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
@@ -242,14 +248,21 @@ component VideoSettingRow inherits VerticalLayout {
Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; } Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; }
HorizontalLayout { HorizontalLayout {
spacing: 4px; spacing: 4px;
for option[index] in root.options: Rectangle { for option[index] in root.options: FlatSurface {
height: 27px; height: 27px;
background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff background: !root.enabled ? ControlStyle.disabled
: option-touch.has-hover ? #eaf3ff : #f5f6f8; : root.selected == index ? (option-touch.pressed ? ButtonStyle.selected-pressed : option-touch.has-hover ? ButtonStyle.selected-hover : ButtonStyle.selected)
: option-touch.pressed ? ButtonStyle.pressed
: option-touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface;
border-radius: UiRadius.control; border-radius: UiRadius.control;
border-width: 1px; outline-color: !root.enabled ? ButtonStyle.disabled-border
border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6; : root.selected == index ? ButtonStyle.selected-pressed
Text { text: option; color: root.enabled ? #30343b : #8b8f95; : option-touch.pressed ? ButtonStyle.pressed-border
: option-touch.has-hover ? ButtonStyle.hover-border : ButtonStyle.border;
animate background, outline-color { duration: 120ms; }
Text { text: option; color: !root.enabled ? #8b8f95
: root.selected == index ? white : #30343b;
font-weight: root.selected == index ? 600 : 400;
font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } font-size: 11px; horizontal-alignment: center; vertical-alignment: center; }
option-touch := TouchArea { option-touch := TouchArea {
mouse-cursor: root.enabled ? pointer : default; mouse-cursor: root.enabled ? pointer : default;
@@ -273,6 +286,7 @@ export component StreamWindow inherits Window {
default-font-size: ImGuiFontStyle.base; default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false; in property <bool> custom-titlebar: false;
in property <bool> control-hand-cursor-supported: true;
in property <bool> native-video-enabled: false; in property <bool> native-video-enabled: false;
// Export the actual input surface in logical pixels. Native rendering and // Export the actual input surface in logical pixels. Native rendering and
// input mapping must follow the same layout, including title and tab bars. // input mapping must follow the same layout, including title and tab bars.
@@ -288,10 +302,16 @@ export component StreamWindow inherits Window {
in property <string> status-text: ""; in property <string> status-text: "";
in property <string> receiving-text: ""; in property <string> receiving-text: "";
in property <bool> remote-update-visible: false; in property <bool> remote-update-visible: false;
in property <bool> privacy-unlock-visible: false;
in property <bool> privacy-can-lock: false;
out property <bool> modal-visible: root.remote-update-visible || root.privacy-unlock-visible;
callback dismiss-privacy-unlock;
callback lock-privacy;
callback dismiss-remote-update; callback dismiss-remote-update;
in property <[string]> displays; in property <[string]> displays;
in-out property <int> selected-display: 0; in-out property <int> selected-display: 0;
in property <bool> mouse-control-enabled: true; in property <bool> mouse-control-enabled: true;
in property <bool> privacy-visible: true;
in property <bool> privacy-can-toggle: false; in property <bool> privacy-can-toggle: false;
in property <bool> privacy-active: false; in property <bool> privacy-active: false;
in property <bool> privacy-off: false; in property <bool> privacy-off: false;
@@ -326,10 +346,8 @@ export component StreamWindow inherits Window {
callback reorder-tab(int, length, length); callback reorder-tab(int, length, length);
callback close-tab(string); callback close-tab(string);
callback switch-display(int); callback switch-display(int);
in property <bool> secure-attention-supported: true; in property <bool> secure-attention-supported: false;
private property <[string]> secure-shortcut-items: ["Ctrl+Alt+Del", "Win+L"]; private property <[string]> shortcut-items: ["Ctrl+Alt+Del", "Win+L"];
private property <[string]> ordinary-shortcut-items: ["Win+L"];
private property <[string]> shortcut-items: root.secure-attention-supported ? root.secure-shortcut-items : root.ordinary-shortcut-items;
private property <length> shortcut-menu-height: root.shortcut-items.length * 30px + 16px; private property <length> shortcut-menu-height: root.shortcut-items.length * 30px + 16px;
callback send-shortcut(string); callback send-shortcut(string);
callback toggle-mouse-control; callback toggle-mouse-control;
@@ -355,6 +373,8 @@ export component StreamWindow inherits Window {
private property <bool> control-visual-expanded: false; private property <bool> control-visual-expanded: false;
private property <length> control-toolbar-height: 38px; private property <length> control-toolbar-height: 38px;
private property <length> control-button-step: 32px; private property <length> control-button-step: 32px;
private property <length> control-shortcut-width: root.secure-attention-supported ? root.control-button-step : 0px;
private property <length> control-privacy-width: root.privacy-visible ? root.control-button-step : 0px;
private property <length> control-disconnect-gap: 8px; private property <length> control-disconnect-gap: 8px;
private property <length> control-buttons-x: root.control-docked-left ? 43px : 7px; private property <length> control-buttons-x: root.control-docked-left ? 43px : 7px;
private property <length> control-panel-gap: 8px; private property <length> control-panel-gap: 8px;
@@ -406,6 +426,9 @@ export component StreamWindow inherits Window {
root.close-control-menus(); root.close-control-menus();
root.cancel-remote-pointer(); root.cancel-remote-pointer();
} }
changed secure-attention-supported => {
if !root.secure-attention-supported { root.shortcut-menu-open = false; }
}
changed control-expanded => { changed control-expanded => {
control-reveal-timer.running = false; control-reveal-timer.running = false;
control-collapse-timer.running = false; control-collapse-timer.running = false;
@@ -479,8 +502,8 @@ export component StreamWindow inherits Window {
changed mouse-control-enabled => { changed mouse-control-enabled => {
if !root.mouse-control-enabled { root.cancel-remote-pointer(); } if !root.mouse-control-enabled { root.cancel-remote-pointer(); }
} }
changed remote-update-visible => { changed modal-visible => {
if root.remote-update-visible { if root.modal-visible {
root.close-control-menus(); root.close-control-menus();
root.control-drag-pending = false; root.control-drag-pending = false;
root.end-control-drag(); root.end-control-drag();
@@ -490,7 +513,7 @@ export component StreamWindow inherits Window {
// adapter and aborts the process; defer focus to the event loop. // adapter and aborts the process; defer focus to the event loop.
input-focus-timer.running = true; input-focus-timer.running = true;
input-focus-timer.restart(); input-focus-timer.restart();
root.keyboard-focus-changed(!root.remote-update-visible); root.keyboard-focus-changed(!root.modal-visible);
} }
input-focus-timer := Timer { input-focus-timer := Timer {
interval: 1ms; interval: 1ms;
@@ -507,9 +530,9 @@ export component StreamWindow inherits Window {
if logo-button.has-hover { return logo-button.tooltip-info; } if logo-button.has-hover { return logo-button.tooltip-info; }
if root.control-expanded && root.control-content-visible { if root.control-expanded && root.control-content-visible {
if display-button.has-hover { return display-button.tooltip-info; } if display-button.has-hover { return display-button.tooltip-info; }
if shortcut-button.has-hover { return shortcut-button.tooltip-info; } if root.secure-attention-supported && shortcut-button.has-hover { return shortcut-button.tooltip-info; }
if mouse-button.has-hover { return mouse-button.tooltip-info; } if mouse-button.has-hover { return mouse-button.tooltip-info; }
if privacy-button.has-hover { return privacy-button.tooltip-info; } if root.privacy-visible && privacy-button.has-hover { return privacy-button.tooltip-info; }
if video-button.has-hover { return video-button.tooltip-info; } if video-button.has-hover { return video-button.tooltip-info; }
if audio-button.has-hover { return audio-button.tooltip-info; } if audio-button.has-hover { return audio-button.tooltip-info; }
if file-button.has-hover { return file-button.tooltip-info; } if file-button.has-hover { return file-button.tooltip-info; }
@@ -605,7 +628,7 @@ export component StreamWindow inherits Window {
root.set-remote-button(button, false); root.set-remote-button(button, false);
root.pointer-input(button, kind, x, y); root.pointer-input(button, kind, x, y);
} }
} else if root.window-active && root.mouse-control-enabled && !root.remote-update-visible { } else if root.window-active && root.mouse-control-enabled && !root.modal-visible {
let remote-target = root.remote-pointer-active let remote-target = root.remote-pointer-active
|| (!root.control-menu-open && !root.is-local-control-area(x, y)); || (!root.control-menu-open && !root.is-local-control-area(x, y));
if kind == PointerEventKind.down && remote-target if kind == PointerEventKind.down && remote-target
@@ -641,7 +664,7 @@ export component StreamWindow inherits Window {
// so a captured drag can never also toggle the toolbar. // so a captured drag can never also toggle the toolbar.
let toggle-expanded = kind == PointerEventKind.up && root.control-drag-pending let toggle-expanded = kind == PointerEventKind.up && root.control-drag-pending
&& !root.control-dragging && !root.control-dragging
&& root.window-active && !root.remote-update-visible && root.window-active && !root.modal-visible
&& x >= control.x + logo-button.x && x < control.x + logo-button.x + logo-button.width && x >= control.x + logo-button.x && x < control.x + logo-button.x + logo-button.width
&& y >= control.y + logo-button.y && y < control.y + logo-button.y + logo-button.height; && y >= control.y + logo-button.y && y < control.y + logo-button.y + logo-button.height;
root.control-drag-pending = false; root.control-drag-pending = false;
@@ -690,7 +713,7 @@ export component StreamWindow inherits Window {
} }
is-local-control-area(pointer-x, pointer-y) => { is-local-control-area(pointer-x, pointer-y) => {
if root.remote-update-visible { return true; } if root.modal-visible { return true; }
let over-control = pointer-x >= root.control-surface-x let over-control = pointer-x >= root.control-surface-x
&& pointer-x <= root.control-surface-x + root.control-surface-width && pointer-x <= root.control-surface-x + root.control-surface-width
&& pointer-y >= control.y && pointer-y >= control.y
@@ -705,7 +728,7 @@ export component StreamWindow inherits Window {
&& pointer-y >= root.display-menu-y && pointer-y >= root.display-menu-y
&& pointer-y <= root.display-menu-y + root.display-menu-height; && pointer-y <= root.display-menu-y + root.display-menu-height;
let shortcut-menu-x = control.x + shortcut-button.x; let shortcut-menu-x = control.x + shortcut-button.x;
let over-shortcut-menu = root.shortcut-menu-open let over-shortcut-menu = root.secure-attention-supported && root.shortcut-menu-open
&& pointer-x >= shortcut-menu-x && pointer-x >= shortcut-menu-x
&& pointer-x <= shortcut-menu-x + 180px && pointer-x <= shortcut-menu-x + 180px
&& pointer-y >= root.shortcut-menu-y && pointer-y >= root.shortcut-menu-y
@@ -806,7 +829,7 @@ export component StreamWindow inherits Window {
: root.window-active ? #e6e6e6 : root.window-active ? #e6e6e6
: transparent; : transparent;
accessible-role: button; accessible-role: button;
accessible-label: "Minimize"; accessible-label: StreamStrings.window-minimize;
accessible-action-default => { stream-minimize-touch.clicked(); } accessible-action-default => { stream-minimize-touch.clicked(); }
Rectangle { Rectangle {
x: 8px; x: 8px;
@@ -832,7 +855,7 @@ export component StreamWindow inherits Window {
: root.window-active ? #e6e6e6 : root.window-active ? #e6e6e6
: transparent; : transparent;
accessible-role: button; accessible-role: button;
accessible-label: root.window-maximized ? "Restore" : "Maximize"; accessible-label: root.window-maximized ? StreamStrings.window-restore : StreamStrings.window-maximize;
accessible-action-default => { stream-maximize-touch.clicked(); } accessible-action-default => { stream-maximize-touch.clicked(); }
if !root.window-maximized: Rectangle { if !root.window-maximized: Rectangle {
x: 8px; x: 8px;
@@ -882,7 +905,7 @@ export component StreamWindow inherits Window {
: root.window-active ? #e6e6e6 : root.window-active ? #e6e6e6
: transparent; : transparent;
accessible-role: button; accessible-role: button;
accessible-label: "Close"; accessible-label: StreamStrings.window-close;
accessible-action-default => { stream-close-touch.clicked(); } accessible-action-default => { stream-close-touch.clicked(); }
Path { Path {
width: parent.width; width: parent.width;
@@ -899,10 +922,10 @@ export component StreamWindow inherits Window {
clicked => { root.close-stream-window(); } clicked => { root.close-stream-window(); }
} }
} }
Rectangle { SurfaceSeparator {
y: parent.height - 1px; y: parent.height - 1px;
height: 1px; height: 1px;
background: #d5d3d0; outline-color: UiSurface.separator;
} }
} }
@@ -922,11 +945,11 @@ export component StreamWindow inherits Window {
z: 10; z: 10;
HorizontalLayout { HorizontalLayout {
spacing: 1px; spacing: 1px;
for tab[index] in root.tabs: tab-item := Rectangle { for tab[index] in root.tabs: tab-item := FlatSurface {
width: min(190px, max(110px, (parent.width - 12px) / root.tabs.length)); width: min(190px, max(110px, (parent.width - 12px) / root.tabs.length));
background: index == root.selected-tab ? white : tab-touch.has-hover || root.dragging-tab == index ? #e5e7eb : #d8dbe0; background: index == root.selected-tab ? white : tab-touch.has-hover || root.dragging-tab == index ? #e5e7eb : #d8dbe0;
border-width: root.dragging-tab == index ? 1px : 0px; outline-enabled: root.dragging-tab == index;
border-color: #2463c7; outline-color: UiSurface.focus;
Rectangle { Rectangle {
width: parent.width - 27px; width: parent.width - 27px;
height: parent.height; height: parent.height;
@@ -1003,11 +1026,11 @@ export component StreamWindow inherits Window {
drop-zone := DropArea { drop-zone := DropArea {
can-drop(event) => { can-drop(event) => {
return !root.remote-update-visible && root.can-drop-file(event.data) return !root.modal-visible && root.can-drop-file(event.data)
? DragAction.copy : DragAction.none; ? DragAction.copy : DragAction.none;
} }
dropped(event) => { dropped(event) => {
if root.remote-update-visible { return DragAction.none; } if root.modal-visible { return DragAction.none; }
root.file-dropped(event.data); root.file-dropped(event.data);
return DragAction.copy; return DragAction.copy;
} }
@@ -1018,16 +1041,16 @@ export component StreamWindow inherits Window {
focus-gained => { root.keyboard-focus-changed(true); } focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); } focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => { key-pressed(event) => {
if !root.remote-update-visible { if !root.modal-visible {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
} }
accept accept
} }
key-released(event) => { key-released(event) => {
if root.remote-update-visible { if root.modal-visible {
// Dismiss on release so the same key cannot reach the peer. // Dismiss on release so the same key cannot reach the peer.
if event.text == Key.Escape || event.text == Key.Return { if event.text == Key.Escape || event.text == Key.Return {
root.dismiss-remote-update(); if root.privacy-unlock-visible { root.dismiss-privacy-unlock(); } else { root.dismiss-remote-update(); }
} }
} else { } else {
root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
@@ -1052,7 +1075,8 @@ export component StreamWindow inherits Window {
// The logo also retains a stable owner if another button is used. // The logo also retains a stable owner if another button is used.
if root.remote-pointer-active || root.control-drag-pending: TouchArea { if root.remote-pointer-active || root.control-drag-pending: TouchArea {
z: 15; z: 15;
mouse-cursor: root.control-drag-pending ? MouseCursor.grabbing mouse-cursor: root.control-drag-pending
? (root.control-hand-cursor-supported ? MouseCursor.pointer : MouseCursor.default)
: root.remote-cursor-active ? RemoteCursor.shape(root.remote-cursor-shape) : root.remote-cursor-active ? RemoteCursor.shape(root.remote-cursor-shape)
: MouseCursor.default; : MouseCursor.default;
pointer-event(event) => { pointer-event(event) => {
@@ -1075,7 +1099,7 @@ export component StreamWindow inherits Window {
control := Rectangle { control := Rectangle {
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width; x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
y: min(max(0px, root.control-pos-y), parent.height - self.height); y: min(max(0px, root.control-pos-y), parent.height - self.height);
width: root.control-visual-expanded ? min(80px + 9 * root.control-button-step + root.control-disconnect-gap, parent.width) : root.control-toolbar-height; width: root.control-visual-expanded ? min(80px + 7 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width + root.control-disconnect-gap, parent.width) : root.control-toolbar-height;
height: root.control-toolbar-height; height: root.control-toolbar-height;
z: 10; z: 10;
@@ -1120,8 +1144,8 @@ export component StreamWindow inherits Window {
} }
shortcut-button := ControlBarButton { shortcut-button := ControlBarButton {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible && root.secure-attention-supported;
action-enabled: root.control-expanded; action-enabled: root.control-expanded && root.secure-attention-supported;
x: root.control-buttons-x + root.control-button-step; x: root.control-buttons-x + root.control-button-step;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut; icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
@@ -1139,7 +1163,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 2 * root.control-button-step; x: root.control-buttons-x + root.control-button-step + root.control-shortcut-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled; icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse; tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
@@ -1149,7 +1173,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 4 * root.control-button-step; x: root.control-buttons-x + 2 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled; icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio; tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
@@ -1159,7 +1183,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 5 * root.control-button-step; x: root.control-buttons-x + 3 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file; icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.close-control-menus(); root.select-file(); } clicked => { root.close-control-menus(); root.select-file(); }
@@ -1168,7 +1192,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 6 * root.control-button-step; x: root.control-buttons-x + 4 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: "\u{f1de}"; selected: root.video-menu-open; icon: "\u{f1de}"; selected: root.video-menu-open;
tooltip: StreamStrings.video-settings; tooltip: StreamStrings.video-settings;
@@ -1183,7 +1207,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 7 * root.control-button-step; x: root.control-buttons-x + 5 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.signal; selected: root.stats-visible; icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats; tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
@@ -1193,23 +1217,23 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 8 * root.control-button-step; x: root.control-buttons-x + 6 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
clicked => { root.close-control-menus(); root.toggle-fullscreen(); } clicked => { root.close-control-menus(); root.toggle-fullscreen(); }
} }
if root.control-content-visible: Rectangle { if root.control-content-visible: SurfaceSeparator {
opacity: root.control-content-progress; opacity: root.control-content-progress;
x: close-button.x - 6px; y: 12px; x: close-button.x - 6px; y: 12px;
width: 1px; height: 14px; width: 1px; height: 14px;
background: #dbe5f1; vertical: true; outline-color: UiSurface.separator;
} }
close-button := ControlBarButton { close-button := ControlBarButton {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible;
action-enabled: root.control-expanded; action-enabled: root.control-expanded;
x: root.control-buttons-x + 9 * root.control-button-step + root.control-disconnect-gap; x: root.control-buttons-x + 7 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width + root.control-disconnect-gap;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect; icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
danger: true; danger: true;
@@ -1228,6 +1252,7 @@ export component StreamWindow inherits Window {
width: root.control-toolbar-height; width: root.control-toolbar-height;
height: root.control-toolbar-height; height: root.control-toolbar-height;
show-logo: true; show-logo: true;
hand-cursor-supported: root.control-hand-cursor-supported;
logo-pressed: root.control-drag-pending; logo-pressed: root.control-drag-pending;
logo-dragging: self.logo-pressed && root.control-dragging; logo-dragging: self.logo-pressed && root.control-dragging;
tooltip: root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control; tooltip: root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control;
@@ -1244,13 +1269,13 @@ export component StreamWindow inherits Window {
privacy-button := ControlBarButton { privacy-button := ControlBarButton {
reveal-progress: root.control-content-progress; reveal-progress: root.control-content-progress;
visible: root.control-content-visible; visible: root.control-content-visible && root.privacy-visible;
x: root.control-buttons-x + 3 * root.control-button-step; x: root.control-buttons-x + 2 * root.control-button-step + root.control-shortcut-width;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.user-secret; icon: FontAwesomeIcons.user-secret;
slashed: root.privacy-off && !root.privacy-unsupported; slashed: root.privacy-off && !root.privacy-unsupported;
muted: root.privacy-unsupported; muted: root.privacy-unsupported;
action-enabled: root.control-expanded && root.privacy-can-toggle; action-enabled: root.control-expanded && root.privacy-visible && root.privacy-can-toggle;
tooltip: root.privacy-status-text tooltip: root.privacy-status-text
+ (root.privacy-can-toggle ? "\n" + (root.privacy-active + (root.privacy-can-toggle ? "\n" + (root.privacy-active
? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : ""); ? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : "");
@@ -1408,12 +1433,12 @@ export component StreamWindow inherits Window {
vertical-alignment: center; vertical-alignment: center;
overflow: elide; overflow: elide;
} }
if index < root.stats-rows.length - 2: Rectangle { if index < root.stats-rows.length - 2: SurfaceSeparator {
x: 4px; x: 4px;
y: parent.height - 1px; y: parent.height - 1px;
width: parent.width - 8px; width: parent.width - 8px;
height: 1px; height: 1px;
background: #edf0f4; outline-color: UiSurface.separator;
} }
} }
@@ -1422,16 +1447,16 @@ export component StreamWindow inherits Window {
text: StreamStrings.stats-encoder + "\n" + StreamStrings.stats-decoder; text: StreamStrings.stats-encoder + "\n" + StreamStrings.stats-decoder;
font-size: ImGuiFontStyle.small; font-size: ImGuiFontStyle.small;
} }
Rectangle { FlatSurface {
x: 0px; x: 0px;
y: root.stats-header-height + root.stats-rows.length * stats-table.row-height y: root.stats-header-height + root.stats-rows.length * stats-table.row-height
+ 70px; + 70px;
width: parent.width; height: 44px; width: parent.width; height: 44px;
background: #f7f9fc; border-radius: UiRadius.control; background: #f7f9fc; border-radius: UiRadius.control;
border-width: 1px; border-color: #e8edf4; outline-color: UiSurface.edge;
Rectangle { SurfaceSeparator {
x: 8px; y: 22px; width: parent.width - 16px; height: 1px; x: 8px; y: 22px; width: parent.width - 16px; height: 1px;
background: #e8edf4; outline-color: UiSurface.separator;
} }
for codec[index] in [ for codec[index] in [
{ label: StreamStrings.stats-encoder, value: root.stats-encoder }, { label: StreamStrings.stats-encoder, value: root.stats-encoder },
@@ -1446,13 +1471,12 @@ export component StreamWindow inherits Window {
color: #667085; font-size: ImGuiFontStyle.small; color: #667085; font-size: ImGuiFontStyle.small;
vertical-alignment: center; vertical-alignment: center;
} }
Text { ScrollingText {
x: codec-label-width-probe.preferred-width + 8px; y: 0px; x: codec-label-width-probe.preferred-width + 8px; y: 0px;
width: parent.width - self.x; height: parent.height; width: parent.width - self.x; height: parent.height;
text: codec.value; color: #344054; text: codec.value; text-color: #344054;
font-size: ImGuiFontStyle.body; font-weight: 500; font-size: ImGuiFontStyle.body; font-weight: 500;
horizontal-alignment: right; align-right: true;
vertical-alignment: center; overflow: elide;
} }
} }
} }
@@ -1639,16 +1663,16 @@ export component StreamWindow inherits Window {
padding: 8px; spacing: 1px; padding: 8px; spacing: 1px;
for display[index] in root.displays: Rectangle { for display[index] in root.displays: Rectangle {
height: 29px; height: 29px;
background: index == display-menu.hovered-index && display-touch.pressed ? #bfd4f2 background: index == root.selected-display
: index == display-menu.hovered-index ? #d8e6f8 ? (index == display-menu.hovered-index ? (display-touch.pressed ? ControlStyle.selected-pressed : ControlStyle.selected-hover) : ControlStyle.selected)
: index == root.selected-display ? #e8eef9 : index == display-menu.hovered-index ? (display-touch.pressed ? ControlStyle.pressed : ControlStyle.hover) : transparent;
: transparent;
border-radius: UiRadius.control; border-radius: UiRadius.control;
Text { Text {
x: 8px; x: 8px;
width: parent.width - 34px; width: parent.width - 34px;
text: display; text: display;
color: #30343b; color: index == root.selected-display ? ControlStyle.selected-text : #30343b;
font-weight: index == root.selected-display ? 600 : 400;
font-size: ImGuiFontStyle.body; font-size: ImGuiFontStyle.body;
overflow: elide; overflow: elide;
vertical-alignment: center; vertical-alignment: center;
@@ -1657,7 +1681,7 @@ export component StreamWindow inherits Window {
x: parent.width - 24px; x: parent.width - 24px;
width: 16px; width: 16px;
text: FontAwesomeIcons.check; text: FontAwesomeIcons.check;
color: #2463c7; color: ControlStyle.selected-text;
font-family: "Font Awesome 6 Free"; font-family: "Font Awesome 6 Free";
font-weight: 900; font-weight: 900;
font-size: 10px; font-size: 10px;
@@ -1688,14 +1712,16 @@ export component StreamWindow inherits Window {
} }
} }
if root.shortcut-menu-open: StreamControlSurface { if root.secure-attention-supported && root.shortcut-menu-open: StreamControlSurface {
x: control.x + shortcut-button.x; y: root.shortcut-menu-y; x: control.x + shortcut-button.x; y: root.shortcut-menu-y;
width: 180px; height: root.shortcut-menu-height; z: 11; width: 180px; height: root.shortcut-menu-height; z: 11;
TouchArea { mouse-cursor: default; scroll-event(event) => { accept } } TouchArea { mouse-cursor: default; scroll-event(event) => { accept } }
VerticalLayout { VerticalLayout {
padding: 8px; spacing: 1px; padding: 8px; spacing: 1px;
for shortcut in root.shortcut-items: Rectangle { for shortcut in root.shortcut-items: Rectangle {
height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent; height: 29px;
border-radius: UiRadius.control;
background: shortcut-touch.pressed ? ControlStyle.pressed : shortcut-touch.has-hover ? ControlStyle.hover : transparent;
Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; } Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
shortcut-touch := TouchArea { shortcut-touch := TouchArea {
mouse-cursor: MouseCursor.pointer; mouse-cursor: MouseCursor.pointer;
@@ -1730,7 +1756,7 @@ export component StreamWindow inherits Window {
} }
} }
if root.remote-update-visible: Rectangle { if root.modal-visible: Rectangle {
width: parent.width; height: parent.height; width: parent.width; height: parent.height;
background: #0c162c52; background: #0c162c52;
z: 13; z: 13;
@@ -1739,17 +1765,49 @@ export component StreamWindow inherits Window {
scroll-event(event) => { accept } scroll-event(event) => { accept }
} }
} }
if root.privacy-unlock-visible: DialogSurface {
x: (parent.width - self.width) / 2;
y: max(16px, (parent.height - self.height) / 2);
width: min(460px, parent.width - 32px);
height: unlock-message.y + unlock-message.height + 84px;
outline-color: UiSurface.edge;
border-radius: UiRadius.surface;
z: 14;
TouchArea { scroll-event(event) => { accept } }
unlock-title := Text {
x: 24px; y: 24px; width: parent.width - 48px;
height: max(28px, self.preferred-height);
text: StreamStrings.privacy-unlocked-title;
color: #1b2433; font-size: 20px; font-weight: 600; wrap: word-wrap;
}
unlock-message := Text {
x: 24px; y: unlock-title.y + unlock-title.height + 8px;
width: parent.width - 48px; height: max(22px, self.preferred-height);
text: StreamStrings.privacy-unlocked-message;
color: #667085; font-size: 15px; wrap: word-wrap;
}
SecondaryButton {
x: parent.width - 244px; y: parent.height - 60px;
width: 104px; height: 36px;
text: StreamStrings.privacy-unlock-confirm;
clicked => { root.dismiss-privacy-unlock(); }
}
PrimaryButton {
x: parent.width - 128px; y: parent.height - 60px;
width: 104px; height: 36px;
text: StreamStrings.privacy-unlock-lock;
enabled: root.privacy-can-lock;
clicked => { root.lock-privacy(); }
}
}
remote-update-banner := DialogSurface { remote-update-banner := DialogSurface {
visible: root.remote-update-visible; visible: root.remote-update-visible && !root.privacy-unlock-visible;
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px)); y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px));
width: min(400px, parent.width - 48px); width: min(400px, parent.width - 48px);
height: remote-update-message.y + remote-update-message.height + 84px; height: remote-update-message.y + remote-update-message.height + 84px;
border-color: #e5e9f0; outline-color: UiSurface.edge;
border-radius: UiRadius.surface; border-radius: UiRadius.surface;
drop-shadow-color: #07142b30;
drop-shadow-blur: 32px;
drop-shadow-offset-y: 12px;
z: 14; z: 14;
TouchArea { TouchArea {
mouse-cursor: default; mouse-cursor: default;
@@ -1774,8 +1832,8 @@ export component StreamWindow inherits Window {
IconButton { IconButton {
x: parent.width - 44px; y: 12px; width: 32px; height: 32px; x: parent.width - 44px; y: 12px; width: 32px; height: 32px;
icon: FontAwesomeIcons.xmark; icon-size: 14px; icon: FontAwesomeIcons.xmark; icon-size: 14px;
icon-color: #8a93a3; icon-color: ControlStyle.icon;
hover-background: #f2f4f7; normal-background: ControlStyle.surface;
clicked => { root.dismiss-remote-update(); } clicked => { root.dismiss-remote-update(); }
} }
PrimaryButton { PrimaryButton {
@@ -1789,10 +1847,10 @@ export component StreamWindow inherits Window {
} }
// Transfer progress overlay at bottom-left. // Transfer progress overlay at bottom-left.
if root.file-transfer-visible: Rectangle { if root.file-transfer-visible: FlatSurface {
x: root.file-panel-x; y: root.file-panel-y; x: root.file-panel-x; y: root.file-panel-y;
width: root.file-panel-width; height: root.file-panel-height; width: root.file-panel-width; height: root.file-panel-height;
background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.surface; z: 7; background: white; outline-color: UiSurface.edge; border-radius: UiRadius.surface; z: 7;
// Header/padding and drags leaving the panel belong to local UI. // Header/padding and drags leaving the panel belong to local UI.
TouchArea { TouchArea {
mouse-cursor: default; mouse-cursor: default;
@@ -1801,6 +1859,7 @@ export component StreamWindow inherits Window {
Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; } Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; }
IconButton { IconButton {
x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px; x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px;
normal-background: ControlStyle.surface;
clicked => { root.close-file-transfer(); } clicked => { root.close-file-transfer(); }
} }
ScrollView { ScrollView {
@@ -1840,12 +1899,11 @@ export component StreamWindow inherits Window {
} }
} }
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle { if root.custom-titlebar && !root.fullscreen-enabled: FlatSurface {
width: parent.width; width: parent.width;
height: parent.height; height: parent.height;
background: transparent; background: transparent;
border-width: 1px; outline-color: UiSurface.edge;
border-color: #aaa8a6;
border-radius: border-radius:
root.window-maximized ? 0px : root.window-corner-radius; root.window-maximized ? 0px : root.window-corner-radius;
z: 200; z: 200;
+31 -3
View File
@@ -40,8 +40,27 @@ inline int ApplyMainWindowStrings(
int language_index) { int language_index) {
const int language = const int language =
localization::detail::ClampLanguageIndex(language_index); localization::detail::ClampLanguageIndex(language_index);
window->global<ui::GalleryStrings>().set_language(language); auto& gallery = window->global<ui::GalleryStrings>();
gallery.set_language(language);
const auto& retry = localization::Literal("请在 {0} 秒后重试", language);
const auto split = retry.find("{0}");
gallery.set_retry_prefix(Text(retry.substr(0, split)));
gallery.set_retry_suffix(Text(split == std::string::npos ? "" : retry.substr(split + 3)));
std::vector<slint::SharedString> literals;
for (const auto* key : localization::detail::kSlintLiterals)
literals.push_back(Text(localization::Literal(key, language)));
gallery.set_texts(std::make_shared<slint::VectorModel<slint::SharedString>>(literals));
std::vector<slint::SharedString> languages;
for (const auto& option : localization::GetSupportedLanguages())
languages.push_back(Text(option.display_name));
languages.push_back(Text(localization::Literal("跟随系统", language)));
window->set_language_options(std::make_shared<slint::VectorModel<slint::SharedString>>(languages));
auto& strings = window->global<ui::UiStrings>(); auto& strings = window->global<ui::UiStrings>();
strings.set_window_minimize(Text(localization::window_minimize[language]));
strings.set_window_close(Text(localization::window_close[language]));
strings.set_save(Text(localization::save[language]));
strings.set_copyright(Text(localization::copyright_notice[language]));
strings.set_license(Text(localization::license_notice[language]));
auto& messages = window->global<ui::MessageStrings>(); auto& messages = window->global<ui::MessageStrings>();
messages.set_messages(Text(localization::messages[language])); messages.set_messages(Text(localization::messages[language]));
messages.set_unread(Text(localization::message_unread[language])); messages.set_unread(Text(localization::message_unread[language]));
@@ -81,7 +100,7 @@ inline int ApplyMainWindowStrings(
strings.set_ok(Text(localization::ok[language])); strings.set_ok(Text(localization::ok[language]));
strings.set_cancel(Text(localization::cancel[language])); strings.set_cancel(Text(localization::cancel[language]));
strings.set_new_password(Text(localization::new_password[language])); strings.set_new_password(Text(localization::new_password[language]));
strings.set_invalid_password(Text(localization::max_password_len[language])); strings.set_invalid_password(Text(localization::password_format_invalid[language]));
strings.set_edit_alias( strings.set_edit_alias(
Text(localization::input_connection_alias[language])); Text(localization::input_connection_alias[language]));
strings.set_delete_connection( strings.set_delete_connection(
@@ -93,7 +112,6 @@ inline int ApplyMainWindowStrings(
strings.set_capture_method_auto( strings.set_capture_method_auto(
Text(localization::screen_capture_method_auto[language])); Text(localization::screen_capture_method_auto[language]));
strings.set_codec(Text(localization::video_encode_format[language])); strings.set_codec(Text(localization::video_encode_format[language]));
strings.set_turn_relay(Text(localization::enable_turn[language]));
strings.set_force_relay(Text(localization::force_relay[language])); strings.set_force_relay(Text(localization::force_relay[language]));
strings.set_self_hosted( strings.set_self_hosted(
Text(localization::self_hosted_server_config[language])); Text(localization::self_hosted_server_config[language]));
@@ -101,7 +119,9 @@ inline int ApplyMainWindowStrings(
strings.set_daemon(Text(localization::enable_daemon[language])); strings.set_daemon(Text(localization::enable_daemon[language]));
strings.set_save_remote_previews(Text(localization::save_remote_previews[language])); strings.set_save_remote_previews(Text(localization::save_remote_previews[language]));
strings.set_unknown_platform(Text(localization::unknown_platform[language])); strings.set_unknown_platform(Text(localization::unknown_platform[language]));
strings.set_privacy_local_unlock(Text(localization::privacy_local_unlock[language]));
strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language])); strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language]));
strings.set_lock_on_disconnect(Text(localization::lock_on_disconnect[language]));
strings.set_file_save_path( strings.set_file_save_path(
Text(localization::file_transfer_save_path[language])); Text(localization::file_transfer_save_path[language]));
strings.set_default_desktop(Text(localization::default_desktop[language])); strings.set_default_desktop(Text(localization::default_desktop[language]));
@@ -192,11 +212,19 @@ inline int ApplyStreamWindowStrings(
const int language = const int language =
localization::detail::ClampLanguageIndex(language_index); localization::detail::ClampLanguageIndex(language_index);
auto& strings = window->template global<ui::StreamStrings>(); auto& strings = window->template global<ui::StreamStrings>();
strings.set_window_minimize(Text(localization::window_minimize[language]));
strings.set_window_maximize(Text(localization::window_maximize[language]));
strings.set_window_restore(Text(localization::window_restore[language]));
strings.set_window_close(Text(localization::window_close[language]));
strings.set_remote_update_title(Text(localization::remote_update_title[language])); strings.set_remote_update_title(Text(localization::remote_update_title[language]));
strings.set_remote_update(Text(localization::remote_update[language])); strings.set_remote_update(Text(localization::remote_update[language]));
strings.set_remote_update_dismiss(Text(localization::remote_update_dismiss[language])); strings.set_remote_update_dismiss(Text(localization::remote_update_dismiss[language]));
strings.set_select_display(Text(localization::select_display[language])); strings.set_select_display(Text(localization::select_display[language]));
strings.set_send_shortcut(Text(localization::send_shortcut[language])); strings.set_send_shortcut(Text(localization::send_shortcut[language]));
strings.set_privacy_unlocked_title(Text(localization::privacy_unlocked_title[language]));
strings.set_privacy_unlocked_message(Text(localization::privacy_unlocked_message[language]));
strings.set_privacy_unlock_confirm(Text(localization::privacy_unlock_confirm[language]));
strings.set_privacy_unlock_lock(Text(localization::privacy_unlock_lock[language]));
strings.set_privacy_enable(Text(localization::privacy_enable[language])); strings.set_privacy_enable(Text(localization::privacy_enable[language]));
strings.set_privacy_disable(Text(localization::privacy_disable[language])); strings.set_privacy_disable(Text(localization::privacy_disable[language]));
strings.set_control_mouse(Text(localization::control_mouse[language])); strings.set_control_mouse(Text(localization::control_mouse[language]));
+35
View File
@@ -0,0 +1,35 @@
// Flat surfaces share solid outlines in software and GPU renderers.
// Interaction states use their actual colors, including focus and validation.
export global UiSurface {
out property <color> edge: #d7dfe9;
out property <color> hover: #8eabd2;
out property <color> focus: #3564e3;
out property <color> error: #d65252;
out property <color> separator: #e1e7ef;
}
export component FlatSurface inherits Rectangle {
in property <color> outline-color: UiSurface.edge;
in property <bool> outline-enabled: true;
in property <bool> outline-focused: false;
border-width: !root.outline-enabled ? 0px : root.outline-focused ? 2px : 1px;
border-color: root.outline-color;
border-top-left-radius: root.border-radius;
border-top-right-radius: root.border-radius;
border-bottom-left-radius: root.border-radius;
border-bottom-right-radius: root.border-radius;
@children
}
export component SurfaceSeparator inherits Rectangle {
in property <bool> vertical: false;
in property <color> outline-color: UiSurface.separator;
height: 1px;
Rectangle {
width: root.vertical ? 1px : root.width;
height: root.vertical ? root.height : 1px;
background: root.outline-color;
}
}
@@ -1,14 +1,13 @@
import { FlatSurface, UiSurface } from "ui_surface.slint";
import { UiRadius } from "ui_radius.slint"; import { UiRadius } from "ui_radius.slint";
import { ControlStyle, ButtonStyle } from "common.slint";
// Colors and spacing for the connection workbench; shapes are shared app-wide. // Colors and spacing for the connection workbench; shapes are shared app-wide.
export global WorkbenchStyle { export global WorkbenchStyle {
out property <color> canvas: #f2f5f9; out property <color> canvas: #edf2f8;
out property <color> surface: #ffffff; out property <color> surface: #ffffff;
out property <color> border: #c6cfdd;
out property <color> control-border: #b2bfd1;
out property <color> hover-border: #7f97b8;
out property <color> field-surface: #f5f7fb; out property <color> field-surface: #f5f7fb;
out property <color> hover-surface: #eef3fb; out property <color> hover-surface: ControlStyle.hover;
out property <color> pressed-surface: #e4ebf6; out property <color> pressed-surface: ControlStyle.pressed;
out property <color> separator: #e1e7ef; out property <color> separator: #e1e7ef;
out property <color> text: #1c2940; out property <color> text: #1c2940;
out property <color> muted: #69788e; out property <color> muted: #69788e;
@@ -16,18 +15,19 @@ export global WorkbenchStyle {
out property <color> success: #18835a; out property <color> success: #18835a;
} }
export component WorkbenchButton inherits Rectangle { export component WorkbenchButton inherits FlatSurface {
in property <string> text; in property <string> text;
in property <length> text-size: 13px; in property <length> text-size: 13px;
in property <color> text-color: root.destructive || root.primary ? white : WorkbenchStyle.text; in property <color> text-color: root.destructive || root.primary ? white : WorkbenchStyle.text;
in property <color> normal-border-color: WorkbenchStyle.control-border; in property <color> normal-outline-color: ButtonStyle.border;
in property <color> normal-background: ButtonStyle.surface;
in property <string> icon; in property <string> icon;
in property <string> tooltip; in property <string> tooltip;
in property <bool> tooltip-above: false; in property <bool> tooltip-above: false;
in property <bool> tooltip-suppressed: false; in property <bool> tooltip-suppressed: false;
in property <color> icon-color: root.destructive ? white : root.primary in property <color> icon-color: root.destructive ? white : root.primary
? (root.soft && !touch.has-hover && !root.show-focus ? WorkbenchStyle.accent : white) ? (root.soft && !touch.has-hover && !root.show-focus ? WorkbenchStyle.accent : white)
: WorkbenchStyle.muted; : ControlStyle.icon;
in property <bool> primary: false; in property <bool> primary: false;
in property <bool> destructive: false; in property <bool> destructive: false;
in property <bool> soft: false; in property <bool> soft: false;
@@ -45,24 +45,23 @@ export component WorkbenchButton inherits Rectangle {
width: 112px; width: 112px;
height: 36px; height: 36px;
border-radius: UiRadius.control; border-radius: UiRadius.control;
border-width: root.show-focus ? 2px : root.soft ? 0px : 1px; outline-enabled: root.show-focus || (!root.quiet && !root.soft && !root.primary && !root.destructive);
border-color: root.destructive outline-focused: root.show-focus;
? (root.show-focus ? #7a271a : touch.pressed ? #912018 : touch.has-hover ? #b42318 : #d92d20) outline-color: !root.enabled ? ButtonStyle.disabled-border
: root.show-focus ? (root.primary ? #193da0 : WorkbenchStyle.accent) : root.show-focus ? WorkbenchStyle.accent
: root.primary ? WorkbenchStyle.accent : touch.pressed ? ButtonStyle.pressed-border
: touch.has-hover ? WorkbenchStyle.hover-border : touch.has-hover ? ButtonStyle.hover-border : root.normal-outline-color;
: root.quiet ? transparent : root.normal-border-color;
background: root.destructive background: root.destructive
? (touch.pressed ? #912018 : touch.has-hover ? #b42318 : #d92d20) ? (touch.pressed ? #912018 : touch.has-hover ? #b42318 : #d92d20)
: root.soft : root.soft
? (root.primary ? (root.primary
? (touch.pressed ? #254cc0 : touch.has-hover || root.show-focus ? WorkbenchStyle.accent : #edf2ff) ? (touch.pressed ? #254cc0 : touch.has-hover || root.show-focus ? WorkbenchStyle.accent : #dce7ff)
: (touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.field-surface)) : (touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover : ButtonStyle.surface))
: root.primary : root.primary
? (touch.pressed ? #254cc0 : touch.has-hover ? #4574f0 : WorkbenchStyle.accent) ? (touch.pressed ? #254cc0 : touch.has-hover ? #4574f0 : WorkbenchStyle.accent)
: (touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : root.quiet ? transparent : white); : (touch.pressed ? ButtonStyle.pressed : touch.has-hover ? ButtonStyle.hover : root.quiet ? transparent : root.normal-background);
opacity: root.enabled ? 1 : 0.45; opacity: root.enabled ? 1 : 0.45;
animate background, border-color { duration: 120ms; } animate background, outline-color { duration: 120ms; }
forward-focus: button-focus; forward-focus: button-focus;
accessible-role: button; accessible-role: button;
accessible-label: root.tooltip != "" ? root.tooltip : root.text; accessible-label: root.tooltip != "" ? root.tooltip : root.text;
@@ -92,6 +91,11 @@ export component WorkbenchButton inherits Rectangle {
accessible-role: none; accessible-role: none;
} }
} }
if root.show-focus && (root.primary || root.destructive): Rectangle {
x: 3px; y: 3px; width: parent.width - 6px; height: parent.height - 6px;
border-radius: max(0px, root.border-radius - 3px);
border-width: 1px; border-color: white;
}
button-focus := FocusScope { button-focus := FocusScope {
enabled: root.enabled; enabled: root.enabled;
focus-gained(reason) => { focus-gained(reason) => {
@@ -150,7 +154,7 @@ export component WorkbenchButton inherits Rectangle {
} }
// A raised surface keeps transient action feedback distinct from the workbench. // A raised surface keeps transient action feedback distinct from the workbench.
export component WorkbenchNotice inherits Rectangle { export component WorkbenchNotice inherits FlatSurface {
in property <string> text; in property <string> text;
in property <string> icon; in property <string> icon;
in property <bool> error: false; in property <bool> error: false;
@@ -159,12 +163,8 @@ export component WorkbenchNotice inherits Rectangle {
width: min(root.width-limit, max(280px, message.preferred-width + 64px)); width: min(root.width-limit, max(280px, message.preferred-width + 64px));
height: 64px; height: 64px;
background: root.error ? #fff1f0 : #e9f6ef; background: root.error ? #fff1f0 : #e9f6ef;
border-width: 1px; outline-color: root.error ? UiSurface.error : UiSurface.edge;
border-color: root.error ? #f0c4bf : #b9dfc9;
border-radius: UiRadius.surface; border-radius: UiRadius.surface;
drop-shadow-color: #14294424;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 6px;
accessible-role: AccessibleRole.text; accessible-role: AccessibleRole.text;
accessible-label: root.text; accessible-label: root.text;
Text { Text {
@@ -192,9 +192,8 @@ export component WorkbenchNotice inherits Rectangle {
} }
} }
export component WorkbenchCard inherits Rectangle { export component WorkbenchCard inherits FlatSurface {
background: WorkbenchStyle.surface; background: WorkbenchStyle.surface;
border-width: 1px; outline-color: UiSurface.edge;
border-color: WorkbenchStyle.border;
border-radius: UiRadius.surface; border-radius: UiRadius.surface;
} }

Some files were not shown because too many files have changed in this diff Show More