mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-14 19:41:45 +08:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89f94485c6 | |||
| 480e9e8234 | |||
| 29e1852a68 | |||
| 8314335b31 | |||
| acb9f63e1d | |||
| 005a8b4a04 | |||
| e2149974cc | |||
| 6c578292e8 | |||
| 28930c0463 | |||
| 37141afece | |||
| 493b14ba78 | |||
| cf1de4de62 | |||
| 9fdb8410d3 | |||
| a2b79462ab | |||
| dce221be5a | |||
| 9d1ab3fba3 | |||
| b3bd18845d | |||
| 435f6ec61d | |||
| 0497814004 | |||
| 4b1ef9e20d | |||
| 10d5250d23 | |||
| 2ee580d49d | |||
| 4a54029cac | |||
| 001848bf2f | |||
| 989bf80fe8 | |||
| 78b5f47668 | |||
| 97e9e44faa | |||
| ff226f6d80 | |||
| 0cbdb6ffb3 | |||
| b8117c5c34 | |||
| a69614d464 | |||
| 58ee593e26 | |||
| 09bc9056c9 | |||
| 0c6df924d1 | |||
| 456817b4f4 | |||
| 16570ee34f | |||
| 2b40c61d8e | |||
| dcc64cdeae |
@@ -44,7 +44,7 @@ env:
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.8"
|
||||
VERSION: "1.4.9"
|
||||
NDK_VERSION: "r28c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -1984,6 +1984,7 @@ jobs:
|
||||
sudo apt-get install -y libarchive-tools libfuse2
|
||||
# set-up appimage-builder
|
||||
# https://github.com/AppImage/AppImageKit/issues/1395
|
||||
sudo pip3 install "setuptools_scm<10"
|
||||
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
|
||||
# run appimage-builder
|
||||
pushd appimage
|
||||
|
||||
@@ -17,7 +17,7 @@ env:
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.8"
|
||||
VERSION: "1.4.9"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
name: wf-cliprdr CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "libs/clipboard/src/windows/**"
|
||||
- "tests/test_invariant_wf_cliprdr.c"
|
||||
- ".github/workflows/wf-cliprdr-ci.yml"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "libs/clipboard/src/windows/**"
|
||||
- "tests/test_invariant_wf_cliprdr.c"
|
||||
- ".github/workflows/wf-cliprdr-ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: wf_cliprdr invariant test
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Setup vcpkg with GitHub Actions binary cache
|
||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||
with:
|
||||
vcpkgDirectory: C:\vcpkg
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependency
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
|
||||
|
||||
- name: Build test
|
||||
shell: pwsh
|
||||
run: |
|
||||
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
|
||||
New-Item -ItemType Directory -Force $testRoot | Out-Null
|
||||
|
||||
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
|
||||
$cmakeLists = @(
|
||||
'cmake_minimum_required(VERSION 3.20)'
|
||||
'project(test_invariant_wf_cliprdr C)'
|
||||
''
|
||||
'set(CMAKE_C_STANDARD 11)'
|
||||
'set(CMAKE_C_STANDARD_REQUIRED ON)'
|
||||
'set(CMAKE_C_EXTENSIONS OFF)'
|
||||
''
|
||||
'find_package(check CONFIG REQUIRED)'
|
||||
''
|
||||
'add_executable(test_invariant_wf_cliprdr'
|
||||
' "TEST_SOURCE"'
|
||||
')'
|
||||
''
|
||||
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
|
||||
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
|
||||
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
|
||||
')'
|
||||
) -join [Environment]::NewLine
|
||||
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
|
||||
|
||||
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
|
||||
cmake --build (Join-Path $testRoot 'out') --config Release
|
||||
|
||||
- name: Run test
|
||||
shell: pwsh
|
||||
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe
|
||||
Generated
+3
-26
@@ -6588,7 +6588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"rpassword 2.1.0",
|
||||
"rpassword",
|
||||
"tempfile",
|
||||
"termios 0.3.3",
|
||||
"winapi 0.3.9",
|
||||
@@ -7152,17 +7152,6 @@ dependencies = [
|
||||
"winapi 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtcp"
|
||||
version = "0.14.0"
|
||||
@@ -7174,16 +7163,6 @@ dependencies = [
|
||||
"webrtc-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtoolbox"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtp"
|
||||
version = "0.14.0"
|
||||
@@ -7270,7 +7249,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.4.8"
|
||||
version = "1.4.9"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -7283,7 +7262,6 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"chrono",
|
||||
"cidr-utils",
|
||||
"clap 4.5.53",
|
||||
"clipboard",
|
||||
"clipboard-master",
|
||||
"cocoa 0.24.1",
|
||||
@@ -7341,7 +7319,6 @@ dependencies = [
|
||||
"repng",
|
||||
"reqwest",
|
||||
"ringbuf",
|
||||
"rpassword 7.3.1",
|
||||
"rubato",
|
||||
"runas",
|
||||
"rust-pulsectl",
|
||||
@@ -7385,7 +7362,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.8"
|
||||
version = "1.4.9"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
|
||||
+2
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.8"
|
||||
version = "1.4.9"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -22,7 +22,6 @@ path = "src/service.rs"
|
||||
|
||||
[features]
|
||||
inline = []
|
||||
cli = []
|
||||
use_samplerate = ["samplerate"]
|
||||
use_rubato = ["rubato"]
|
||||
use_dasp = ["dasp"]
|
||||
@@ -62,8 +61,6 @@ dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpol
|
||||
rubato = { version = "0.12", optional = true }
|
||||
samplerate = { version = "0.2", optional = true }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
clap = "4.2"
|
||||
rpassword = "7.2"
|
||||
num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
default-net = "0.14"
|
||||
@@ -213,7 +210,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
|
||||
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
||||
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
FileDescription = "RustDesk Remote Desktop"
|
||||
OriginalFilename = "rustdesk.exe"
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.8
|
||||
version: 1.4.9
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.8
|
||||
version: 1.4.9
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -8,6 +8,7 @@ package com.carriez.flutter_hbb
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.content.Intent
|
||||
import android.graphics.Path
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -68,6 +69,16 @@ class InputService : AccessibilityService() {
|
||||
get() = ctx != null
|
||||
}
|
||||
|
||||
private fun notifyInputState() {
|
||||
val inputState = isOpen.toString()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to inputState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val logTag = "input service"
|
||||
private var leftIsDown = false
|
||||
private var touchPath = Path()
|
||||
@@ -716,6 +727,7 @@ class InputService : AccessibilityService() {
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
ctx = this
|
||||
notifyInputState()
|
||||
val info = AccessibilityServiceInfo()
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
@@ -734,8 +746,16 @@ class InputService : AccessibilityService() {
|
||||
|
||||
override fun onDestroy() {
|
||||
ctx = null
|
||||
// Keep this fallback even though onUnbind usually notifies first.
|
||||
notifyInputState()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
ctx = null
|
||||
notifyInputState()
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
override fun onInterrupt() {}
|
||||
}
|
||||
|
||||
@@ -200,12 +200,13 @@ class MainActivity : FlutterActivity() {
|
||||
"stop_input" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
InputService.ctx?.disableSelf()
|
||||
} else {
|
||||
InputService.ctx = null
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
}
|
||||
InputService.ctx = null
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
"cancel_notification" -> {
|
||||
|
||||
@@ -6,83 +6,82 @@ ANDROID_ABI=$1
|
||||
|
||||
# Build RustDesk dependencies for Android using vcpkg.json
|
||||
# Required:
|
||||
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
|
||||
# 1. set VCPKG_ROOT / ANDROID_NDK_HOME path environment variables
|
||||
# 2. vcpkg initialized
|
||||
# 3. ndk, version: r25c or newer
|
||||
|
||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
||||
echo "Failed! Please set ANDROID_NDK_HOME"
|
||||
exit 1
|
||||
if [ -z "${ANDROID_NDK_HOME}" ]; then
|
||||
echo "ERROR: Please set ANDROID_NDK_HOME environment variable" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$VCPKG_ROOT" ]; then
|
||||
echo "Failed! Please set VCPKG_ROOT"
|
||||
exit 1
|
||||
if [ -z "${VCPKG_ROOT}" ]; then
|
||||
echo "ERROR: Please set VCPKG_ROOT environment variable" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_LEVEL="21"
|
||||
case "${ANDROID_ABI}" in
|
||||
arm64-v8a)
|
||||
VCPKG_TARGET=arm64-android
|
||||
;;
|
||||
armeabi-v7a)
|
||||
VCPKG_TARGET=arm-neon-android
|
||||
;;
|
||||
x86_64)
|
||||
VCPKG_TARGET=x64-android
|
||||
;;
|
||||
x86)
|
||||
VCPKG_TARGET=x86-android
|
||||
;;
|
||||
*)
|
||||
echo "Usage: build_android_deps.sh <arm64-v8a|armeabi-v7a|x86_64|x86>" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Get directory of this script
|
||||
|
||||
SCRIPTDIR="$(readlink -f "$0")"
|
||||
SCRIPTDIR="$(dirname "$SCRIPTDIR")"
|
||||
SCRIPTDIR="$(dirname "${SCRIPTDIR}")"
|
||||
|
||||
# Check if vcpkg.json is one level up - in root directory of RD
|
||||
|
||||
if [ ! -f "$SCRIPTDIR/../vcpkg.json" ]; then
|
||||
echo "Failed! Please check where vcpkg.json is!"
|
||||
exit 1
|
||||
if [ ! -f "${SCRIPTDIR}/../vcpkg.json" ]; then
|
||||
echo "ERROR: Can not find vcpkg.json in RustDesk top-level directory" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NDK llvm toolchain
|
||||
echo "INFO: Building and install vcpkg dependencies for Android ${ANDROID_ABI} ..."
|
||||
|
||||
HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/`
|
||||
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
|
||||
pushd "${SCRIPTDIR}/.."
|
||||
|
||||
function build {
|
||||
ANDROID_ABI=$1
|
||||
"${VCPKG_ROOT}/vcpkg" install \
|
||||
--triplet "${VCPKG_TARGET}" \
|
||||
--x-install-root="${VCPKG_ROOT}/installed"
|
||||
|
||||
case "$ANDROID_ABI" in
|
||||
arm64-v8a)
|
||||
ABI=aarch64-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=arm64-android
|
||||
;;
|
||||
armeabi-v7a)
|
||||
ABI=armv7a-linux-androideabi$API_LEVEL
|
||||
VCPKG_TARGET=arm-neon-android
|
||||
;;
|
||||
x86_64)
|
||||
ABI=x86_64-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=x64-android
|
||||
;;
|
||||
x86)
|
||||
ABI=i686-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=x86-android
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: ANDROID_ABI must be one of: arm64-v8a, armeabi-v7a, x86_64, x86" >&2
|
||||
return 1
|
||||
esac
|
||||
popd
|
||||
|
||||
echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies"
|
||||
pushd "$SCRIPTDIR/.."
|
||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
||||
popd
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
|
||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
||||
echo "INFO: Completed building vcpkg dependencies for Android ${ANDROID_ABI}"
|
||||
|
||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
||||
echo "*** [Start] Move arm-neon-android to arm-android"
|
||||
if [ "${ANDROID_ABI}" = 'armeabi-v7a' ]; then
|
||||
# Symlink arm-neon-android to arm-android because cargo-ndk does not
|
||||
# understand NEON suffix.
|
||||
|
||||
mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android"
|
||||
if [ -d "${VCPKG_ROOT}/installed/arm-neon-android" ]; then
|
||||
echo 'INFO: Symlinking arm-neon-android to arm-android'
|
||||
|
||||
echo "*** [Finished] Move arm-neon-android to arm-android"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ ! -z "$ANDROID_ABI" ]; then
|
||||
build "$ANDROID_ABI"
|
||||
else
|
||||
echo "Usage: build-android-deps.sh <ANDROID-ABI>" >&2
|
||||
exit 1
|
||||
ln -sf \
|
||||
"${VCPKG_ROOT}/installed/arm-neon-android" \
|
||||
"${VCPKG_ROOT}/installed/arm-android"
|
||||
|
||||
echo 'INFO: Symlinked arm-neon-android to arm-android'
|
||||
else
|
||||
cat 0<<.a
|
||||
ERROR: 'vcpkg install' seem to complete successfully but
|
||||
directory '${VCPKG_ROOT}/installed/arm-neon-android' is missing!
|
||||
|
||||
.a
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
+48
-1
@@ -1185,6 +1185,48 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
VoidCallback? onSubmit,
|
||||
int? submitTimeout}) {
|
||||
dialogManager.dismissAll();
|
||||
if (type.contains('insecure-connection')) {
|
||||
Future<void> closeSession() async {
|
||||
await bind.sessionSetCommon(
|
||||
sessionId: sessionId,
|
||||
key: 'continue-insecure-connection',
|
||||
value: 'N',
|
||||
);
|
||||
dialogManager.dismissAll();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
void continueSession() {
|
||||
unawaited(
|
||||
bind.sessionSetCommon(
|
||||
sessionId: sessionId,
|
||||
key: 'continue-insecure-connection',
|
||||
value: 'Y',
|
||||
),
|
||||
);
|
||||
dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
dialogManager.show(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(child: msgboxContent(type, title, text)),
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Continue',
|
||||
onPressed: continueSession,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton('Disconnect', onPressed: closeSession),
|
||||
],
|
||||
onSubmit: closeSession,
|
||||
onCancel: closeSession,
|
||||
),
|
||||
tag: '$sessionId-$type-$title-$text-$link',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Widget> buttons = [];
|
||||
bool hasOk = false;
|
||||
submit() {
|
||||
@@ -3350,7 +3392,12 @@ Future<List<Rect>> getScreenRectList() async {
|
||||
}
|
||||
|
||||
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
||||
{bool updateCursorPos = true}) {
|
||||
{bool updateCursorPos = true, bool recordSelection = true}) {
|
||||
if (recordSelection) {
|
||||
ffi.ffiModel.lastUserDisplay = i;
|
||||
ffi.ffiModel.cancelPendingRestoreTimer();
|
||||
ffi.ffiModel.pendingMonitorRestore = null;
|
||||
}
|
||||
final displays = i == kAllDisplayValue
|
||||
? List.generate(pi.displays.length, (index) => index)
|
||||
: [i];
|
||||
|
||||
@@ -606,7 +606,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
// to-do:
|
||||
// 1. Web desktop
|
||||
// 2. Mobile, copy the image to the clipboard
|
||||
if (isDesktop) {
|
||||
if ((isDefaultConn || ffi.connType == ConnType.viewCamera) && isDesktop) {
|
||||
final isScreenshotSupported = bind.sessionGetCommonSync(
|
||||
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
||||
if ('true' == isScreenshotSupported) {
|
||||
|
||||
@@ -483,13 +483,15 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
final incomingOnly = bind.isIncomingOnly();
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
||||
final children = <Widget>[
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
if (!isWeb && !incomingOnly)
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
kOptionEnableConfirmClosingTabs,
|
||||
isServer: false),
|
||||
if (!bind.isIncomingOnly())
|
||||
if (!incomingOnly)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'allow-remote-toolbar-docking-any-edge',
|
||||
@@ -499,9 +501,10 @@ class _GeneralState extends State<_General> {
|
||||
reloadAllWindows();
|
||||
},
|
||||
),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||
if (!isWeb && !outgoingOnly)
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||
if (!isWeb) wallpaper(),
|
||||
if (!isWeb && !bind.isIncomingOnly()) ...[
|
||||
if (!isWeb && !incomingOnly) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
@@ -540,40 +543,40 @@ class _GeneralState extends State<_General> {
|
||||
isServer: false,
|
||||
),
|
||||
),
|
||||
if (!isWeb && !bind.isCustomClient())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
kOptionEnableCheckUpdate,
|
||||
isServer: false,
|
||||
),
|
||||
if (showAutoUpdate)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Auto update',
|
||||
kOptionAllowAutoUpdate,
|
||||
isServer: true,
|
||||
),
|
||||
if (isWindows && !bind.isOutgoingOnly())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Capture screen using DirectX',
|
||||
kOptionDirectxCapture,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable UDP hole punching',
|
||||
kOptionEnableUdpPunch,
|
||||
isServer: false,
|
||||
),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable IPv6 P2P connection',
|
||||
kOptionEnableIpv6Punch,
|
||||
isServer: false,
|
||||
),
|
||||
],
|
||||
],
|
||||
if (!isWeb && !bind.isCustomClient())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
kOptionEnableCheckUpdate,
|
||||
isServer: false,
|
||||
),
|
||||
if (showAutoUpdate)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Auto update',
|
||||
kOptionAllowAutoUpdate,
|
||||
isServer: true,
|
||||
),
|
||||
if (isWindows && !outgoingOnly)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Capture screen using DirectX',
|
||||
kOptionDirectxCapture,
|
||||
),
|
||||
if (!isWeb && !incomingOnly) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable UDP hole punching',
|
||||
kOptionEnableUdpPunch,
|
||||
isServer: false,
|
||||
),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable IPv6 P2P connection',
|
||||
kOptionEnableIpv6Punch,
|
||||
isServer: false,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2471,7 +2474,7 @@ class _AboutState extends State<_About> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Tech Pte. Ltd.\n$license',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
|
||||
@@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Auto-close tab when shell exits
|
||||
_terminalModel.onClosed = () {
|
||||
if (mounted) {
|
||||
widget.tabController.closeBy(widget.tabKey);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
|
||||
@@ -1167,8 +1167,8 @@ class _MonitorMenu extends StatelessWidget {
|
||||
tooltip: isMulti
|
||||
? ''
|
||||
: isAllMonitors
|
||||
? 'all monitors'
|
||||
: '#${i + 1} monitor',
|
||||
? 'All monitors'
|
||||
: '#{${i + 1}} monitor',
|
||||
hMargin: isMulti ? null : 6,
|
||||
vMargin: isMulti ? null : 12,
|
||||
topLevel: false,
|
||||
@@ -2852,7 +2852,7 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
|
||||
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
|
||||
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
|
||||
button = Tooltip(
|
||||
message: widget.tooltip,
|
||||
message: translate(widget.tooltip),
|
||||
child: button,
|
||||
);
|
||||
if (widget.topLevel) {
|
||||
|
||||
@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Auto-close connection when shell exits
|
||||
_terminalModel.onClosed = () {
|
||||
if (mounted) {
|
||||
closeConnection(id: widget.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Web desktop users have full hardware keyboard access, so the on-screen
|
||||
// terminal extra keys bar is unnecessary and disabled.
|
||||
_showTerminalExtraKeys = !isWebDesktop &&
|
||||
|
||||
@@ -142,12 +142,22 @@ class FileModel {
|
||||
}
|
||||
|
||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id == null || !jobController.hasTransferConflictJob(id)) {
|
||||
debugPrint("Ignore stale override confirm event: $evt");
|
||||
return;
|
||||
}
|
||||
evtLoop.pushEvent(
|
||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||
}
|
||||
|
||||
Future<void> overrideFileConfirm(Map<String, dynamic> evt,
|
||||
{bool? overrideConfirm, bool skip = false}) async {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0;
|
||||
if (id == 0 || !jobController.hasTransferConflictJob(id)) {
|
||||
debugPrint("Ignore override confirm for inactive job: $evt");
|
||||
return;
|
||||
}
|
||||
// If `skip == true`, it means to skip this file without showing dialog.
|
||||
// Because `resp` may be null after the user operation or the last remembered operation,
|
||||
// and we should distinguish them.
|
||||
@@ -156,15 +166,12 @@ class FileModel {
|
||||
? await showFileConfirmDialog(translate("Overwrite"),
|
||||
"${evt['read_path']}", true, evt['is_identical'] == "true")
|
||||
: null);
|
||||
final id = int.tryParse(evt['id']) ?? 0;
|
||||
if (!jobController.hasTransferConflictJob(id)) {
|
||||
debugPrint("Ignore override confirm result for inactive job: $evt");
|
||||
return;
|
||||
}
|
||||
if (false == resp) {
|
||||
final jobIndex = jobController.getJob(id);
|
||||
if (jobIndex != -1) {
|
||||
await jobController.cancelJob(id);
|
||||
final job = jobController.jobTable[jobIndex];
|
||||
job.state = JobState.done;
|
||||
jobController.jobTable.refresh();
|
||||
}
|
||||
await jobController.cancelTransferConflictBatch(id);
|
||||
} else {
|
||||
var need_override = false;
|
||||
if (resp == null) {
|
||||
@@ -176,6 +183,7 @@ class FileModel {
|
||||
}
|
||||
// Update the loop config.
|
||||
if (fileConfirmCheckboxRemember) {
|
||||
jobController.rememberTransferConflictBatch(id, resp);
|
||||
evtLoop.setSkip(!need_override);
|
||||
}
|
||||
await bind.sessionSetConfirmOverrideFile(
|
||||
@@ -285,6 +293,8 @@ class FileModel {
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
final jobID = jobController.addTransferJob(entry, false);
|
||||
jobController.registerTransferConflictBatch([jobID],
|
||||
batchId: int.tryParse(obj['batchId']?.toString() ?? ''));
|
||||
webSendLocalFiles(
|
||||
handleIndex: handleIndex,
|
||||
actId: jobID,
|
||||
@@ -570,8 +580,15 @@ class FileController {
|
||||
final toPath = otherSideData.directory.path;
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
final transferJobs = <(Entry, int)>[];
|
||||
final transferJobIds = <int>[];
|
||||
for (var from in items.items) {
|
||||
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||
transferJobs.add((from, jobID));
|
||||
transferJobIds.add(jobID);
|
||||
}
|
||||
jobController.registerTransferConflictBatch(transferJobIds);
|
||||
for (final (from, jobID) in transferJobs) {
|
||||
bind.sessionSendFiles(
|
||||
sessionId: sessionId,
|
||||
actId: jobID,
|
||||
@@ -917,6 +934,10 @@ class JobController {
|
||||
static final JobID jobID = JobID();
|
||||
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||
int _nextTransferConflictBatchId = 1;
|
||||
final Map<int, int> _transferConflictJobToBatch = {};
|
||||
int? _transferConflictRememberBatchId;
|
||||
bool? _transferConflictRememberOverrideConfirm;
|
||||
final GetSessionID getSessionID;
|
||||
final GetDialogManager getDialogManager;
|
||||
SessionID get sessionId => getSessionID();
|
||||
@@ -929,6 +950,57 @@ class JobController {
|
||||
return jobTable.indexWhere((element) => element.id == id);
|
||||
}
|
||||
|
||||
void registerTransferConflictBatch(Iterable<int> jobIds, {int? batchId}) {
|
||||
final ids = jobIds.toList(growable: false);
|
||||
if (ids.isEmpty) {
|
||||
return;
|
||||
}
|
||||
batchId ??= _nextTransferConflictBatchId++;
|
||||
if (batchId >= _nextTransferConflictBatchId) {
|
||||
_nextTransferConflictBatchId = batchId + 1;
|
||||
}
|
||||
for (final jobId in ids) {
|
||||
_transferConflictJobToBatch[jobId] = batchId;
|
||||
}
|
||||
}
|
||||
|
||||
int? transferConflictBatchId(int jobId) {
|
||||
return _transferConflictJobToBatch[jobId];
|
||||
}
|
||||
|
||||
bool hasTransferConflictJob(int jobId) {
|
||||
return transferConflictBatchId(jobId) != null;
|
||||
}
|
||||
|
||||
bool isTransferConflictRememberBatch(int? batchId) {
|
||||
return batchId != null && batchId == _transferConflictRememberBatchId;
|
||||
}
|
||||
|
||||
bool? transferConflictRememberOverrideConfirm(int? batchId) {
|
||||
if (!isTransferConflictRememberBatch(batchId)) {
|
||||
return null;
|
||||
}
|
||||
return _transferConflictRememberOverrideConfirm;
|
||||
}
|
||||
|
||||
void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) {
|
||||
_transferConflictRememberBatchId = _transferConflictJobToBatch[jobId];
|
||||
_transferConflictRememberOverrideConfirm = overrideConfirm;
|
||||
}
|
||||
|
||||
void unregisterTransferConflictJob(int jobId) {
|
||||
final batchId = _transferConflictJobToBatch.remove(jobId);
|
||||
if (batchId == null) {
|
||||
return;
|
||||
}
|
||||
if (!_transferConflictJobToBatch.containsValue(batchId)) {
|
||||
if (_transferConflictRememberBatchId == batchId) {
|
||||
_transferConflictRememberBatchId = null;
|
||||
_transferConflictRememberOverrideConfirm = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return jobID
|
||||
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||
final jobID = JobController.jobID.next();
|
||||
@@ -1000,7 +1072,10 @@ class JobController {
|
||||
id = int.parse(evt['id']);
|
||||
} catch (_) {}
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex == -1) return true;
|
||||
if (jobIndex == -1) {
|
||||
unregisterTransferConflictJob(id);
|
||||
return true;
|
||||
}
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.deleteFile) {
|
||||
@@ -1026,6 +1101,9 @@ class JobController {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.state == JobState.done || job.state == JobState.error) {
|
||||
unregisterTransferConflictJob(id);
|
||||
}
|
||||
if (job.type == JobType.deleteDir) {
|
||||
return job.state == JobState.done;
|
||||
} else {
|
||||
@@ -1035,9 +1113,15 @@ class JobController {
|
||||
|
||||
void jobError(Map<String, dynamic> evt) {
|
||||
final err = evt['err'].toString();
|
||||
int jobIndex = getJob(int.parse(evt['id']));
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id == null) {
|
||||
debugPrint("Ignore job error with invalid id: $evt");
|
||||
return;
|
||||
}
|
||||
int jobIndex = getJob(id);
|
||||
if (jobIndex != -1) {
|
||||
final job = jobTable[jobIndex];
|
||||
if (job.state == JobState.done && job.err == "cancel") return;
|
||||
job.state = JobState.error;
|
||||
job.err = err;
|
||||
job.recvJobRes = true;
|
||||
@@ -1060,6 +1144,11 @@ class JobController {
|
||||
}
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.state == JobState.done || job.state == JobState.error) {
|
||||
unregisterTransferConflictJob(job.id);
|
||||
}
|
||||
} else {
|
||||
unregisterTransferConflictJob(id);
|
||||
}
|
||||
if (err == _kOneWayFileTransferError) {
|
||||
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
|
||||
@@ -1096,9 +1185,42 @@ class JobController {
|
||||
}
|
||||
|
||||
Future<void> cancelJob(int id) async {
|
||||
unregisterTransferConflictJob(id);
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
|
||||
Future<void> cancelTransferConflictBatch(int jobId) async {
|
||||
final batchId = _transferConflictJobToBatch[jobId];
|
||||
final batchJobIds = batchId == null ? [jobId] : <int>[];
|
||||
if (batchId != null) {
|
||||
for (final entry in _transferConflictJobToBatch.entries) {
|
||||
if (entry.value == batchId) {
|
||||
batchJobIds.add(entry.key);
|
||||
}
|
||||
}
|
||||
for (final id in batchJobIds) {
|
||||
unregisterTransferConflictJob(id);
|
||||
}
|
||||
}
|
||||
final jobIdsToCancel = batchJobIds.toSet();
|
||||
for (final job in jobTable) {
|
||||
if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) {
|
||||
continue;
|
||||
}
|
||||
job.state = JobState.done;
|
||||
job.err = "cancel";
|
||||
job.recvJobRes = true;
|
||||
}
|
||||
jobTable.refresh();
|
||||
for (final id in batchJobIds) {
|
||||
try {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to cancel transfer job $id in conflict batch: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadLastJob(Map<String, dynamic> evt) async {
|
||||
debugPrint("load last job: $evt");
|
||||
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
||||
@@ -1145,7 +1267,7 @@ class JobController {
|
||||
..state = JobState.paused;
|
||||
jobTable.add(jobProgress);
|
||||
}
|
||||
|
||||
registerTransferConflictBatch([currJobId]);
|
||||
await bind.sessionAddJob(
|
||||
sessionId: sessionId,
|
||||
isRemote: isRemote,
|
||||
@@ -1193,6 +1315,9 @@ class JobController {
|
||||
|
||||
void clear() {
|
||||
jobTable.clear();
|
||||
_transferConflictJobToBatch.clear();
|
||||
_transferConflictRememberBatchId = null;
|
||||
_transferConflictRememberOverrideConfirm = null;
|
||||
jobResultListener.clear();
|
||||
}
|
||||
}
|
||||
@@ -1535,6 +1660,9 @@ class JobProgress {
|
||||
|
||||
String display() {
|
||||
if (type == JobType.transfer) {
|
||||
if (state == JobState.done && err == "cancel") {
|
||||
return translate("Cancel");
|
||||
}
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
}
|
||||
@@ -1844,21 +1972,44 @@ class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
|
||||
|
||||
class FileDialogEventLoop
|
||||
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
|
||||
int? _batchId;
|
||||
bool? _overrideConfirm;
|
||||
bool _skip = false;
|
||||
|
||||
@override
|
||||
Future<void> onPreConsume(
|
||||
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
|
||||
var event = evt as _FileDialogEvent;
|
||||
final event = evt as _FileDialogEvent;
|
||||
final model = event.fileModel.target;
|
||||
final jobId = int.tryParse(evt.data['id']?.toString() ?? '');
|
||||
final batchId = model == null || jobId == null
|
||||
? null
|
||||
: model.jobController.transferConflictBatchId(jobId);
|
||||
final keepRemembered = model != null &&
|
||||
model.jobController.isTransferConflictRememberBatch(batchId);
|
||||
// The loop only preloads the remembered batch choice. The model updates it
|
||||
// after the user answers the current overwrite dialog.
|
||||
if (_batchId != batchId && !keepRemembered) {
|
||||
_batchId = batchId;
|
||||
_overrideConfirm = null;
|
||||
_skip = false;
|
||||
} else {
|
||||
_batchId = batchId;
|
||||
}
|
||||
if (keepRemembered) {
|
||||
_overrideConfirm =
|
||||
model.jobController.transferConflictRememberOverrideConfirm(batchId);
|
||||
_skip = _overrideConfirm == null;
|
||||
}
|
||||
event.setOverrideConfirm(_overrideConfirm);
|
||||
event.setSkip(_skip);
|
||||
debugPrint(
|
||||
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>");
|
||||
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} batchId: $_batchId overrideConfirm: $_overrideConfirm, skip: $_skip>");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onEventsClear() {
|
||||
_batchId = null;
|
||||
_overrideConfirm = null;
|
||||
_skip = false;
|
||||
return super.onEventsClear();
|
||||
|
||||
@@ -112,6 +112,9 @@ class CachedPeerData {
|
||||
class FfiModel with ChangeNotifier {
|
||||
CachedPeerData cachedPeerData = CachedPeerData();
|
||||
PeerInfo _pi = PeerInfo();
|
||||
int? lastUserDisplay;
|
||||
int? pendingMonitorRestore;
|
||||
Timer? _pendingRestoreTimer;
|
||||
Rect? _rect;
|
||||
|
||||
var _inputBlocked = false;
|
||||
@@ -248,6 +251,8 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
clear() {
|
||||
_pi = PeerInfo();
|
||||
lastUserDisplay = null;
|
||||
_cancelPendingMonitorRestore();
|
||||
_secure = null;
|
||||
_direct = null;
|
||||
_inputBlocked = false;
|
||||
@@ -932,6 +937,7 @@ class FfiModel with ChangeNotifier {
|
||||
// frame briefly, then shows the Connecting overlay.
|
||||
if (_restartReconnectDelayTimer == null) {
|
||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||
_cancelPendingMonitorRestore();
|
||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
|
||||
clearPermissions();
|
||||
// Retry once more after the silent window so restart reconnect attempts
|
||||
@@ -1084,10 +1090,22 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelPendingMonitorRestore() {
|
||||
_pendingRestoreTimer?.cancel();
|
||||
_pendingRestoreTimer = null;
|
||||
pendingMonitorRestore = null;
|
||||
}
|
||||
|
||||
void cancelPendingRestoreTimer() {
|
||||
_pendingRestoreTimer?.cancel();
|
||||
_pendingRestoreTimer = null;
|
||||
}
|
||||
|
||||
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
||||
bool forceRelay) {
|
||||
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||
_cancelPendingMonitorRestore();
|
||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
||||
clearPermissions();
|
||||
dialogManager.dismissAll();
|
||||
@@ -1401,6 +1419,25 @@ class FfiModel with ChangeNotifier {
|
||||
// now replaced to _updateCurDisplay
|
||||
updateCurDisplay(sessionId);
|
||||
}
|
||||
// After reconnecting, restore the last selected monitor once the canvas is ready.
|
||||
// Switching earlier can offset the view if the monitor sizes differ.
|
||||
final last = lastUserDisplay;
|
||||
pendingMonitorRestore = (!isCache &&
|
||||
last != null &&
|
||||
last != currentDisplay &&
|
||||
bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: sessionId) !=
|
||||
'Y' &&
|
||||
((last == kAllDisplayValue && _pi.displays.isNotEmpty) ||
|
||||
(last >= 0 && last < _pi.displays.length)))
|
||||
? last
|
||||
: null;
|
||||
// Fallback if the first image event never reaches this tab (multi-UI).
|
||||
_pendingRestoreTimer?.cancel();
|
||||
if (pendingMonitorRestore != null) {
|
||||
_pendingRestoreTimer = Timer(const Duration(milliseconds: 1500),
|
||||
() => parent.target?._applyPendingMonitorRestore());
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
_reconnects = 1;
|
||||
_offlineReconnectStartTime = null;
|
||||
@@ -3911,17 +3948,35 @@ class FFI {
|
||||
}
|
||||
if (ffiModel.waitForFirstImage.value == true) {
|
||||
ffiModel.waitForFirstImage.value = false;
|
||||
ffiModel.cancelPendingRestoreTimer();
|
||||
ffiModel.resetRestartReconnectState();
|
||||
dialogManager.dismissAll();
|
||||
await canvasModel.updateViewStyle();
|
||||
await canvasModel.updateScrollStyle();
|
||||
await canvasModel.initializeEdgeScrollEdgeThickness();
|
||||
for (final cb in imageModel.callbacksOnFirstImage) {
|
||||
cb(id);
|
||||
try {
|
||||
await canvasModel.updateViewStyle();
|
||||
await canvasModel.updateScrollStyle();
|
||||
await canvasModel.initializeEdgeScrollEdgeThickness();
|
||||
for (final cb in imageModel.callbacksOnFirstImage) {
|
||||
cb(id);
|
||||
}
|
||||
} finally {
|
||||
_applyPendingMonitorRestore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _applyPendingMonitorRestore() {
|
||||
final restore = ffiModel.pendingMonitorRestore;
|
||||
ffiModel._cancelPendingMonitorRestore();
|
||||
if (restore == null || closed) return;
|
||||
// The display list may have changed since the restore was queued.
|
||||
final displays = ffiModel.pi.displays;
|
||||
if ((restore == kAllDisplayValue && displays.isNotEmpty) ||
|
||||
(restore >= 0 && restore < displays.length)) {
|
||||
openMonitorInTheSameTab(restore, this, ffiModel.pi,
|
||||
recordSelection: false, updateCursorPos: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Login with [password], choose if the client should [remember] it.
|
||||
void login(String osUsername, String osPassword, SessionID sessionId,
|
||||
String password, bool remember) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
@@ -38,6 +37,10 @@ class TerminalModel with ChangeNotifier {
|
||||
|
||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||
|
||||
/// Called when the terminal session ends (shell exits).
|
||||
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
|
||||
VoidCallback? onClosed;
|
||||
|
||||
Future<void> _handleInput(String data) async {
|
||||
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
|
||||
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
|
||||
@@ -247,6 +250,33 @@ class TerminalModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
|
||||
if (evt.containsKey('exit_code')) {
|
||||
final v = evt['exit_code'];
|
||||
if (v is int) {
|
||||
// Desktop and mobile send exit_code as an int
|
||||
return v;
|
||||
} else if (v is String) {
|
||||
// Web sends exit_code as a string
|
||||
final parsed = int.tryParse(v);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Event does not contain exit_code');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void handleTerminalResponse(Map<String, dynamic> evt) {
|
||||
final String? type = evt['type'];
|
||||
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
||||
@@ -469,10 +499,12 @@ class TerminalModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||
final int exitCode = evt['exit_code'] ?? 0;
|
||||
final int exitCode = getExitCodeFromEvt(evt);
|
||||
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
// Auto-close the tab/page
|
||||
onClosed?.call();
|
||||
}
|
||||
|
||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||
|
||||
@@ -1914,6 +1914,15 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("sessionHandleScreenshot");
|
||||
}
|
||||
|
||||
Future<void> sessionSetCommon(
|
||||
{required UuidValue sessionId, required String key, required String value, dynamic hint}) {
|
||||
js.context.callMethod('setByName', [
|
||||
'common',
|
||||
jsonEncode({'name': key, 'value': value})
|
||||
]);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
String? sessionGetCommonSync(
|
||||
{required UuidValue sessionId,
|
||||
required String key,
|
||||
|
||||
@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved.
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.4.8+66
|
||||
version: 1.4.9+67
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -89,11 +89,11 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Purslane Ltd" "\0"
|
||||
VALUE "CompanyName", "Purslane Tech Pte. Ltd." "\0"
|
||||
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "rustdesk" "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "rustdesk.exe" "\0"
|
||||
VALUE "ProductName", "RustDesk" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
|
||||
@@ -533,7 +533,7 @@ impl FuseServer {
|
||||
offset: i64,
|
||||
size: u32,
|
||||
) -> Result<Vec<u8>, std::io::Error> {
|
||||
// todo: async and concurrent read, generate stream_id per request
|
||||
let request_stream_id = rand::random();
|
||||
let cb_requested = unsafe {
|
||||
// convert `size` from u32 to i32
|
||||
// yet with same bit representation
|
||||
@@ -543,7 +543,7 @@ impl FuseServer {
|
||||
let (n_position_high, n_position_low) =
|
||||
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
|
||||
let request = ClipboardFile::FileContentsRequest {
|
||||
stream_id: node.stream_id,
|
||||
stream_id: request_stream_id,
|
||||
list_index: node.index as i32,
|
||||
dw_flags: 2,
|
||||
n_position_low,
|
||||
@@ -573,7 +573,7 @@ impl FuseServer {
|
||||
stream_id,
|
||||
requested_data,
|
||||
} => {
|
||||
if stream_id != node.stream_id {
|
||||
if stream_id != request_stream_id {
|
||||
log::debug!("stream id mismatch, ignore");
|
||||
continue;
|
||||
}
|
||||
@@ -611,11 +611,6 @@ struct FuseNode {
|
||||
/// connection id
|
||||
pub conn_id: i32,
|
||||
|
||||
// todo: use stream_id to identify a FileContents request-reply
|
||||
// instead of a whole file
|
||||
/// stream id
|
||||
pub stream_id: i32,
|
||||
|
||||
/// file index in peer's file list
|
||||
/// NOTE:
|
||||
/// it is NOT the same as inode, this is the index in the file list
|
||||
@@ -639,7 +634,6 @@ impl FuseNode {
|
||||
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
|
||||
Self {
|
||||
conn_id: desc.conn_id,
|
||||
stream_id: rand::random(),
|
||||
index: inode as usize - 2,
|
||||
name: desc
|
||||
.name
|
||||
@@ -656,7 +650,6 @@ impl FuseNode {
|
||||
pub fn new_root() -> Self {
|
||||
Self {
|
||||
conn_id: 0,
|
||||
stream_id: rand::random(),
|
||||
index: 0,
|
||||
name: String::from("/"),
|
||||
parent: None,
|
||||
|
||||
@@ -4,23 +4,24 @@ use super::filetype::FileDescription;
|
||||
use crate::{ClipboardFile, CliprdrError};
|
||||
use cs::FuseServer;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
use hbb_common::{config::Config, log};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
|
||||
let mnt_path = fuse_mount_point("cliprdr-client");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
|
||||
let mnt_path = fuse_mount_point("cliprdr-server");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
@@ -31,6 +32,21 @@ lazy_static::lazy_static! {
|
||||
|
||||
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum MountPointState {
|
||||
HealthyMount,
|
||||
NotMounted,
|
||||
StaleMount,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
fn fuse_mount_point(name: &str) -> String {
|
||||
let mut path = PathBuf::from(Config::ipc_path(""));
|
||||
path.pop();
|
||||
path.push(name);
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
||||
if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
@@ -53,8 +69,27 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
if fuse_context_lock.is_some() {
|
||||
return Ok(());
|
||||
if let Some(ctx) = fuse_context_lock.as_ref() {
|
||||
match inspect_mount_point_state(&ctx.mount_point) {
|
||||
MountPointState::HealthyMount => return Ok(()),
|
||||
MountPointState::StaleMount | MountPointState::NotMounted => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is disconnected, remounting",
|
||||
ctx.mount_point.display()
|
||||
);
|
||||
let stale_context = fuse_context_lock.take();
|
||||
drop(fuse_context_lock);
|
||||
drop(stale_context);
|
||||
return init_fuse_context(is_client);
|
||||
}
|
||||
MountPointState::Unknown => {
|
||||
log::warn!(
|
||||
"failed to verify clipboard FUSE mount {}",
|
||||
ctx.mount_point.display()
|
||||
);
|
||||
return Err(CliprdrError::CliprdrInit);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mount_point = if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
@@ -63,10 +98,32 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
};
|
||||
|
||||
let mount_point = std::path::PathBuf::from(&*mount_point);
|
||||
match inspect_mount_point_state(&mount_point) {
|
||||
MountPointState::HealthyMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is already active in another context",
|
||||
mount_point.display()
|
||||
);
|
||||
return Err(CliprdrError::ClipboardOccupied);
|
||||
}
|
||||
MountPointState::StaleMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is stale, cleaning up before remount",
|
||||
mount_point.display()
|
||||
);
|
||||
unmount_fuse_mount_point(&mount_point);
|
||||
validate_mount_state_after_stale_cleanup(
|
||||
&mount_point,
|
||||
inspect_mount_point_state(&mount_point),
|
||||
)?;
|
||||
}
|
||||
MountPointState::Unknown => return Err(CliprdrError::CliprdrInit),
|
||||
MountPointState::NotMounted => {}
|
||||
}
|
||||
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
prepare_fuse_mount_point(&mount_point);
|
||||
prepare_fuse_mount_point(&mount_point)?;
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
@@ -159,35 +216,246 @@ struct FuseContext {
|
||||
}
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) -> Result<(), CliprdrError> {
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
};
|
||||
|
||||
fs::create_dir(mount_point).ok();
|
||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
||||
if let Some(parent) = mount_point.parent() {
|
||||
reject_symlink_path(parent)?;
|
||||
if let Err(e) = fs::create_dir_all(parent) {
|
||||
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
|
||||
return Err(CliprdrError::CliprdrInit);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = std::process::Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
||||
reject_symlink_path(mount_point)?;
|
||||
|
||||
let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) {
|
||||
log::warn!(
|
||||
"failed to create clipboard FUSE mount point {}, trying stale mount cleanup: {:?}",
|
||||
mount_point.display(),
|
||||
e
|
||||
);
|
||||
unmount_fuse_mount_point(mount_point);
|
||||
fs::create_dir_all(mount_point).map_err(|e| {
|
||||
log::error!(
|
||||
"failed to create clipboard FUSE mount point {} after cleanup: {:?}",
|
||||
mount_point.display(),
|
||||
e
|
||||
);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
|
||||
log::warn!(
|
||||
"failed to set clipboard FUSE mount point permissions {}: {:?}",
|
||||
mount_point.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if !recovered_stale_mount {
|
||||
unmount_fuse_mount_point(mount_point);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inspect_mount_point_state(mount_point: &Path) -> MountPointState {
|
||||
if ensure_mount_point_path_is_safe(mount_point).is_err() {
|
||||
return MountPointState::Unknown;
|
||||
}
|
||||
inspect_mount_point_state_with(
|
||||
mount_point,
|
||||
std::fs::metadata(mount_point),
|
||||
std::fs::read_to_string("/proc/self/mountinfo"),
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_mount_state_after_stale_cleanup(
|
||||
mount_point: &Path,
|
||||
mount_state: MountPointState,
|
||||
) -> Result<(), CliprdrError> {
|
||||
match mount_state {
|
||||
MountPointState::NotMounted => Ok(()),
|
||||
MountPointState::HealthyMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is still active after stale cleanup",
|
||||
mount_point.display()
|
||||
);
|
||||
Err(CliprdrError::ClipboardOccupied)
|
||||
}
|
||||
MountPointState::StaleMount => {
|
||||
log::warn!(
|
||||
"clipboard FUSE mount {} is still stale after cleanup",
|
||||
mount_point.display()
|
||||
);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
MountPointState::Unknown => {
|
||||
log::warn!(
|
||||
"failed to verify clipboard FUSE mount {} after cleanup",
|
||||
mount_point.display()
|
||||
);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
if is_client {
|
||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
||||
} else {
|
||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
||||
fn inspect_mount_point_state_with<T>(
|
||||
mount_point: &Path,
|
||||
metadata_result: io::Result<T>,
|
||||
mountinfo_result: io::Result<String>,
|
||||
) -> MountPointState {
|
||||
match metadata_result {
|
||||
Ok(_) => match mountinfo_result {
|
||||
Ok(mountinfo) => {
|
||||
if is_mount_point_listed_in_mountinfo(mount_point, &mountinfo) {
|
||||
MountPointState::HealthyMount
|
||||
} else {
|
||||
MountPointState::NotMounted
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to read mountinfo for {:?}: {:?}", mount_point, e);
|
||||
MountPointState::Unknown
|
||||
}
|
||||
},
|
||||
Err(e) if e.raw_os_error() == Some(libc::ENOTCONN) => MountPointState::StaleMount,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => MountPointState::NotMounted,
|
||||
Err(e) => {
|
||||
log::warn!("failed to inspect FUSE mount {:?}: {:?}", mount_point, e);
|
||||
MountPointState::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mount_point_listed_in_mountinfo(mount_point: &Path, mountinfo: &str) -> bool {
|
||||
let mount_point = mount_point.to_string_lossy();
|
||||
mountinfo.lines().any(|line| {
|
||||
let mut fields = line.split_whitespace();
|
||||
let _mount_id = fields.next();
|
||||
let _parent_id = fields.next();
|
||||
let _major_minor = fields.next();
|
||||
let _root = fields.next();
|
||||
let mount_path = fields.next();
|
||||
mount_path == Some(mount_point.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
fn reject_symlink_metadata_result(
|
||||
path: &Path,
|
||||
metadata_result: io::Result<std::fs::Metadata>,
|
||||
allow_disconnected_mount: bool,
|
||||
) -> Result<(), CliprdrError> {
|
||||
match metadata_result {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||
log::warn!("refusing to use symlinked FUSE path {:?}", path);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) if allow_disconnected_mount && e.raw_os_error() == Some(libc::ENOTCONN) => Ok(()),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => {
|
||||
log::warn!("failed to inspect FUSE path {:?}: {:?}", path, e);
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_symlink_path(path: &Path) -> Result<(), CliprdrError> {
|
||||
reject_symlink_metadata_result(path, std::fs::symlink_metadata(path), false)
|
||||
}
|
||||
|
||||
fn ensure_mount_point_path_is_safe(mount_point: &Path) -> Result<(), CliprdrError> {
|
||||
if let Some(parent) = mount_point.parent() {
|
||||
reject_symlink_path(parent)?;
|
||||
}
|
||||
reject_symlink_metadata_result(mount_point, std::fs::symlink_metadata(mount_point), true)
|
||||
}
|
||||
|
||||
fn unmount_fuse_mount_point(mount_point: &Path) {
|
||||
if ensure_mount_point_path_is_safe(mount_point).is_err() {
|
||||
log::warn!(
|
||||
"refusing to unmount unsafe clipboard FUSE mount point {:?}",
|
||||
mount_point
|
||||
);
|
||||
return;
|
||||
}
|
||||
if inspect_mount_point_state_with(
|
||||
mount_point,
|
||||
std::fs::metadata(mount_point),
|
||||
std::fs::read_to_string("/proc/self/mountinfo"),
|
||||
) == MountPointState::NotMounted
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (program, args) in unmount_command_candidates() {
|
||||
if run_unmount_command(program, args, mount_point) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
log::warn!(
|
||||
"failed to unmount clipboard FUSE mount point {:?}",
|
||||
mount_point
|
||||
);
|
||||
}
|
||||
|
||||
fn unmount_command_candidates() -> [(&'static str, &'static [&'static str]); 3] {
|
||||
[
|
||||
("fusermount3", &["-uz"]),
|
||||
("fusermount", &["-uz"]),
|
||||
("umount", &["-l"]),
|
||||
]
|
||||
}
|
||||
|
||||
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
|
||||
match std::process::Command::new(program)
|
||||
.args(args)
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
Ok(status) if status.success() => {}
|
||||
Ok(status) => {
|
||||
log::debug!(
|
||||
"{} {:?} exited with status {:?}",
|
||||
program,
|
||||
mount_point,
|
||||
status.code()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
let ctx = {
|
||||
let mut fuse_context_lock = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
fuse_context_lock.take()
|
||||
};
|
||||
drop(ctx);
|
||||
}
|
||||
|
||||
impl Drop for FuseContext {
|
||||
fn drop(&mut self) {
|
||||
self.session.lock().take().map(|s| s.join());
|
||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
||||
log::info!(
|
||||
"unmounting clipboard FUSE from {}",
|
||||
self.mount_point.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,3 +491,66 @@ impl FuseContext {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{fs, io};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
#[test]
|
||||
fn classifies_mount_point_state_from_metadata_and_mountinfo() {
|
||||
let mount_point = std::env::temp_dir().join(format!(
|
||||
"rustdesk-fuse-mount-state-test-{}-{}",
|
||||
std::process::id(),
|
||||
line!()
|
||||
));
|
||||
let mountinfo = format!(
|
||||
"123 1 0:45 / {} rw,nosuid,nodev - fuse.rustdesk rustdesk rw\n",
|
||||
mount_point.display()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(mountinfo)),
|
||||
MountPointState::HealthyMount
|
||||
);
|
||||
assert_eq!(
|
||||
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(String::new())),
|
||||
MountPointState::NotMounted
|
||||
);
|
||||
|
||||
let disconnected_metadata: io::Result<()> =
|
||||
Err(io::Error::from_raw_os_error(libc::ENOTCONN));
|
||||
assert_eq!(
|
||||
inspect_mount_point_state_with(&mount_point, disconnected_metadata, Ok(String::new())),
|
||||
MountPointState::StaleMount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn rejects_symlink_mount_point() {
|
||||
let base = std::env::temp_dir().join(format!(
|
||||
"rustdesk-fuse-symlink-test-{}-{}",
|
||||
std::process::id(),
|
||||
line!()
|
||||
));
|
||||
let mount_parent = base.join("parent");
|
||||
let mount_point = mount_parent.join("cliprdr-client");
|
||||
let symlink_target = base.join("symlink-target");
|
||||
let _ = fs::remove_dir_all(&base);
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
fs::create_dir_all(&mount_parent).unwrap();
|
||||
fs::create_dir_all(&symlink_target).unwrap();
|
||||
symlink(&symlink_target, &mount_point).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
prepare_fuse_mount_point(&mount_point),
|
||||
Err(CliprdrError::CliprdrInit)
|
||||
));
|
||||
|
||||
let _ = fs::remove_dir_all(&base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,20 +192,16 @@ impl LocalFile {
|
||||
});
|
||||
};
|
||||
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
let read_result = if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
.and_then(|_| handle.read_exact(buf))
|
||||
} else {
|
||||
handle.read_exact(buf)
|
||||
};
|
||||
if let Err(e) = read_result {
|
||||
return Err(self.invalidate_handle(e));
|
||||
}
|
||||
handle
|
||||
.read_exact(buf)
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let new_offset = offset + (buf.len() as u64);
|
||||
self.offset.store(new_offset, Ordering::Relaxed);
|
||||
|
||||
@@ -217,6 +213,15 @@ impl LocalFile {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn invalidate_handle(&mut self, err: std::io::Error) -> CliprdrError {
|
||||
self.offset.store(0, Ordering::Relaxed);
|
||||
self.handle = None;
|
||||
CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
@@ -278,7 +283,10 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
|
||||
#[cfg(test)]
|
||||
mod file_list_test {
|
||||
use std::{path::PathBuf, sync::atomic::AtomicU64};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use hbb_common::bytes::{BufMut, BytesMut};
|
||||
|
||||
@@ -384,4 +392,30 @@ mod file_list_test {
|
||||
as_bin_parse_test("/test")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exact_at_reopens_after_read_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_path = std::env::temp_dir().join(format!(
|
||||
"rustdesk-clipboard-local-file-{}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::write(&file_path, b"")?;
|
||||
|
||||
let mut file = LocalFile::try_open(&std::env::temp_dir(), &file_path)?;
|
||||
file.size = 1;
|
||||
|
||||
let mut buf = [0u8; 1];
|
||||
assert!(file.read_exact_at(&mut buf, 0).is_err());
|
||||
assert!(file.handle.is_none());
|
||||
assert_eq!(file.offset.load(Ordering::Relaxed), 0);
|
||||
|
||||
std::fs::write(&file_path, [42u8])?;
|
||||
|
||||
file.read_exact_at(&mut buf, 0)?;
|
||||
assert_eq!(buf, [42u8]);
|
||||
assert!(file.handle.is_none());
|
||||
|
||||
std::fs::remove_file(file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use hbb_common::{
|
||||
log,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{path::PathBuf, sync::Arc, usize};
|
||||
use std::{path::PathBuf, sync::Arc, time::SystemTime, usize};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// local files are cached, this value should not be changed when copying files
|
||||
@@ -30,9 +30,35 @@ enum FileContentsRequest {
|
||||
},
|
||||
}
|
||||
|
||||
// Cheap fingerprint of one top-level selected entry. A change in size/mtime --
|
||||
// or a directory in the selection -- forces sync_files() to rebuild (see below).
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
struct FileSig {
|
||||
size: u64,
|
||||
mtime: Option<SystemTime>,
|
||||
is_dir: bool,
|
||||
}
|
||||
|
||||
// Stat the top-level selected paths only (no recursion), same order as `files`.
|
||||
fn fingerprint(files: &[String]) -> Vec<FileSig> {
|
||||
files
|
||||
.iter()
|
||||
.map(|s| match std::fs::metadata(s) {
|
||||
Ok(mt) => FileSig {
|
||||
size: mt.len(),
|
||||
mtime: mt.modified().ok(),
|
||||
is_dir: mt.is_dir(),
|
||||
},
|
||||
Err(_) => FileSig::default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClipFiles {
|
||||
files: Vec<String>,
|
||||
// Fingerprint of `files` (same len/order); detects in-place edits on re-copy.
|
||||
sigs: Vec<FileSig>,
|
||||
file_list: Vec<LocalFile>,
|
||||
first_file_index: usize,
|
||||
files_pdu: Vec<u8>,
|
||||
@@ -41,12 +67,17 @@ struct ClipFiles {
|
||||
impl ClipFiles {
|
||||
fn clear(&mut self) {
|
||||
self.files.clear();
|
||||
self.sigs.clear();
|
||||
self.file_list.clear();
|
||||
self.first_file_index = usize::MAX;
|
||||
self.files_pdu.clear();
|
||||
}
|
||||
|
||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
||||
fn sync_files(
|
||||
&mut self,
|
||||
clipboard_files: &[String],
|
||||
sigs: Vec<FileSig>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
let clipboard_paths = clipboard_files
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(s))
|
||||
@@ -58,6 +89,7 @@ impl ClipFiles {
|
||||
.position(|f| !f.path.is_dir())
|
||||
.unwrap_or(usize::MAX);
|
||||
self.files = clipboard_files.to_vec();
|
||||
self.sigs = sigs;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -258,14 +290,128 @@ pub fn read_file_contents(
|
||||
}
|
||||
|
||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||
// Dedup: skip the rebuild only when paths + sizes + mtimes match and no dir is
|
||||
// selected (a dir's own mtime doesn't change when a file inside it is edited).
|
||||
let current = fingerprint(files);
|
||||
let mut files_lock = CLIP_FILES.lock();
|
||||
if files_lock.files == files {
|
||||
if files_lock.files == files
|
||||
&& files_lock.sigs == current
|
||||
&& !current.iter().any(|sig| sig.is_dir)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
files_lock.sync_files(files)?;
|
||||
files_lock.sync_files(files, current)?;
|
||||
Ok(files_lock.build_file_list_pdu())
|
||||
}
|
||||
|
||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||
CLIP_FILES.lock().files_pdu.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sig_test {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Unique temp dir under the system temp dir; removed on drop (no dev-dep).
|
||||
struct TmpDir(PathBuf);
|
||||
impl TmpDir {
|
||||
fn new(tag: &str) -> Self {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let mut dir = std::env::temp_dir();
|
||||
dir.push(format!("rustdesk_sig_test_{}_{}", tag, nanos));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
TmpDir(dir)
|
||||
}
|
||||
fn join(&self, name: &str) -> PathBuf {
|
||||
self.0.join(name)
|
||||
}
|
||||
}
|
||||
impl Drop for TmpDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_str(p: &PathBuf) -> String {
|
||||
p.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_missing_path_is_default() {
|
||||
let tmp = TmpDir::new("missing");
|
||||
let missing = path_str(&tmp.join("does_not_exist.bin"));
|
||||
let sigs = fingerprint(&[missing]);
|
||||
assert_eq!(sigs.len(), 1);
|
||||
// A path that can't be stat'd -> default sig, which forces a rebuild.
|
||||
assert_eq!(sigs[0], FileSig::default());
|
||||
assert_eq!(sigs[0].mtime, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_detects_inplace_edit() {
|
||||
let tmp = TmpDir::new("edit");
|
||||
let file = tmp.join("a.bin");
|
||||
fs::write(&file, b"small").unwrap();
|
||||
let p = path_str(&file);
|
||||
|
||||
let before = fingerprint(&[p.clone()]);
|
||||
// Same content, same path: fingerprint must be stable.
|
||||
let again = fingerprint(&[p.clone()]);
|
||||
assert_eq!(before, again);
|
||||
assert_eq!(before[0].size, 5);
|
||||
assert!(!before[0].is_dir);
|
||||
|
||||
// Edit in place so the file grows.
|
||||
fs::write(&file, b"much larger contents than before").unwrap();
|
||||
let after = fingerprint(&[p]);
|
||||
assert_ne!(before, after);
|
||||
assert!(after[0].size > before[0].size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_flags_directory() {
|
||||
let tmp = TmpDir::new("dir");
|
||||
let sub = tmp.join("subdir");
|
||||
fs::create_dir_all(&sub).unwrap();
|
||||
let sigs = fingerprint(&[path_str(&sub)]);
|
||||
assert_eq!(sigs.len(), 1);
|
||||
assert!(sigs[0].is_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recopy_after_edit_refreshes_cached_size() {
|
||||
let tmp = TmpDir::new("recopy");
|
||||
let file = tmp.join("doc.bin");
|
||||
fs::write(&file, b"v1").unwrap(); // 2 bytes
|
||||
let files = vec![path_str(&file)];
|
||||
|
||||
// Drive the public, guarded `sync_files` over the global CLIP_FILES;
|
||||
// reset first (this is the only test that touches the global).
|
||||
clear_files();
|
||||
|
||||
sync_files(&files).unwrap();
|
||||
{
|
||||
let cache = CLIP_FILES.lock();
|
||||
let idx = cache.first_file_index;
|
||||
assert_eq!(cache.file_list[idx].size, 2);
|
||||
}
|
||||
|
||||
// In-place edit grows the file; the re-copy must rebuild. Pre-fix the
|
||||
// path-only guard early-returned and left the cached size stale at 2.
|
||||
fs::write(&file, b"v2 is bigger").unwrap(); // 12 bytes
|
||||
sync_files(&files).unwrap();
|
||||
{
|
||||
let cache = CLIP_FILES.lock();
|
||||
let idx = cache.first_file_index;
|
||||
assert_eq!(cache.file_list[idx].size, 12);
|
||||
}
|
||||
|
||||
clear_files(); // leave the global clean for other tests
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,14 @@
|
||||
|
||||
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
|
||||
#define WF_CLIPRDR_MAX_STREAMS 16384
|
||||
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
|
||||
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
|
||||
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
|
||||
/* Registered format names are string atoms; cap the converted WCHAR name.
|
||||
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
|
||||
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
|
||||
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
|
||||
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)
|
||||
|
||||
/* Validates the remote descriptor array size after cItems has been read safely. */
|
||||
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
||||
@@ -61,6 +69,25 @@ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
||||
return size >= descriptors_size;
|
||||
}
|
||||
|
||||
static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (!value || !len)
|
||||
return FALSE;
|
||||
|
||||
for (i = 0; i <= max_len; i++)
|
||||
{
|
||||
if (value[i] == '\0')
|
||||
{
|
||||
*len = i;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clipboard Formats
|
||||
*/
|
||||
@@ -205,6 +232,7 @@ struct _CliprdrStream
|
||||
FILEDESCRIPTORW m_Dsc;
|
||||
void *m_pData;
|
||||
UINT32 m_connID;
|
||||
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
|
||||
};
|
||||
typedef struct _CliprdrStream CliprdrStream;
|
||||
|
||||
@@ -258,6 +286,9 @@ struct wf_clipboard
|
||||
char *req_fdata;
|
||||
HANDLE req_fevent;
|
||||
BOOL req_f_received;
|
||||
UINT32 req_f_conn_id_expected; // connID of the outstanding request
|
||||
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
|
||||
LONG req_f_stream_id_seq; // source of unique per-stream ids
|
||||
|
||||
size_t nFiles;
|
||||
size_t file_array_size;
|
||||
@@ -288,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
|
||||
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
||||
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
||||
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
|
||||
ULONG index, UINT32 flag, DWORD positionhigh,
|
||||
DWORD positionlow, ULONG request);
|
||||
|
||||
@@ -371,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
|
||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
||||
ULONG *pcbRead)
|
||||
{
|
||||
int ret;
|
||||
UINT ret;
|
||||
CliprdrStream *instance = (CliprdrStream *)This;
|
||||
wfClipboard *clipboard;
|
||||
|
||||
@@ -384,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
||||
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
||||
return S_FALSE;
|
||||
|
||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
|
||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
|
||||
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
||||
instance->m_lOffset.LowPart, cb);
|
||||
|
||||
if (ret < 0)
|
||||
if (ret != CHANNEL_RC_OK)
|
||||
{
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
if (clipboard->req_fsize > cb)
|
||||
{
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
return STG_E_READFAULT;
|
||||
}
|
||||
|
||||
if (clipboard->req_fdata)
|
||||
{
|
||||
@@ -601,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
instance->m_pData = pData;
|
||||
instance->m_lOffset.QuadPart = 0;
|
||||
instance->m_connID = connID;
|
||||
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
|
||||
|
||||
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
||||
{
|
||||
@@ -611,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
||||
{
|
||||
/* get content size of this stream */
|
||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
|
||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
|
||||
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
||||
8) == CHANNEL_RC_OK)
|
||||
{
|
||||
success = TRUE;
|
||||
}
|
||||
|
||||
if (clipboard->req_fdata != NULL)
|
||||
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
|
||||
{
|
||||
LONGLONG sz = 0;
|
||||
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
|
||||
if (sz < 0)
|
||||
success = FALSE;
|
||||
else
|
||||
instance->m_lSize.QuadPart = sz;
|
||||
}
|
||||
else
|
||||
{
|
||||
success = FALSE;
|
||||
}
|
||||
if (clipboard->req_fdata)
|
||||
{
|
||||
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
|
||||
free(clipboard->req_fdata);
|
||||
clipboard->req_fdata = NULL;
|
||||
}
|
||||
@@ -1406,25 +1461,35 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format)
|
||||
return local_format;
|
||||
}
|
||||
|
||||
static void map_ensure_capacity(wfClipboard *clipboard)
|
||||
static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
|
||||
{
|
||||
size_t old_size;
|
||||
formatMapping *new_map;
|
||||
|
||||
if (!clipboard)
|
||||
return;
|
||||
return FALSE;
|
||||
|
||||
if (clipboard->map_size >= clipboard->map_capacity)
|
||||
{
|
||||
size_t new_size;
|
||||
formatMapping *new_map;
|
||||
new_size = clipboard->map_capacity * 2;
|
||||
new_map =
|
||||
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
|
||||
if (!clipboard->format_mappings)
|
||||
return FALSE;
|
||||
|
||||
if (!new_map)
|
||||
return;
|
||||
if (capacity <= clipboard->map_capacity)
|
||||
return TRUE;
|
||||
|
||||
clipboard->format_mappings = new_map;
|
||||
clipboard->map_capacity = new_size;
|
||||
}
|
||||
if (capacity > WF_CLIPRDR_MAX_FORMATS ||
|
||||
capacity > ((size_t)-1) / sizeof(formatMapping))
|
||||
return FALSE;
|
||||
|
||||
old_size = clipboard->map_capacity;
|
||||
new_map =
|
||||
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);
|
||||
|
||||
if (!new_map)
|
||||
return FALSE;
|
||||
|
||||
memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
|
||||
clipboard->format_mappings = new_map;
|
||||
clipboard->map_capacity = capacity;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL clear_format_map(wfClipboard *clipboard)
|
||||
@@ -1451,6 +1516,13 @@ static BOOL clear_format_map(wfClipboard *clipboard)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
|
||||
{
|
||||
clear_format_map(clipboard);
|
||||
clipboard->copied = FALSE;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
||||
{
|
||||
CLIPRDR_TEMP_DIRECTORY tempDirectory;
|
||||
@@ -1729,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||
}
|
||||
|
||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
|
||||
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
||||
ULONG nreq)
|
||||
{
|
||||
UINT rc;
|
||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
|
||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
|
||||
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
@@ -1745,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
||||
return rc;
|
||||
}
|
||||
clipboard->req_f_received = FALSE;
|
||||
clipboard->req_f_conn_id_expected = connID;
|
||||
clipboard->req_f_stream_id_expected = streamId;
|
||||
|
||||
fileContentsRequest.connID = connID;
|
||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
||||
// But it is OK, because it is only used to check if the stream is the same in
|
||||
// `wf_cliprdr_server_file_contents_request()` function.
|
||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
||||
fileContentsRequest.streamId = streamId;
|
||||
fileContentsRequest.listIndex = index;
|
||||
fileContentsRequest.dwFlags = flag;
|
||||
fileContentsRequest.nPositionLow = positionlow;
|
||||
@@ -2443,6 +2514,16 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
|
||||
if (!clear_format_map(clipboard))
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
clipboard->copied = FALSE;
|
||||
|
||||
if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
if (formatList->numFormats > 0 && !formatList->formats)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
if (!map_ensure_capacity(clipboard, formatList->numFormats))
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
clipboard->copied = TRUE;
|
||||
|
||||
@@ -2450,19 +2531,58 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
{
|
||||
format = &(formatList->formats[i]);
|
||||
mapping = &(clipboard->format_mappings[i]);
|
||||
/* Do not validate the peer-provided formatId as a Windows registered format.
|
||||
* It is only a remote protocol ID used when requesting data from the peer.
|
||||
* For named formats, RegisterClipboardFormatW creates the local Windows
|
||||
* clipboard ID below, and that local ID is checked before publishing. */
|
||||
mapping->remote_format_id = format->formatId;
|
||||
|
||||
if (format->formatName)
|
||||
{
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName,
|
||||
strlen(format->formatName), NULL, 0);
|
||||
mapping->name = calloc(size + 1, sizeof(WCHAR));
|
||||
size_t name_len;
|
||||
int size;
|
||||
|
||||
if (mapping->name)
|
||||
if (!wf_cliprdr_bounded_strlen(format->formatName,
|
||||
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
|
||||
{
|
||||
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName),
|
||||
mapping->name, size);
|
||||
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
if (name_len == 0)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||
NULL, 0);
|
||||
if (size <= 0)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
|
||||
if (!mapping->name)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||
mapping->name, size) != size)
|
||||
{
|
||||
free(mapping->name);
|
||||
mapping->name = NULL;
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
|
||||
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
||||
if (mapping->local_format_id == 0)
|
||||
{
|
||||
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2472,7 +2592,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
}
|
||||
|
||||
clipboard->map_size++;
|
||||
map_ensure_capacity(clipboard);
|
||||
}
|
||||
|
||||
if (file_transferring(clipboard))
|
||||
@@ -2923,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
BOOL bIsStreamFile = TRUE;
|
||||
static LPSTREAM pStreamStc = NULL;
|
||||
static UINT32 uStreamIdStc = 0;
|
||||
static UINT32 uConnIdStc = 0;
|
||||
wfClipboard *clipboard;
|
||||
UINT rc = ERROR_INTERNAL_ERROR;
|
||||
UINT sRc;
|
||||
@@ -2996,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
||||
vFormatEtc.ptd = NULL;
|
||||
|
||||
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
|
||||
if ((uStreamIdStc != fileContentsRequest->streamId) ||
|
||||
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
|
||||
{
|
||||
LPENUMFORMATETC pEnumFormatEtc;
|
||||
ULONG CeltFetched;
|
||||
@@ -3027,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
{
|
||||
pStreamStc = vStgMedium.pstm;
|
||||
uStreamIdStc = fileContentsRequest->streamId;
|
||||
uConnIdStc = fileContentsRequest->connID;
|
||||
bIsStreamFile = TRUE;
|
||||
}
|
||||
|
||||
@@ -3190,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
|
||||
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
|
||||
return CHANNEL_RC_OK;
|
||||
clipboard->req_fsize = 0;
|
||||
clipboard->req_fdata = NULL;
|
||||
|
||||
@@ -3200,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
}
|
||||
|
||||
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
||||
/*
|
||||
* Keep the zero-size allocation: supported Windows builds use the Microsoft
|
||||
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
|
||||
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
|
||||
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
|
||||
* nonzero length cannot have a NULL data pointer on the normal call path.
|
||||
*/
|
||||
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
||||
if (!clipboard->req_fdata)
|
||||
{
|
||||
|
||||
@@ -113,11 +113,11 @@ pub enum MouseButton {
|
||||
|
||||
/// Scroll up button
|
||||
ScrollUp,
|
||||
/// Left right button
|
||||
/// Scroll down button
|
||||
ScrollDown,
|
||||
/// Left right button
|
||||
/// Scroll left button
|
||||
ScrollLeft,
|
||||
/// Left right button
|
||||
/// Scroll right button
|
||||
ScrollRight,
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ impl KeyboardControllable for Enigo {
|
||||
// Windows uses uft-16 encoding. We need to check
|
||||
// for variable length characters. As such some
|
||||
// characters can be 32 bit long and those are
|
||||
// encoded in such called hight and low surrogates
|
||||
// encoded in so-called high and low surrogates
|
||||
// each 16 bit wide that needs to be send after
|
||||
// another to the SendInput function without
|
||||
// being interrupted by "keyup"
|
||||
|
||||
+1
-1
Submodule libs/hbb_common updated: 387603f47c...7e1c392c62
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.8"
|
||||
version = "1.4.9"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
|
||||
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
||||
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
OriginalFilename = "rustdesk.exe"
|
||||
FileDescription = "RustDesk Remote Desktop"
|
||||
|
||||
@@ -79,6 +79,10 @@ mod webrtc {
|
||||
}
|
||||
}
|
||||
|
||||
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
|
||||
(threads as f64).log2().ceil() as _
|
||||
}
|
||||
|
||||
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
||||
use aom_superblock_size::*;
|
||||
let resolution = width * height;
|
||||
@@ -160,8 +164,7 @@ mod webrtc {
|
||||
} else {
|
||||
AV1E_SET_TILE_COLUMNS
|
||||
};
|
||||
// Failed on android
|
||||
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
|
||||
call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
|
||||
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
||||
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
||||
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
||||
@@ -197,6 +200,23 @@ mod webrtc {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::raw::c_uint;
|
||||
|
||||
#[test]
|
||||
fn tile_log2_uses_c_uint_and_rounds_up() {
|
||||
let one_thread: c_uint = tile_log2(1);
|
||||
let three_threads: c_uint = tile_log2(3);
|
||||
let max_threads: c_uint = tile_log2(64);
|
||||
|
||||
assert_eq!(one_thread, 0);
|
||||
assert_eq!(three_threads, 2);
|
||||
assert_eq!(max_threads, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderApi for AomEncoder {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.4.8
|
||||
pkgver=1.4.9
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -79,7 +79,7 @@ heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\li
|
||||
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
|
||||
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
|
||||
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
|
||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Tech Pte. Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
|
||||
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
|
||||
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
|
||||
@@ -300,4 +300,4 @@ b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a6
|
||||
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
|
||||
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
|
||||
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
|
||||
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
|
||||
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
|
||||
|
||||
@@ -85,7 +85,7 @@ def make_parser():
|
||||
"-m",
|
||||
"--manufacturer",
|
||||
type=str,
|
||||
default="PURSLANE",
|
||||
default="Purslane Tech Pte. Ltd.",
|
||||
help="The app manufacturer.",
|
||||
)
|
||||
return parser
|
||||
@@ -499,7 +499,7 @@ def update_license_file(app_name):
|
||||
license_content = f.read()
|
||||
license_content = license_content.replace("website rustdesk.com and other ", "")
|
||||
license_content = license_content.replace("RustDesk", app_name)
|
||||
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
|
||||
license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
|
||||
with open(license_file, "w", encoding="utf-8") as f:
|
||||
f.write(license_content)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.8
|
||||
Version: 1.4.9
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.8
|
||||
Version: 1.4.9
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.8
|
||||
Version: 1.4.9
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ pre_start()
|
||||
return 0
|
||||
}
|
||||
|
||||
# When loging out from the interactive shell, the execution sequence is:
|
||||
# When logging out from the interactive shell, the execution sequence is:
|
||||
#
|
||||
# IF ~/.bash_logout exists THEN
|
||||
# execute ~/.bash_logout
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE arm)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=armv7a-linux-androideabi")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=armeabi-v7a -DANDROID_ARM_NEON=ON)
|
||||
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE arm64)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=aarch64-linux-android")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=arm64-v8a)
|
||||
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE x64)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=x86_64-linux-android")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86_64)
|
||||
@@ -0,0 +1,7 @@
|
||||
set(VCPKG_TARGET_ARCHITECTURE x86)
|
||||
set(VCPKG_CRT_LINKAGE dynamic)
|
||||
set(VCPKG_LIBRARY_LINKAGE static)
|
||||
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||
set(VCPKG_MAKE_BUILD_TRIPLET "--host=i686-linux-android")
|
||||
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86)
|
||||
-199
@@ -1,199 +0,0 @@
|
||||
use crate::client::*;
|
||||
use async_trait::async_trait;
|
||||
use hbb_common::{
|
||||
config::PeerConfig,
|
||||
config::READ_TIMEOUT,
|
||||
futures::{SinkExt, StreamExt},
|
||||
log,
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
rendezvous_proto::ConnType,
|
||||
tokio::{self, sync::mpsc},
|
||||
Stream,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
id: String,
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
sender: mpsc::UnboundedSender<Data>,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
||||
let mut password = "".to_owned();
|
||||
if PeerConfig::load(id).password.is_empty() {
|
||||
match rpassword::prompt_password("Enter password: ") {
|
||||
Ok(p) => password = p,
|
||||
Err(e) => {
|
||||
log::error!("Failed to read password: {:?}", e);
|
||||
password = "".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
let session = Self {
|
||||
id: id.to_owned(),
|
||||
sender,
|
||||
password,
|
||||
lc: Default::default(),
|
||||
};
|
||||
session.lc.write().unwrap().initialize(
|
||||
id.to_owned(),
|
||||
ConnType::PORT_FORWARD,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
session
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Interface for Session {
|
||||
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
||||
return self.lc.clone();
|
||||
}
|
||||
|
||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
|
||||
match msgtype {
|
||||
"input-password" => {
|
||||
self.sender
|
||||
.send(Data::Login((self.password.clone(), true)))
|
||||
.ok();
|
||||
}
|
||||
"re-input-password" => {
|
||||
log::error!("{}: {}", title, text);
|
||||
match rpassword::prompt_password("Enter password: ") {
|
||||
Ok(password) => {
|
||||
let login_data = Data::Login((password, true));
|
||||
self.sender.send(login_data).ok();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("reinput password failed, {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
msg if msg.contains("error") => {
|
||||
log::error!("{}: {}: {}", msgtype, title, text);
|
||||
}
|
||||
_ => {
|
||||
log::info!("{}: {}: {}", msgtype, title, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_login_error(&self, err: &str) -> bool {
|
||||
handle_login_error(self.lc.clone(), err, self)
|
||||
}
|
||||
|
||||
fn handle_peer_info(&self, pi: PeerInfo) {
|
||||
self.lc.write().unwrap().handle_peer_info(&pi);
|
||||
}
|
||||
|
||||
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
|
||||
log::info!(
|
||||
"password={}",
|
||||
hbb_common::password_security::temporary_password()
|
||||
);
|
||||
handle_hash(self.lc.clone(), &pass, hash, self, peer).await;
|
||||
}
|
||||
|
||||
async fn handle_login_from_ui(
|
||||
&self,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: String,
|
||||
remember: bool,
|
||||
peer: &mut Stream,
|
||||
) {
|
||||
handle_login_from_ui(
|
||||
self.lc.clone(),
|
||||
os_username,
|
||||
os_password,
|
||||
password,
|
||||
remember,
|
||||
peer,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream) {
|
||||
handle_test_delay(t, peer).await;
|
||||
}
|
||||
|
||||
fn send(&self, data: Data) {
|
||||
self.sender.send(data).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn connect_test(id: &str, key: String, token: String) {
|
||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
||||
let handler = Session::new(&id, sender);
|
||||
match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await {
|
||||
Err(err) => {
|
||||
log::error!("Failed to connect {}: {}", &id, err);
|
||||
}
|
||||
Ok((mut stream, direct)) => {
|
||||
log::info!("direct: {}", direct);
|
||||
// rpassword::prompt_password("Input anything to exit").ok();
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res {
|
||||
Err(_) => {
|
||||
log::error!("Timeout");
|
||||
break;
|
||||
}
|
||||
Ok(Some(Ok(bytes))) => {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
match msg_in.union {
|
||||
Some(message::Union::Hash(hash)) => {
|
||||
log::info!("Got hash");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn start_one_port_forward(
|
||||
id: String,
|
||||
port: i32,
|
||||
remote_host: String,
|
||||
remote_port: i32,
|
||||
key: String,
|
||||
token: String,
|
||||
) {
|
||||
crate::common::test_rendezvous_server();
|
||||
crate::common::test_nat_type();
|
||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
||||
let handler = Session::new(&id, sender);
|
||||
if let Err(err) = crate::port_forward::listen(
|
||||
handler.id.clone(),
|
||||
handler.password.clone(),
|
||||
port,
|
||||
handler.clone(),
|
||||
receiver,
|
||||
&key,
|
||||
&token,
|
||||
handler.lc.clone(),
|
||||
remote_host,
|
||||
remote_port,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to listen on {}: {}", port, err);
|
||||
}
|
||||
log::info!("port forward (:{}) exit", port);
|
||||
}
|
||||
+33
-8
@@ -941,21 +941,23 @@ impl Client {
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
fn try_stop_clipboard() {
|
||||
// There's a bug here.
|
||||
// If session is closed by the peer, `has_sessions_running()` will always return true.
|
||||
// It's better to check if the active session number.
|
||||
// But it's not a problem, because the clipboard thread does not consume CPU.
|
||||
//
|
||||
// If we want to fix it, we can add a flag to indicate if session is active.
|
||||
// But I think it's not necessary to introduce complexity at this point.
|
||||
// Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions
|
||||
// should block clipboard cleanup.
|
||||
#[cfg(feature = "flutter")]
|
||||
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
|
||||
if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
|
||||
CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||
if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync(
|
||||
crate::clipboard::ClipboardSide::Client,
|
||||
0,
|
||||
) {
|
||||
log::error!("Failed to empty client clipboard files: {}", e);
|
||||
}
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||
clipboard::platform::unix::fuse::uninit_fuse_context(true);
|
||||
}
|
||||
|
||||
@@ -3790,6 +3792,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
||||
#[derive(Clone)]
|
||||
pub enum Data {
|
||||
Close,
|
||||
RejectInsecureConnection,
|
||||
Login((String, String, String, bool)),
|
||||
Message(Message),
|
||||
SendFiles((i32, JobType, String, String, i32, bool, bool)),
|
||||
@@ -3813,11 +3816,33 @@ pub enum Data {
|
||||
ElevateWithLogon(String, String),
|
||||
NewVoiceCall,
|
||||
CloseVoiceCall,
|
||||
ContinueInsecureConnection,
|
||||
ResetDecoder(Option<usize>),
|
||||
RenameFile((i32, String, String, bool)),
|
||||
TakeScreenshot((i32, String)),
|
||||
}
|
||||
|
||||
pub async fn confirm_insecure_connection(
|
||||
interface: &impl Interface,
|
||||
receiver: &mut UnboundedReceiver<Data>,
|
||||
) -> bool {
|
||||
interface.msgbox(
|
||||
"insecure-connection-nocancel-hasclose",
|
||||
"Insecure Connection",
|
||||
"conn-e2ee-unavailable-tip",
|
||||
"",
|
||||
);
|
||||
while let Some(data) = receiver.recv().await {
|
||||
match data {
|
||||
Data::ContinueInsecureConnection => return true,
|
||||
Data::RejectInsecureConnection => return false,
|
||||
Data::Close => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Keycode for key events.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Key {
|
||||
|
||||
@@ -6,7 +6,6 @@ pub trait FileManager: Interface {
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
feature = "cli",
|
||||
feature = "flutter"
|
||||
)))]
|
||||
fn get_home_dir(&self) -> String {
|
||||
@@ -16,7 +15,6 @@ pub trait FileManager: Interface {
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
feature = "cli",
|
||||
feature = "flutter"
|
||||
)))]
|
||||
fn get_next_job_id(&self) -> i32 {
|
||||
@@ -26,7 +24,6 @@ pub trait FileManager: Interface {
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
feature = "cli",
|
||||
feature = "flutter"
|
||||
)))]
|
||||
fn update_next_job_id(&self, id: i32) {
|
||||
@@ -36,7 +33,6 @@ pub trait FileManager: Interface {
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
feature = "cli",
|
||||
feature = "flutter"
|
||||
)))]
|
||||
fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value {
|
||||
@@ -90,7 +86,6 @@ pub trait FileManager: Interface {
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
feature = "cli",
|
||||
feature = "flutter"
|
||||
)))]
|
||||
fn confirm_delete_files(&self, id: i32, file_num: i32) {
|
||||
@@ -100,7 +95,6 @@ pub trait FileManager: Interface {
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
feature = "cli",
|
||||
feature = "flutter"
|
||||
)))]
|
||||
fn set_no_confirm(&self, id: i32) {
|
||||
|
||||
+27
-2
@@ -14,6 +14,7 @@ use crate::{
|
||||
// Empirical no-data window before exposing the restart reconnect state to the UI.
|
||||
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
|
||||
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30);
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||
#[cfg(any(
|
||||
@@ -183,8 +184,20 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_connected();
|
||||
let is_secured = peer.is_secured();
|
||||
self.handler
|
||||
.set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready
|
||||
.set_connection_type(is_secured, direct, stream_type); // flutter -> connection_ready
|
||||
if !is_secured
|
||||
&& !crate::common::is_direct_ip_access(&self.handler.get_id())
|
||||
&& !client::confirm_insecure_connection(&self.handler, &mut self.receiver).await
|
||||
{
|
||||
self.send_close_reason(&mut peer, "").await;
|
||||
if kcp.is_some() {
|
||||
tokio::time::sleep(KCP_CLOSE_REASON_FLUSH_DELAY).await;
|
||||
}
|
||||
self.handle_disconnected(round);
|
||||
return;
|
||||
}
|
||||
self.handler.update_direct(Some(direct));
|
||||
if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA {
|
||||
self.handler
|
||||
@@ -338,13 +351,17 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.send_close_reason(&mut peer, "kcp").await;
|
||||
// KCP does not send messages immediately, so wait to ensure the last message is sent.
|
||||
// 1ms works in my test, but 30ms is more reliable.
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
tokio::time::sleep(KCP_CLOSE_REASON_FLUSH_DELAY).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.handler.on_establish_connection_error(err.to_string());
|
||||
}
|
||||
}
|
||||
self.handle_disconnected(round);
|
||||
}
|
||||
|
||||
fn handle_disconnected(&self, round: u32) {
|
||||
// set_disconnected_ok is used to check if new connection round is started.
|
||||
let _set_disconnected_ok = self
|
||||
.handler
|
||||
@@ -360,6 +377,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
if self.handler.is_default() && _set_disconnected_ok {
|
||||
// Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is
|
||||
// unmounted. Keep this async path for other file-clipboard platforms.
|
||||
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
|
||||
}
|
||||
}
|
||||
@@ -1088,6 +1107,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
|
||||
if self.handler.is_view_camera() {
|
||||
return;
|
||||
}
|
||||
if !self.peer_info.is_support_virtual_display() {
|
||||
return;
|
||||
}
|
||||
@@ -1109,6 +1131,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) {
|
||||
if self.handler.is_view_camera() {
|
||||
return;
|
||||
}
|
||||
let lc = self.handler.lc.read().unwrap();
|
||||
if lc.version >= hbb_common::get_version_number("1.2.4")
|
||||
&& lc.get_toggle_option("privacy-mode")
|
||||
|
||||
+40
-31
@@ -151,41 +151,50 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
||||
std::thread::spawn(move || {
|
||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||
if ctx.is_none() {
|
||||
match ClipboardContext::new() {
|
||||
Ok(x) => {
|
||||
*ctx = Some(x);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create clipboard context: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
if let Some(mut ctx) = ctx.as_mut() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use clipboard::platform::unix;
|
||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
// No need to make sure the context is enabled.
|
||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.empty_clipboard(_conn_id).ok();
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
|
||||
log::error!("Failed to empty clipboard files: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> {
|
||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||
if ctx.is_none() {
|
||||
match ClipboardContext::new() {
|
||||
Ok(x) => {
|
||||
*ctx = Some(x);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create clipboard context: {}", e);
|
||||
bail!("Failed to create clipboard context: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
if let Some(mut ctx) = ctx.as_mut() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use clipboard::platform::unix;
|
||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
// No need to make sure the context is enabled.
|
||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||
if !context.empty_clipboard(_conn_id)? {
|
||||
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
|
||||
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);
|
||||
|
||||
+20
-12
@@ -332,12 +332,16 @@ pub mod unix_file_clip {
|
||||
log::debug!("format data response: msg_flags: {}", msg_flags);
|
||||
|
||||
if msg_flags != 0x1 {
|
||||
// return failure message?
|
||||
log::error!(
|
||||
"peer reported clipboard format data failure: {}",
|
||||
msg_flags
|
||||
);
|
||||
return vec![];
|
||||
}
|
||||
|
||||
log::debug!("parsing file descriptors");
|
||||
if fuse::init_fuse_context(true).is_ok() {
|
||||
match fuse::format_data_response_to_urls(
|
||||
match fuse::init_fuse_context(side == ClipboardSide::Client) {
|
||||
Ok(()) => match fuse::format_data_response_to_urls(
|
||||
side == ClipboardSide::Client,
|
||||
format_data,
|
||||
conn_id,
|
||||
@@ -348,9 +352,10 @@ pub mod unix_file_clip {
|
||||
Err(e) => {
|
||||
log::error!("failed to parse file descriptors: {:?}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("failed to initialize clipboard FUSE context: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
// send error message to server
|
||||
}
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
@@ -386,6 +391,7 @@ pub mod unix_file_clip {
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
..
|
||||
} => {
|
||||
log::debug!(
|
||||
@@ -393,13 +399,15 @@ pub mod unix_file_clip {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
);
|
||||
if fuse::init_fuse_context(true).is_ok() {
|
||||
hbb_common::allow_err!(fuse::handle_file_content_response(
|
||||
side == ClipboardSide::Client,
|
||||
clip
|
||||
));
|
||||
} else {
|
||||
// send error message to server
|
||||
let response = ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
};
|
||||
if let Err(e) =
|
||||
fuse::handle_file_content_response(side == ClipboardSide::Client, response)
|
||||
{
|
||||
log::error!("failed to handle file contents response: {:?}", e);
|
||||
}
|
||||
}
|
||||
ClipboardFile::NotifyCallback {
|
||||
|
||||
+6
-3
@@ -764,15 +764,14 @@ async fn test_rendezvous_server_() {
|
||||
Config::reset_online();
|
||||
}
|
||||
|
||||
// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
pub fn test_rendezvous_server() {
|
||||
std::thread::spawn(test_rendezvous_server_);
|
||||
}
|
||||
|
||||
pub fn refresh_rendezvous_server() {
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
test_rendezvous_server();
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
std::thread::spawn(|| {
|
||||
if crate::ipc::test_rendezvous_server().is_err() {
|
||||
test_rendezvous_server();
|
||||
@@ -2620,6 +2619,10 @@ pub fn get_control_permission(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_direct_ip_access(peer: &str) -> bool {
|
||||
hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+15
-2
@@ -1437,7 +1437,7 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
|
||||
pub fn update_text_clipboard_required() {
|
||||
let is_required = sessions::get_sessions()
|
||||
.iter()
|
||||
.any(|s| s.is_text_clipboard_required());
|
||||
.any(|s| s.is_default() && s.is_text_clipboard_required());
|
||||
#[cfg(target_os = "android")]
|
||||
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
|
||||
Client::set_is_text_clipboard_required(is_required);
|
||||
@@ -1447,13 +1447,16 @@ pub fn update_text_clipboard_required() {
|
||||
pub fn update_file_clipboard_required() {
|
||||
let is_required = sessions::get_sessions()
|
||||
.iter()
|
||||
.any(|s| s.is_file_clipboard_required());
|
||||
.any(|s| s.is_default() && s.is_file_clipboard_required());
|
||||
Client::set_is_file_clipboard_required(is_required);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
||||
for s in sessions::get_sessions() {
|
||||
if !s.is_default() {
|
||||
continue;
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if _is_file {
|
||||
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
|
||||
@@ -2297,6 +2300,16 @@ pub mod sessions {
|
||||
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn has_connected_sessions_running(conn_type: ConnType) -> bool {
|
||||
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
|
||||
*r#type == conn_type
|
||||
&& s.session_handlers.read().unwrap().len() != 0
|
||||
&& s.connection_round_state.lock().unwrap().is_connected()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) mod async_tasks {
|
||||
|
||||
+29
-3
@@ -1026,7 +1026,7 @@ pub fn main_set_option(key: String, value: String) {
|
||||
set_option(key, value.clone());
|
||||
#[cfg(target_os = "android")]
|
||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
crate::common::test_rendezvous_server();
|
||||
} else {
|
||||
set_option(key, value.clone());
|
||||
@@ -1224,9 +1224,14 @@ pub fn main_set_local_option(key: String, value: String) {
|
||||
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
||||
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
||||
set_local_option(key, value.clone());
|
||||
let is_render_target =
|
||||
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
||||
if is_texture_render_key {
|
||||
let session_event = [("v", &value)];
|
||||
for session in sessions::get_sessions() {
|
||||
if !is_render_target(&session) {
|
||||
continue;
|
||||
}
|
||||
session.push_event("use_texture_render", &session_event, &[]);
|
||||
session.use_texture_render_changed();
|
||||
session.ui_handler.update_use_texture_render();
|
||||
@@ -1234,6 +1239,9 @@ pub fn main_set_local_option(key: String, value: String) {
|
||||
}
|
||||
if is_d3d_render_key {
|
||||
for session in sessions::get_sessions() {
|
||||
if !is_render_target(&session) {
|
||||
continue;
|
||||
}
|
||||
session.update_supported_decodings();
|
||||
}
|
||||
}
|
||||
@@ -2852,8 +2860,16 @@ pub fn main_get_common(key: String) -> String {
|
||||
crate::platform::windows::is_msi_installed(),
|
||||
crate::common::is_custom_client(),
|
||||
) {
|
||||
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"),
|
||||
(Ok(true), false) => match crate::platform::windows::release_arch_suffix() {
|
||||
Some(arch) => format!("rustdesk-{_version}-{arch}.msi"),
|
||||
None => "error:unsupported".to_owned(),
|
||||
},
|
||||
(Ok(true), true) | (Ok(false), _) => {
|
||||
match crate::platform::windows::release_arch_suffix() {
|
||||
Some(arch) => format!("rustdesk-{_version}-{arch}.exe"),
|
||||
None => "error:unsupported".to_owned(),
|
||||
}
|
||||
}
|
||||
(Err(e), _) => {
|
||||
log::error!("Failed to check if is msi: {}", e);
|
||||
format!("error:update-failed-check-msi-tip")
|
||||
@@ -3012,6 +3028,16 @@ pub fn main_set_common(_key: String, _value: String) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_set_common(session_id: SessionID, key: String, value: String) {
|
||||
if let Some(s) = sessions::get_session_by_session_id(&session_id) {
|
||||
if key == "continue-insecure-connection"
|
||||
{
|
||||
s.continue_insecure_connection(value == "Y");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_common_sync(
|
||||
session_id: SessionID,
|
||||
key: String,
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ mod http_client;
|
||||
pub mod record_upload;
|
||||
pub mod sync;
|
||||
pub use http_client::{
|
||||
create_http_client_async, create_http_client_async_with_url, create_http_client_with_url,
|
||||
get_url_for_tls,
|
||||
create_http_client_async, create_http_client_async_with_url_strict,
|
||||
create_http_client_with_url, create_http_client_with_url_strict, get_url_for_tls,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::create_http_client_async_with_url;
|
||||
use super::create_http_client_async_with_url_strict;
|
||||
use hbb_common::{
|
||||
bail,
|
||||
lazy_static::lazy_static,
|
||||
@@ -167,7 +167,7 @@ async fn do_download(
|
||||
auto_del_dur: Option<Duration>,
|
||||
mut rx_cancel: UnboundedReceiver<()>,
|
||||
) -> ResultType<bool> {
|
||||
let client = create_http_client_async_with_url(&url).await;
|
||||
let client = create_http_client_async_with_url_strict(&url).await?;
|
||||
|
||||
let mut is_all_downloaded = false;
|
||||
tokio::select! {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use hbb_common::{
|
||||
async_recursion::async_recursion,
|
||||
bail,
|
||||
config::{Config, Socks5Server},
|
||||
log::{self, info},
|
||||
proxy::{Proxy, ProxyScheme},
|
||||
@@ -7,6 +8,7 @@ use hbb_common::{
|
||||
get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache,
|
||||
TlsType,
|
||||
},
|
||||
ResultType,
|
||||
};
|
||||
use reqwest::{blocking::Client as SyncClient, Client as AsyncClient};
|
||||
|
||||
@@ -137,6 +139,32 @@ pub fn create_http_client_with_url(url: &str) -> SyncClient {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_http_client_with_url_strict(url: &str) -> ResultType<SyncClient> {
|
||||
let parsed_url = url::Url::parse(url)?;
|
||||
if parsed_url.scheme() != "https" {
|
||||
bail!("Strict HTTP client requires HTTPS: {}", url);
|
||||
}
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let cached_tls_type = get_cached_tls_type(tls_url);
|
||||
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let can_reuse_cached_probe =
|
||||
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
|
||||
let tls_type = if can_reuse_cached_probe {
|
||||
cached_tls_type.unwrap_or(TlsType::Rustls)
|
||||
} else {
|
||||
TlsType::Rustls
|
||||
};
|
||||
Ok(create_http_client_with_url_(
|
||||
url,
|
||||
tls_url,
|
||||
tls_type,
|
||||
can_reuse_cached_probe,
|
||||
Some(false),
|
||||
Some(false),
|
||||
))
|
||||
}
|
||||
|
||||
fn create_http_client_with_url_(
|
||||
url: &str,
|
||||
tls_url: &str,
|
||||
@@ -247,6 +275,33 @@ pub async fn create_http_client_async_with_url(url: &str) -> AsyncClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_http_client_async_with_url_strict(url: &str) -> ResultType<AsyncClient> {
|
||||
let parsed_url = url::Url::parse(url)?;
|
||||
if parsed_url.scheme() != "https" {
|
||||
bail!("Strict HTTP client requires HTTPS: {}", url);
|
||||
}
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let cached_tls_type = get_cached_tls_type(tls_url);
|
||||
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let can_reuse_cached_probe =
|
||||
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
|
||||
let tls_type = if can_reuse_cached_probe {
|
||||
cached_tls_type.unwrap_or(TlsType::Rustls)
|
||||
} else {
|
||||
TlsType::Rustls
|
||||
};
|
||||
Ok(create_http_client_async_with_url_(
|
||||
url,
|
||||
tls_url,
|
||||
tls_type,
|
||||
can_reuse_cached_probe,
|
||||
Some(false),
|
||||
Some(false),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn create_http_client_async_with_url_(
|
||||
url: &str,
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@
|
||||
use crate::flutter;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk};
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
use crate::ui::CUR_SESSION;
|
||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -469,7 +469,7 @@ static mut IS_LEFT_OPTION_DOWN: bool = false;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn get_keyboard_mode() -> String {
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||
return session.get_keyboard_mode();
|
||||
}
|
||||
@@ -991,7 +991,7 @@ pub fn event_to_key_events(
|
||||
}
|
||||
|
||||
pub fn send_key_event(key_event: &KeyEvent) {
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||
session.send_key_event(key_event);
|
||||
}
|
||||
@@ -1003,7 +1003,7 @@ pub fn send_key_event(key_event: &KeyEvent) {
|
||||
}
|
||||
|
||||
pub fn get_peer_platform() -> String {
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||
return session.peer_platform();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "اتصال الوسيط"),
|
||||
("Secure Connection", "اتصال آمن"),
|
||||
("Insecure Connection", "اتصال غير آمن"),
|
||||
("Continue", ""),
|
||||
("Scale original", "المقياس الأصلي"),
|
||||
("Scale adaptive", "مقياس التكيف"),
|
||||
("General", "عام"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "تبديل الشاشة"),
|
||||
("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"),
|
||||
("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"),
|
||||
("All monitors", "جميع الشاشات"),
|
||||
("#{} monitor", "الشاشة رقم {}"),
|
||||
("conn-e2ee-unavailable-tip", "تعذر التحقق من التشفير من طرف إلى طرف.\nقد يكون الجهاز البعيد ما يزال قيد الإعداد. حاول مرة أخرى لاحقًا.\nإذا استمر حدوث ذلك، فقد يكون الخادم غير موثوق به.\nهل تريد المتابعة على أي حال؟"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Рэтрансляванае падключэнне"),
|
||||
("Secure Connection", "Бяспечнае падключэнне"),
|
||||
("Insecure Connection", "Нябяспечнае падключэнне"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Арыгінальны маштаб"),
|
||||
("Scale adaptive", "Адаптыўны маштаб"),
|
||||
("General", "Агульныя"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Пераключыць дысплэй"),
|
||||
("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"),
|
||||
("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"),
|
||||
("All monitors", "Усе манітори"),
|
||||
("#{} monitor", "Манітор {}"),
|
||||
("conn-e2ee-unavailable-tip", "Не ўдалося праверыць скразное шыфраванне.\nАддаленая прылада, магчыма, яшчэ наладжваецца. Паспрабуйце пазней.\nКалі гэта будзе паўтарацца, сервер можа быць ненадзейным.\nУсё роўна працягнуць?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Релейна връзка"),
|
||||
("Secure Connection", "Сигурна връзка"),
|
||||
("Insecure Connection", "Несигурна връзка"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Оригинален мащаб"),
|
||||
("Scale adaptive", "Приспособимо мащабиране"),
|
||||
("General", "Основен"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Превключване на дисплея"),
|
||||
("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"),
|
||||
("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"),
|
||||
("All monitors", "Всички монитори"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
("conn-e2ee-unavailable-tip", "Шифроването от край до край не може да бъде проверено.\nОтдалеченото устройство може все още да се настройва. Опитайте отново по-късно.\nАко това продължи, сървърът може да не е надежден.\nДа се продължи ли въпреки това?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Connexió amb repetidor"),
|
||||
("Secure Connection", "Connexió segura"),
|
||||
("Insecure Connection", "Connexió no segura"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Escala original"),
|
||||
("Scale adaptive", "Escala adaptativa"),
|
||||
("General", "General"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Canvia de pantalla"),
|
||||
("Show monitor switch button on the main toolbar", "Mostra el botó de canvi de monitor a la barra d’eines principal"),
|
||||
("Show on the minimized toolbar", "Mostra a la barra d’eines minimitzada"),
|
||||
("All monitors", "Tots els monitors"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "No s'ha pogut verificar el xifratge d'extrem a extrem.\nEl dispositiu remot encara es pot estar configurant. Torneu-ho a provar més tard.\nSi això continua passant, el servidor pot no ser de confiança.\nVoleu continuar igualment?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "中继连接"),
|
||||
("Secure Connection", "安全连接"),
|
||||
("Insecure Connection", "非安全连接"),
|
||||
("Continue", ""),
|
||||
("Scale original", "原始尺寸"),
|
||||
("Scale adaptive", "适应窗口"),
|
||||
("General", "常规"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "切换显示器"),
|
||||
("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"),
|
||||
("Show on the minimized toolbar", "在最小化工具栏上显示"),
|
||||
("All monitors", "所有显示器"),
|
||||
("#{} monitor", "{}号显示器"),
|
||||
("conn-e2ee-unavailable-tip", "无法验证端到端加密。\n远程设备可能仍在准备中,请稍后重试。\n如果此问题持续出现,服务器可能不受信任。\n仍要继续吗?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Připojení předávací server"),
|
||||
("Secure Connection", "Zabezpečené připojení"),
|
||||
("Insecure Connection", "Nezabezpečené připojení"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Originální měřítko"),
|
||||
("Scale adaptive", "Adaptivní měřítko"),
|
||||
("General", "Obecné"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Přepnout obrazovku"),
|
||||
("Show monitor switch button on the main toolbar", "Zobrazit tlačítko přepnutí monitoru na hlavním panelu nástrojů"),
|
||||
("Show on the minimized toolbar", "Zobrazit na minimalizovaném panelu nástrojů"),
|
||||
("All monitors", "Všechny monitory"),
|
||||
("#{} monitor", "Monitor č. {}"),
|
||||
("conn-e2ee-unavailable-tip", "Nepodařilo se ověřit koncové šifrování.\nVzdálené zařízení se možná stále nastavuje. Zkuste to znovu později.\nPokud se to bude opakovat, server nemusí být důvěryhodný.\nPřesto pokračovat?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Viderestillingsforbindelse"),
|
||||
("Secure Connection", "Sikker forbindelse"),
|
||||
("Insecure Connection", "Usikker forbindelse"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Original skalering"),
|
||||
("Scale adaptive", "Adaptiv skalering"),
|
||||
("General", "Generelt"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Skift skærm"),
|
||||
("Show monitor switch button on the main toolbar", "Vis knap til skærmskift på hovedværktøjslinjen"),
|
||||
("Show on the minimized toolbar", "Vis på den minimerede værktøjslinje"),
|
||||
("All monitors", "Alle skærme"),
|
||||
("#{} monitor", "Skærm {}"),
|
||||
("conn-e2ee-unavailable-tip", "End-to-end-kryptering kunne ikke bekræftes.\nDen eksterne enhed er muligvis stadig ved at blive konfigureret. Prøv igen senere.\nHvis dette fortsætter, er serveren muligvis ikke pålidelig.\nFortsæt alligevel?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Relay-Verbindung"),
|
||||
("Secure Connection", "Sichere Verbindung"),
|
||||
("Insecure Connection", "Unsichere Verbindung"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Keine Skalierung"),
|
||||
("Scale adaptive", "Anpassbare Skalierung"),
|
||||
("General", "Allgemein"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Anzeige wechseln"),
|
||||
("Show monitor switch button on the main toolbar", "Schaltfläche zum Monitorwechsel in der Haupt-Symbolleiste anzeigen"),
|
||||
("Show on the minimized toolbar", "In der minimierten Symbolleiste anzeigen"),
|
||||
("All monitors", "Alle Bildschirme"),
|
||||
("#{} monitor", "Bildschirm {}"),
|
||||
("conn-e2ee-unavailable-tip", "Ende-zu-Ende-Verschlüsselung konnte nicht verifiziert werden.\nDas entfernte Gerät wird möglicherweise noch eingerichtet. Versuchen Sie es später erneut.\nWenn dies weiterhin auftritt, ist der Server möglicherweise nicht vertrauenswürdig.\nTrotzdem fortfahren?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Αναμεταδιδόμενη σύνδεση"),
|
||||
("Secure Connection", "Ασφαλής σύνδεση"),
|
||||
("Insecure Connection", "Μη ασφαλής σύνδεση"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Κλιμάκωση πρωτότυπου"),
|
||||
("Scale adaptive", "Προσαρμοσμένη κλίμακα"),
|
||||
("General", "Γενικά"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Εναλλαγή οθόνης"),
|
||||
("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"),
|
||||
("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"),
|
||||
("All monitors", "Όλες οι οθόνες"),
|
||||
("#{} monitor", "Οθόνη {}"),
|
||||
("conn-e2ee-unavailable-tip", "Δεν ήταν δυνατή η επαλήθευση της κρυπτογράφησης από άκρο σε άκρο.\nΗ απομακρυσμένη συσκευή μπορεί να ρυθμίζεται ακόμα. Δοκιμάστε ξανά αργότερα.\nΑν αυτό συνεχιστεί, ο διακομιστής μπορεί να μην είναι αξιόπιστος.\nΣυνέχεια παρ' όλα αυτά;"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -279,5 +279,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("wayland-soft-keyboard-input-label", "Soft keyboard input"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
|
||||
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
|
||||
("conn-e2ee-unavailable-tip", "Could not verify end-to-end encryption.\nThe remote device may still be setting up. Try again later.\nIf this keeps happening, the server may be untrusted.\nContinue anyway?")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Relajsa Konekto"),
|
||||
("Secure Connection", "Sekura Konekto"),
|
||||
("Insecure Connection", "Nesekura Konekto"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Skalo originalo"),
|
||||
("Scale adaptive", "Skalo adapta"),
|
||||
("General", "Ĝenerala"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ŝalti ekranon"),
|
||||
("Show monitor switch button on the main toolbar", "Montri ekran-ŝaltan butonon en la ĉefa ilobreto"),
|
||||
("Show on the minimized toolbar", "Montri en la minimumigita ilobreto"),
|
||||
("All monitors", "Ĉiuj monitoroj"),
|
||||
("#{} monitor", "Monitoro {}"),
|
||||
("conn-e2ee-unavailable-tip", "Ne eblis kontroli la fin-al-finan ĉifradon.\nLa fora aparato eble ankoraŭ estas agordata. Provu denove poste.\nSe tio daŭre okazas, la servilo eble estas nefidinda.\nĈu daŭrigi tamen?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Conexión Relay"),
|
||||
("Secure Connection", "Conexión segura"),
|
||||
("Insecure Connection", "Conexión insegura"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Escala original"),
|
||||
("Scale adaptive", "Escala adaptativa"),
|
||||
("General", "General"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Cambiar de pantalla"),
|
||||
("Show monitor switch button on the main toolbar", "Mostrar el botón de cambio de monitor en la barra de herramientas principal"),
|
||||
("Show on the minimized toolbar", "Mostrar en la barra de herramientas minimizada"),
|
||||
("All monitors", "Todos los monitores"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "No se pudo verificar el cifrado de extremo a extremo.\nEs posible que el dispositivo remoto aún se esté configurando. Inténtelo de nuevo más tarde.\nSi esto sigue ocurriendo, es posible que el servidor no sea de confianza.\n¿Continuar de todos modos?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Releeühendus"),
|
||||
("Secure Connection", "Turvaline ühendus"),
|
||||
("Insecure Connection", "Ebaturvaline ühendus"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Originaalskaala"),
|
||||
("Scale adaptive", "Kohanduv skaala"),
|
||||
("General", "Üldine"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Vaheta kuva"),
|
||||
("Show monitor switch button on the main toolbar", "Näita monitori vahetamise nuppu peamisel tööriistaribal"),
|
||||
("Show on the minimized toolbar", "Näita minimeeritud tööriistaribal"),
|
||||
("All monitors", "Kõik kuvarid"),
|
||||
("#{} monitor", "Kuvar {}"),
|
||||
("conn-e2ee-unavailable-tip", "Otspunktkrüptimist ei saanud kontrollida.\nKaugseade võib olla veel seadistamisel. Proovige hiljem uuesti.\nKui see jätkub, ei pruugi server olla usaldusväärne.\nKas jätkata siiski?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Konexio igorria"),
|
||||
("Secure Connection", "Konexio segurua"),
|
||||
("Insecure Connection", "Konexio ez-segurua"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Jatorrizko eskala"),
|
||||
("Scale adaptive", "Eskala moldagarria"),
|
||||
("General", "Orokorra"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Aldatu pantaila"),
|
||||
("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"),
|
||||
("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"),
|
||||
("All monitors", "Monitore guztiak"),
|
||||
("#{} monitor", "{}. monitorea"),
|
||||
("conn-e2ee-unavailable-tip", "Ezin izan da muturretik muturrerako enkriptatzea egiaztatu.\nUrruneko gailua oraindik konfiguratzen ari daiteke. Saiatu berriro geroago.\nHonek jarraitzen badu, zerbitzaria fidagaitza izan daiteke.\nHala ere jarraitu?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Relay ارتباط"),
|
||||
("Secure Connection", "ارتباط امن"),
|
||||
("Insecure Connection", "ارتباط غیر امن"),
|
||||
("Continue", ""),
|
||||
("Scale original", "مقیاس اصلی"),
|
||||
("Scale adaptive", "مقیاس تطبیقی"),
|
||||
("General", "عمومی"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "تعویض نمایشگر"),
|
||||
("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"),
|
||||
("Show on the minimized toolbar", "نمایش در نوار ابزار کوچکشده"),
|
||||
("All monitors", "همه نمایشگرها"),
|
||||
("#{} monitor", "نمایشگر {}"),
|
||||
("conn-e2ee-unavailable-tip", "رمزنگاری سرتاسری قابل تأیید نیست.\nدستگاه راه دور ممکن است هنوز در حال آمادهسازی باشد. بعداً دوباره تلاش کنید.\nاگر این مشکل ادامه داشت، سرور ممکن است نامطمئن باشد.\nبا این حال ادامه میدهید؟"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Välitetty yhteys"),
|
||||
("Secure Connection", "Suojattu yhteys"),
|
||||
("Insecure Connection", "Suojaamaton yhteys"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Skaalaa alkuperäinen"),
|
||||
("Scale adaptive", "Mukautuva skaalaus"),
|
||||
("General", "Yleiset"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Vaihda näyttöä"),
|
||||
("Show monitor switch button on the main toolbar", "Näytä näytön vaihtopainike päätyökalurivillä"),
|
||||
("Show on the minimized toolbar", "Näytä pienennetyssä työkalurivissä"),
|
||||
("All monitors", "Kaikki näytöt"),
|
||||
("#{} monitor", "Näyttö {}"),
|
||||
("conn-e2ee-unavailable-tip", "Päästä päähän -salausta ei voitu vahvistaa.\nEtälaite voi olla vielä määritettävänä. Yritä myöhemmin uudelleen.\nJos tämä jatkuu, palvelin ei ehkä ole luotettava.\nJatketaanko silti?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Connexion via relais"),
|
||||
("Secure Connection", "Connexion sécurisée"),
|
||||
("Insecure Connection", "Connexion non sécurisée"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Échelle originale"),
|
||||
("Scale adaptive", "Échelle adaptative"),
|
||||
("General", "Général"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Changer d’écran"),
|
||||
("Show monitor switch button on the main toolbar", "Afficher le bouton de changement d’écran dans la barre d’outils principale"),
|
||||
("Show on the minimized toolbar", "Afficher dans la barre d’outils réduite"),
|
||||
("All monitors", "Tous les moniteurs"),
|
||||
("#{} monitor", "Moniteur {}"),
|
||||
("conn-e2ee-unavailable-tip", "Impossible de vérifier le chiffrement de bout en bout.\nL'appareil distant est peut-être encore en cours de configuration. Réessayez plus tard.\nSi le problème persiste, le serveur n'est peut-être pas fiable.\nContinuer quand même ?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "რეტრანსლირებული კავშირი"),
|
||||
("Secure Connection", "უსაფრთხო კავშირი"),
|
||||
("Insecure Connection", "არაუსაფრთხო კავშირი"),
|
||||
("Continue", ""),
|
||||
("Scale original", "ორიგინალური მასშტაბი"),
|
||||
("Scale adaptive", "ადაპტირებადი მასშტაბი"),
|
||||
("General", "ზოგადი"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ეკრანის გადართვა"),
|
||||
("Show monitor switch button on the main toolbar", "მონიტორის გადართვის ღილაკის ჩვენება მთავარ ხელსაწყოთა ზოლზე"),
|
||||
("Show on the minimized toolbar", "ჩვენება ჩაკეცილ ხელსაწყოთა ზოლზე"),
|
||||
("All monitors", "ყველა მონიტორი"),
|
||||
("#{} monitor", "მონიტორი {}"),
|
||||
("conn-e2ee-unavailable-tip", "ბოლომდე დაშიფვრის გადამოწმება ვერ მოხერხდა.\nდისტანციური მოწყობილობა შესაძლოა ჯერ კიდევ მზადდება. სცადეთ მოგვიანებით.\nთუ ეს კვლავ გაგრძელდება, სერვერი შესაძლოა არასანდო იყოს.\nმაინც გააგრძელებთ?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "રિલે કનેક્શન"),
|
||||
("Secure Connection", "સુરક્ષિત કનેક્શન"),
|
||||
("Insecure Connection", "અસુરક્ષિત કનેક્શન"),
|
||||
("Continue", ""),
|
||||
("Scale original", "મૂળ સ્કેલ"),
|
||||
("Scale adaptive", "એડેપ્ટિવ સ્કેલ"),
|
||||
("General", "સામાન્ય"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ડિસ્પ્લે બદલો"),
|
||||
("Show monitor switch button on the main toolbar", "મુખ્ય ટૂલબાર પર મોનિટર સ્વિચ બટન બતાવો"),
|
||||
("Show on the minimized toolbar", "ન્યૂનતમ કરેલા ટૂલબાર પર બતાવો"),
|
||||
("All monitors", "બધા મોનિટર"),
|
||||
("#{} monitor", "મોનિટર {}"),
|
||||
("conn-e2ee-unavailable-tip", "એન્ડ-ટુ-એન્ડ એન્ક્રિપ્શન ચકાસી શકાયું નથી.\nરિમોટ ઉપકરણ હજી સેટ થઈ રહ્યું હોઈ શકે છે. પછીથી ફરી પ્રયાસ કરો.\nજો આ ચાલુ રહે, તો સર્વર અવિશ્વસનીય હોઈ શકે છે.\nશું તેમ છતાં ચાલુ રાખવું?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "חיבור באמצעות ממסר"),
|
||||
("Secure Connection", "חיבור מאובטח"),
|
||||
("Insecure Connection", "חיבור לא מאובטח"),
|
||||
("Continue", ""),
|
||||
("Scale original", "קנה מידה מקורי"),
|
||||
("Scale adaptive", "קנה מידה מותאם"),
|
||||
("General", "כללי"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "החלפת צג"),
|
||||
("Show monitor switch button on the main toolbar", "הצגת לחצן החלפת צג בסרגל הכלים הראשי"),
|
||||
("Show on the minimized toolbar", "הצגה בסרגל הכלים הממוזער"),
|
||||
("All monitors", "כל המסכים"),
|
||||
("#{} monitor", "מסך {}"),
|
||||
("conn-e2ee-unavailable-tip", "לא ניתן לאמת הצפנה מקצה לקצה.\nייתכן שהמכשיר המרוחק עדיין בתהליך הגדרה. נסה שוב מאוחר יותר.\nאם זה ממשיך לקרות, ייתכן שהשרת אינו מהימן.\nלהמשיך בכל זאת?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "रिले कनेक्शन"),
|
||||
("Secure Connection", "सुरक्षित कनेक्शन"),
|
||||
("Insecure Connection", "असुरक्षित कनेक्शन"),
|
||||
("Continue", ""),
|
||||
("Scale original", "मूल पैमाना"),
|
||||
("Scale adaptive", "अनुकूली पैमाना"),
|
||||
("General", "सामान्य"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "डिस्प्ले बदलें"),
|
||||
("Show monitor switch button on the main toolbar", "मुख्य टूलबार पर मॉनिटर स्विच बटन दिखाएं"),
|
||||
("Show on the minimized toolbar", "न्यूनतम किए गए टूलबार पर दिखाएं"),
|
||||
("All monitors", "सभी मॉनिटर"),
|
||||
("#{} monitor", "मॉनिटर {}"),
|
||||
("conn-e2ee-unavailable-tip", "एंड-टू-एंड एन्क्रिप्शन सत्यापित नहीं किया जा सका।\nदूरस्थ डिवाइस अभी भी सेट अप हो रहा हो सकता है। बाद में फिर प्रयास करें।\nयदि यह समस्या बनी रहती है, तो सर्वर अविश्वसनीय हो सकता है।\nफिर भी जारी रखें?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Posredna veza"),
|
||||
("Secure Connection", "Sigurna veza"),
|
||||
("Insecure Connection", "Nesigurna veza"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Skaliraj izvornik"),
|
||||
("Scale adaptive", "Prilagođeno skaliranje"),
|
||||
("General", "Općenito"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Promijeni zaslon"),
|
||||
("Show monitor switch button on the main toolbar", "Prikaži gumb za prebacivanje monitora na glavnoj alatnoj traci"),
|
||||
("Show on the minimized toolbar", "Prikaži na minimiziranoj alatnoj traci"),
|
||||
("All monitors", "Svi monitori"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "End-to-end enkripcija nije mogla biti potvrđena.\nUdaljeni uređaj se možda još postavlja. Pokušajte ponovno kasnije.\nAko se to nastavi događati, poslužitelj možda nije pouzdan.\nIpak nastaviti?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Kapcsolódás továbbító-kiszolgálón keresztül"),
|
||||
("Secure Connection", "Biztonságos kapcsolat"),
|
||||
("Insecure Connection", "Nem biztonságos kapcsolat"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Eredeti méretarány"),
|
||||
("Scale adaptive", "Adaptív méretarány"),
|
||||
("General", "Általános"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Kijelző váltása"),
|
||||
("Show monitor switch button on the main toolbar", "Monitorváltó gomb megjelenítése a fő eszköztáron"),
|
||||
("Show on the minimized toolbar", "Megjelenítés a kis méretű eszköztáron"),
|
||||
("All monitors", "Minden monitor"),
|
||||
("#{} monitor", "{}. monitor"),
|
||||
("conn-e2ee-unavailable-tip", "A végpontok közötti titkosítás nem volt ellenőrizhető.\nA távoli eszköz talán még beállítás alatt áll. Próbálja újra később.\nHa ez továbbra is előfordul, a szerver lehet, hogy nem megbízható.\nFolytatja így is?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Koneksi Relay"),
|
||||
("Secure Connection", "Koneksi aman"),
|
||||
("Insecure Connection", "Koneksi Tidak Aman"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Skala asli"),
|
||||
("Scale adaptive", "Skala adaptif"),
|
||||
("General", "Umum"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ganti tampilan"),
|
||||
("Show monitor switch button on the main toolbar", "Tampilkan tombol pengalih monitor di bilah alat utama"),
|
||||
("Show on the minimized toolbar", "Tampilkan di bilah alat yang diperkecil"),
|
||||
("All monitors", "Semua monitor"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "Tidak dapat memverifikasi enkripsi ujung ke ujung.\nPerangkat jarak jauh mungkin masih disiapkan. Coba lagi nanti.\nJika ini terus terjadi, server mungkin tidak tepercaya.\nTetap lanjutkan?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Connessione relay"),
|
||||
("Secure Connection", "Connessione sicura"),
|
||||
("Insecure Connection", "Connessione non sicura"),
|
||||
("Continue", "Continua"),
|
||||
("Scale original", "Scala originale"),
|
||||
("Scale adaptive", "Scala adattiva"),
|
||||
("General", "Generale"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Cambia schermo"),
|
||||
("Show monitor switch button on the main toolbar", "Visualizza nella barra strumenti principale il pulsante per il cambio schermo"),
|
||||
("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
|
||||
("All monitors", "Tutti gli schermi"),
|
||||
("#{} monitor", "Schermo {}"),
|
||||
("conn-e2ee-unavailable-tip", "Impossibile verificare la crittografia end-to-end.\nIl dispositivo remoto potrebbe essere ancora in configurazione. Riprova più tardi.\nSe il problema persiste, il server potrebbe non essere attendibile.\nVuoi continuare?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "中継接続"),
|
||||
("Secure Connection", "安全な接続"),
|
||||
("Insecure Connection", "安全でない接続"),
|
||||
("Continue", ""),
|
||||
("Scale original", "オリジナルのサイズ"),
|
||||
("Scale adaptive", "ウィンドウに合わせる"),
|
||||
("General", "一般"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ディスプレイを切り替え"),
|
||||
("Show monitor switch button on the main toolbar", "メインツールバーにモニター切り替えボタンを表示"),
|
||||
("Show on the minimized toolbar", "最小化したツールバーに表示"),
|
||||
("All monitors", "すべてのディスプレイ"),
|
||||
("#{} monitor", "ディスプレイ {}"),
|
||||
("conn-e2ee-unavailable-tip", "エンドツーエンド暗号化を確認できませんでした。\nリモートデバイスはまだ準備中の可能性があります。後でもう一度お試しください。\nこの問題が続く場合、サーバーが信頼できない可能性があります。\nそれでも続行しますか?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
+9
-6
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Share", "화면 공유"),
|
||||
("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."),
|
||||
("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."),
|
||||
("xdp-portal-unavailable", "Wayland 화면 캡처에 실패했습니다. XDG Desktop Portal이 충돌했거나 사용할 수 없는 상태일 수 있습니다. `systemctl --user restart xdg-desktop-portal` 명령으로 다시 시작해 보세요."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "점프 링크"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"),
|
||||
("Show RustDesk", "RustDesk 표시"),
|
||||
@@ -749,17 +749,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Deploy", "배포"),
|
||||
("Custom ID (optional)", "사용자 지정 ID (선택 사항)"),
|
||||
("server_requires_deployment_tip", "서버에서 이 장치를 명시적으로 배포하도록 요구합니다. 지금 배포하시겠습니까?"),
|
||||
("The server does not require explicit deployment.", "서버에서 명시적인 배포를 요구하지 않습니다."),
|
||||
("The server does not require explicit deployment.", "서버에서 명시적 배포를 요구하지 않습니다."),
|
||||
("Unknown response.", "알 수 없는 응답입니다."),
|
||||
("wayland-keyboard-input-disabled-tip", "키보드 입력을 허용하시겠습니까?"),
|
||||
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용(비밀번호 포함)은 해당 컴퓨터의 다른 앱이 읽을 수 있습니다."),
|
||||
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용 (비밀번호 포함)은 해당 컴퓨터의 다른 앱에서 읽을 수 있습니다."),
|
||||
("wayland-keyboard-input-applies-to-tip", "이 선택이 적용되는 대상:"),
|
||||
("wayland-soft-keyboard-input-label", "소프트 키보드 입력"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 초기화"),
|
||||
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않기"),
|
||||
("Why this happens", "이런 현상이 발생하는 이유"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 재설정"),
|
||||
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않음"),
|
||||
("Why this happens", "이 현상이 발생하는 이유"),
|
||||
("Switch display", "디스플레이 전환"),
|
||||
("Show monitor switch button on the main toolbar", "기본 도구 모음에 모니터 전환 버튼 표시"),
|
||||
("Show on the minimized toolbar", "최소화된 도구 모음에 표시"),
|
||||
("All monitors", "모든 모니터"),
|
||||
("#{} monitor", "#{} 모니터"),
|
||||
("conn-e2ee-unavailable-tip", "종단 간 암호화를 확인할 수 없습니다.\n원격 장치가 여전히 설정 중일 수 있습니다. 나중에 다시 시도해 보세요.\n이런 일이 계속 발생하면 서버가 신뢰할 수 없을 수도 있습니다.\n어쨌든 계속하시겠습니까?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Релай Қосылым"),
|
||||
("Secure Connection", "Қауіпсіз Қосылым"),
|
||||
("Insecure Connection", "Қатерлі Қосылым"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Scale original"),
|
||||
("Scale adaptive", "Scale adaptive"),
|
||||
("General", "Жалпы"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Дисплейді ауыстыру"),
|
||||
("Show monitor switch button on the main toolbar", "Негізгі құралдар тақтасында мониторды ауыстыру түймесін көрсету"),
|
||||
("Show on the minimized toolbar", "Кішірейтілген құралдар тақтасында көрсету"),
|
||||
("All monitors", "Барлық мониторлар"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
("conn-e2ee-unavailable-tip", "Ұштан-ұшқа шифрлауды тексеру мүмкін болмады.\nҚашықтағы құрылғы әлі бапталып жатқан болуы мүмкін. Кейінірек қайталап көріңіз.\nЕгер бұл қайталана берсе, сервер сенімсіз болуы мүмкін.\nСонда да жалғастыру керек пе?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Tarpinė jungtis"),
|
||||
("Secure Connection", "Saugus ryšys"),
|
||||
("Insecure Connection", "Nesaugus ryšys"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Pakeisti originalų mastelį"),
|
||||
("Scale adaptive", "Pritaikomas mastelis"),
|
||||
("General", "Bendra"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Perjungti ekraną"),
|
||||
("Show monitor switch button on the main toolbar", "Rodyti monitoriaus perjungimo mygtuką pagrindinėje įrankių juostoje"),
|
||||
("Show on the minimized toolbar", "Rodyti sumažintoje įrankių juostoje"),
|
||||
("All monitors", "Visi monitoriai"),
|
||||
("#{} monitor", "Monitorius {}"),
|
||||
("conn-e2ee-unavailable-tip", "Nepavyko patikrinti galinio šifravimo.\nNuotolinis įrenginys galbūt vis dar nustatomas. Bandykite dar kartą vėliau.\nJei tai kartojasi, serveris gali būti nepatikimas.\nVis tiek tęsti?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Releja savienojums"),
|
||||
("Secure Connection", "Drošs savienojums"),
|
||||
("Insecure Connection", "Nedrošs savienojums"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Mērogs oriģināls"),
|
||||
("Scale adaptive", "Mērogs adaptīvs"),
|
||||
("General", "Vispārīgi"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Pārslēgt displeju"),
|
||||
("Show monitor switch button on the main toolbar", "Rādīt monitora pārslēgšanas pogu galvenajā rīkjoslā"),
|
||||
("Show on the minimized toolbar", "Rādīt minimizētajā rīkjoslā"),
|
||||
("All monitors", "Visi monitori"),
|
||||
("#{} monitor", "Monitors {}"),
|
||||
("conn-e2ee-unavailable-tip", "Neizdevās pārbaudīt pilnīgu šifrēšanu.\nAttālā ierīce, iespējams, vēl tiek iestatīta. Mēģiniet vēlreiz vēlāk.\nJa tas turpinās, serveris var nebūt uzticams.\nVai tomēr turpināt?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "റിലേ കണക്ഷൻ"),
|
||||
("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"),
|
||||
("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"),
|
||||
("Continue", ""),
|
||||
("Scale original", "ഒറിജിനൽ വലിപ്പം"),
|
||||
("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"),
|
||||
("General", "പൊതുവായവ"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "ഡിസ്പ്ലേ മാറ്റുക"),
|
||||
("Show monitor switch button on the main toolbar", "പ്രധാന ടൂൾബാറിൽ മോണിറ്റർ സ്വിച്ച് ബട്ടൺ കാണിക്കുക"),
|
||||
("Show on the minimized toolbar", "ചെറുതാക്കിയ ടൂൾബാറിൽ കാണിക്കുക"),
|
||||
("All monitors", "എല്ലാ മോണിറ്ററുകളും"),
|
||||
("#{} monitor", "മോണിറ്റർ {}"),
|
||||
("conn-e2ee-unavailable-tip", "എൻഡ്-ടു-എൻഡ് എൻക്രിപ്ഷൻ പരിശോധിക്കാൻ കഴിഞ്ഞില്ല.\nദൂരസ്ഥ ഉപകരണം ഇനിയും സജ്ജീകരണത്തിലായിരിക്കാം. പിന്നീട് വീണ്ടും ശ്രമിക്കുക.\nഇത് തുടർന്നാൽ സർവർ വിശ്വസനീയമല്ലായിരിക്കാം.\nഎങ്കിലും തുടരണമോ?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Viderekoblet tilkobling"),
|
||||
("Secure Connection", "Sikker tilkobling"),
|
||||
("Insecure Connection", "Usikker tilkobling"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Original skalering"),
|
||||
("Scale adaptive", "Adaptiv skalering"),
|
||||
("General", "Generelt"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Bytt skjerm"),
|
||||
("Show monitor switch button on the main toolbar", "Vis knapp for skjermbytte på hovedverktøylinjen"),
|
||||
("Show on the minimized toolbar", "Vis på den minimerte verktøylinjen"),
|
||||
("All monitors", "Alle skjermer"),
|
||||
("#{} monitor", "Skjerm {}"),
|
||||
("conn-e2ee-unavailable-tip", "Ende-til-ende-kryptering kunne ikke verifiseres.\nDen eksterne enheten kan fortsatt være under oppsett. Prøv igjen senere.\nHvis dette fortsetter, kan serveren være upålitelig.\nFortsette likevel?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Relaisverbinding"),
|
||||
("Secure Connection", "Beveiligde Verbinding"),
|
||||
("Insecure Connection", "Onveilige Verbinding"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Oorspronkelijk formaat"),
|
||||
("Scale adaptive", "Automatisch schalen"),
|
||||
("General", "Algemeen"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Beeldscherm wisselen"),
|
||||
("Show monitor switch button on the main toolbar", "Knop voor monitorwisseling weergeven op de hoofdwerkbalk"),
|
||||
("Show on the minimized toolbar", "Weergeven op de geminimaliseerde werkbalk"),
|
||||
("All monitors", "Alle monitoren"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "End-to-endversleuteling kon niet worden geverifieerd.\nHet externe apparaat wordt mogelijk nog ingesteld. Probeer het later opnieuw.\nAls dit blijft gebeuren, is de server mogelijk niet vertrouwd.\nToch doorgaan?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Połączenie przez bramkę"),
|
||||
("Secure Connection", "Połączenie szyfrowane"),
|
||||
("Insecure Connection", "Połączenie nieszyfrowane"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Skalowanie oryginalne"),
|
||||
("Scale adaptive", "Dopasuj do wyświetlacza"),
|
||||
("General", "Ogólne"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Przełącz ekran"),
|
||||
("Show monitor switch button on the main toolbar", "Pokaż przycisk przełączania monitora na głównym pasku narzędzi"),
|
||||
("Show on the minimized toolbar", "Pokaż na zminimalizowanym pasku narzędzi"),
|
||||
("All monitors", "Wszystkie ekrany"),
|
||||
("#{} monitor", "Ekran {}"),
|
||||
("conn-e2ee-unavailable-tip", "Nie można zweryfikować szyfrowania end-to-end.\nUrządzenie zdalne może nadal się konfigurować. Spróbuj ponownie później.\nJeśli problem będzie się powtarzał, serwer może być niezaufany.\nKontynuować mimo to?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Conexão de relé"),
|
||||
("Secure Connection", "Conexão segura"),
|
||||
("Insecure Connection", "Conexão insegura"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Escala original"),
|
||||
("Scale adaptive", "Escala adaptável"),
|
||||
("General", "Geral"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Trocar de ecrã"),
|
||||
("Show monitor switch button on the main toolbar", "Mostrar o botão de troca de monitor na barra de ferramentas principal"),
|
||||
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
|
||||
("All monitors", "Todos os monitores"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "Não foi possível verificar a encriptação de ponta a ponta.\nO dispositivo remoto ainda pode estar a ser configurado. Tente novamente mais tarde.\nSe isto continuar a acontecer, o servidor pode não ser fidedigno.\nContinuar mesmo assim?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
+6
-2
@@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Accept and Install", "Aceitar e Instalar"),
|
||||
("End-user license agreement", "Acordo de licença do usuário final"),
|
||||
("Generating ...", "Gerando ..."),
|
||||
("Your installation is lower version.", "Instalação desatualizada"),
|
||||
("Your installation is lower version.", "Sua instalação está com uma versão desatualizada."),
|
||||
("not_close_tcp_tip", "Não feche esta janela enquanto estiver utilizando o túnel"),
|
||||
("Listening ...", "Escutando ..."),
|
||||
("Remote Host", "Host Remoto"),
|
||||
@@ -321,7 +321,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fullscreen", "Tela Cheia"),
|
||||
("Mobile Actions", "Ações móveis"),
|
||||
("Select Monitor", "Selecionar tela"),
|
||||
("Control Actions", "Controlar ações"),
|
||||
("Control Actions", "Ações de controle"),
|
||||
("Display Settings", "Configurações de exibição"),
|
||||
("Ratio", "Proporção"),
|
||||
("Image Quality", "Qualidade de imagem"),
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Conexão via Relay"),
|
||||
("Secure Connection", "Conexão Segura"),
|
||||
("Insecure Connection", "Conexão Insegura"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Escala original"),
|
||||
("Scale adaptive", "Escala adaptada"),
|
||||
("General", "Geral"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Trocar de tela"),
|
||||
("Show monitor switch button on the main toolbar", "Mostrar botão de troca de tela na barra de ferramentas"),
|
||||
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
|
||||
("All monitors", "Todas as telas"),
|
||||
("#{} monitor", "Tela {}"),
|
||||
("conn-e2ee-unavailable-tip", "Não foi possível verificar a criptografia de ponta a ponta.\nO dispositivo remoto ainda pode estar sendo configurado. Tente novamente mais tarde.\nSe isso continuar acontecendo, o servidor pode não ser confiável.\nContinuar mesmo assim?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Conexiune prin retransmisie"),
|
||||
("Secure Connection", "Conexiune securizată"),
|
||||
("Insecure Connection", "Conexiune nesecurizată"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Dimensiune originală"),
|
||||
("Scale adaptive", "Scalare automată"),
|
||||
("General", "General"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Comută afișajul"),
|
||||
("Show monitor switch button on the main toolbar", "Afișează butonul de comutare a monitorului în bara de instrumente principală"),
|
||||
("Show on the minimized toolbar", "Afișează în bara de instrumente minimizată"),
|
||||
("All monitors", "Toate monitoarele"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "Criptarea end-to-end nu a putut fi verificată.\nDispozitivul la distanță poate fi încă în curs de configurare. Încercați din nou mai târziu.\nDacă acest lucru continuă, serverul poate să nu fie de încredere.\nContinuați oricum?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Ретранслируемое подключение"),
|
||||
("Secure Connection", "Безопасное подключение"),
|
||||
("Insecure Connection", "Небезопасное подключение"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Оригинальный масштаб"),
|
||||
("Scale adaptive", "Адаптивный масштаб"),
|
||||
("General", "Общие"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Переключить дисплей"),
|
||||
("Show monitor switch button on the main toolbar", "Показывать кнопку переключения монитора на главной панели инструментов"),
|
||||
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
|
||||
("All monitors", "Все мониторы"),
|
||||
("#{} monitor", "Монитор {}"),
|
||||
("conn-e2ee-unavailable-tip", "Не удалось проверить сквозное шифрование.\nУдаленное устройство, возможно, еще настраивается. Повторите попытку позже.\nЕсли это повторяется, сервер может быть ненадежным.\nВсе равно продолжить?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Connessione tramudada (relay)"),
|
||||
("Secure Connection", "Connessione segura"),
|
||||
("Insecure Connection", "Connessione non segura"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Iscala originale"),
|
||||
("Scale adaptive", "Iscala adativa"),
|
||||
("General", "Generale"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Càmbia ischermu"),
|
||||
("Show monitor switch button on the main toolbar", "Mustra su butone de càmbiu de monitor in sa barra de aina printzipale"),
|
||||
("Show on the minimized toolbar", "Mustra in sa barra de aina minimizada"),
|
||||
("All monitors", "Totu sos ischermos"),
|
||||
("#{} monitor", "Ischermu {}"),
|
||||
("conn-e2ee-unavailable-tip", "No est istadu possìbile verificare sa tzifratzione de punta a punta.\nSu dispositivu remotu podet èssere ancora in fase de configuratzione. Torra a proare prus a tardu.\nSi custu sighit a acontèssere, su server podet non èssere fidadu.\nBoles sighire comente siat?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Reléové pripojenie"),
|
||||
("Secure Connection", "Zabezpečené pripojenie"),
|
||||
("Insecure Connection", "Nezabezpečené pripojenie"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Pôvodná mierka"),
|
||||
("Scale adaptive", "Prispôsobivá mierka"),
|
||||
("General", "Všeobecné"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Prepnúť obrazovku"),
|
||||
("Show monitor switch button on the main toolbar", "Zobraziť tlačidlo prepnutia monitora na hlavnom paneli nástrojov"),
|
||||
("Show on the minimized toolbar", "Zobraziť na minimalizovanom paneli nástrojov"),
|
||||
("All monitors", "Všetky monitory"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "Nepodarilo sa overiť koncové šifrovanie.\nVzdialené zariadenie sa možno stále nastavuje. Skúste to znova neskôr.\nAk sa to bude opakovať, server nemusí byť dôveryhodný.\nNapriek tomu pokračovať?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Posredovana povezava"),
|
||||
("Secure Connection", "Zavarovana povezava"),
|
||||
("Insecure Connection", "Nezavarovana povezava"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Originalna velikost"),
|
||||
("Scale adaptive", "Prilagojena velikost"),
|
||||
("General", "Splošno"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Preklopi zaslon"),
|
||||
("Show monitor switch button on the main toolbar", "Pokaži gumb za preklop monitorja v glavni orodni vrstici"),
|
||||
("Show on the minimized toolbar", "Pokaži v pomanjšani orodni vrstici"),
|
||||
("All monitors", "Vsi zasloni"),
|
||||
("#{} monitor", "Zaslon {}"),
|
||||
("conn-e2ee-unavailable-tip", "Šifriranja od konca do konca ni bilo mogoče preveriti.\nOddaljena naprava se morda še nastavlja. Poskusite znova pozneje.\nČe se to še naprej dogaja, strežnik morda ni zaupanja vreden.\nVseeno nadaljevati?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Lidhja rele"),
|
||||
("Secure Connection", "Lidhje e sigurt"),
|
||||
("Insecure Connection", "Lidhje e pasigurt"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Shkalla origjinale"),
|
||||
("Scale adaptive", " E përsjhtatshme në shkallë"),
|
||||
("General", "Gjeneral"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Ndërro ekranin"),
|
||||
("Show monitor switch button on the main toolbar", "Shfaq butonin e ndërrimit të monitorit te shiriti kryesor i veglave"),
|
||||
("Show on the minimized toolbar", "Shfaq te shiriti i minimizuar i veglave"),
|
||||
("All monitors", "Të gjithë monitorët"),
|
||||
("#{} monitor", "Monitori {}"),
|
||||
("conn-e2ee-unavailable-tip", "Enkriptimi nga skaji në skaj nuk mund të verifikohej.\nPajisja e largët mund të jetë ende duke u konfiguruar. Provoni përsëri më vonë.\nNëse kjo vazhdon të ndodhë, serveri mund të mos jetë i besueshëm.\nTë vazhdohet gjithsesi?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relay Connection", "Posredna konekcija"),
|
||||
("Secure Connection", "Bezbedna konekcija"),
|
||||
("Insecure Connection", "Nebezbedna konekcija"),
|
||||
("Continue", ""),
|
||||
("Scale original", "Skaliraj original"),
|
||||
("Scale adaptive", "Adaptivno skaliranje"),
|
||||
("General", "Uopšteno"),
|
||||
@@ -761,5 +762,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Switch display", "Промени екран"),
|
||||
("Show monitor switch button on the main toolbar", "Прикажи дугме за пребацивање монитора на главној траци са алаткама"),
|
||||
("Show on the minimized toolbar", "Прикажи на умањеној траци са алаткама"),
|
||||
("All monitors", "Svi monitori"),
|
||||
("#{} monitor", "Monitor {}"),
|
||||
("conn-e2ee-unavailable-tip", "Није могуће проверити енд-то-енд шифровање.\nУдаљени уређај се можда још подешава. Покушајте поново касније.\nАко се ово настави, сервер можда није поуздан.\nНаставити свеједно?"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user