Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e86d0a0fc1 | ||
|
|
cf13957b45 | ||
|
|
2a18d7f77d | ||
|
|
7dadf3da7a | ||
|
|
de67e7967a | ||
|
|
face49de00 | ||
|
|
365781a0c0 | ||
|
|
ae9c4e5995 | ||
|
|
7bc7ef383a | ||
|
|
54b37862f1 | ||
|
|
0409d8f761 | ||
|
|
b669bdd8d5 | ||
|
|
b41658138a | ||
|
|
d68edb9e1a | ||
|
|
1931cb8c7c | ||
|
|
32c5437c5d | ||
|
|
1b61b77fbe | ||
|
|
c0b6367137 | ||
|
|
12ab22e048 | ||
|
|
7bde8a3713 | ||
|
|
9c55e1efc8 | ||
|
|
b24e16d6c1 | ||
|
|
6767dda246 | ||
|
|
5f61c2442a | ||
|
|
5631ffac4c | ||
|
|
78097f4006 | ||
|
|
1d8cdb5e93 | ||
|
|
3e590b8212 | ||
|
|
533efd04d7 | ||
|
|
fb392b81cb | ||
|
|
cbcad03bc6 | ||
|
|
2ec5f8fe7e | ||
|
|
875570e040 | ||
|
|
6d506cbb64 | ||
|
|
b17bda9a55 | ||
|
|
d6b56a2f35 | ||
|
|
64f181e4a1 | ||
|
|
1552402907 | ||
|
|
f0a6c706fa | ||
|
|
042114ae37 | ||
|
|
241623c406 | ||
|
|
40730cbbae | ||
|
|
cedc384ba1 | ||
|
|
15f3c36c5f | ||
|
|
ee088a8ddf | ||
|
|
cb1818ab37 | ||
|
|
66af938858 | ||
|
|
e26a838882 | ||
|
|
e0bb25cbe5 | ||
|
|
3491c79cbc | ||
|
|
365fc90d8b | ||
|
|
8be7331cb6 | ||
|
|
ea46d2493e | ||
|
|
1141be01f5 | ||
|
|
6c713f3135 | ||
|
|
3059ca061e | ||
|
|
2631c68b88 | ||
|
|
f69f6bc8d4 | ||
|
|
66e39b62a2 | ||
|
|
993a419793 | ||
|
|
f1444049bf | ||
|
|
da629ec42c | ||
|
|
7225c1fcf0 | ||
|
|
dbdff120bc | ||
|
|
51f4f736bc | ||
|
|
d4d90da196 | ||
|
|
9903e9df9e | ||
|
|
af218dbc83 | ||
|
|
b3e3f6151d | ||
|
|
3d39d9c27c | ||
|
|
589177fc94 | ||
|
|
4703a7d332 | ||
|
|
8d90c5a93b | ||
|
|
885d8a4586 | ||
|
|
0e8fc4a815 | ||
|
|
96afb88518 | ||
|
|
a30aaebc72 | ||
|
|
252d3cb797 | ||
|
|
26a4b1f3eb | ||
|
|
dc724a30e7 | ||
|
|
334c94ceb3 | ||
|
|
b6feaa695f | ||
|
|
5f190297a9 | ||
|
|
362b5594a2 | ||
|
|
b9d8de1ec1 | ||
|
|
8a6b33b841 | ||
|
|
3ffd8ac146 | ||
|
|
07ea52d620 | ||
|
|
4cb967251b | ||
|
|
29905608b5 | ||
|
|
36db9f65cd | ||
|
|
91445da094 | ||
|
|
083d6c591a | ||
|
|
e352ea67f4 | ||
|
|
4103ab70a7 | ||
|
|
fd9d6db2ed | ||
|
|
d0b15fbd2a | ||
|
|
7c42c04b23 | ||
|
|
81e4998a37 | ||
|
|
1ab5bb0a2e | ||
|
|
0f39626782 | ||
|
|
dc2edcec5d | ||
|
|
3198236a8c | ||
|
|
e044212eb6 | ||
|
|
33e8f2aa85 | ||
|
|
031ca4e50c | ||
|
|
73d0da726d | ||
|
|
5c97f5dce5 | ||
|
|
8d5aa731a1 | ||
|
|
e8c5a63986 | ||
|
|
46fe37015f | ||
|
|
b79f086df5 | ||
|
|
6afb6aaff2 | ||
|
|
5a1ab37171 | ||
|
|
5e0bf8f5a9 | ||
|
|
7964afeb84 | ||
|
|
3759347d47 | ||
|
|
2758f6b7d5 | ||
|
|
70a4ee4318 | ||
|
|
7f13edd14a | ||
|
|
96208e9fef | ||
|
|
e6bd5b0698 | ||
|
|
231231e51f | ||
|
|
247ea5fed3 | ||
|
|
0cf2db9239 | ||
|
|
318e3cbd7d | ||
|
|
5164f76919 | ||
|
|
972ec8d79d | ||
|
|
516904551c | ||
|
|
1bf80e3dd6 | ||
|
|
8f22273759 | ||
|
|
1ddd5203e5 | ||
|
|
62d2c0fb6d | ||
|
|
0db250f02a | ||
|
|
8be912a8cf | ||
|
|
03f913e49d | ||
|
|
fb126164ef | ||
|
|
eea85b9b5a | ||
|
|
b16b2bc1b4 | ||
|
|
55d21ab151 | ||
|
|
d25e17317d | ||
|
|
7a7cca7e45 | ||
|
|
0ef4246360 | ||
|
|
686e495171 | ||
|
|
c0db2e6da4 | ||
|
|
c0459b8f94 | ||
|
|
678a3d2d28 | ||
|
|
2894a5ab7b | ||
|
|
2efd0e61b4 | ||
|
|
18b1646cd4 | ||
|
|
66cdbbacd0 | ||
|
|
aaba4c144c | ||
|
|
2d5758e880 | ||
|
|
b20ec86fac | ||
|
|
1df5234eea | ||
|
|
1b8dbbeac9 | ||
|
|
c5f1222476 | ||
|
|
1009eabdfe | ||
|
|
8138a661a6 | ||
|
|
d369955790 | ||
|
|
0afca9d1ea | ||
|
|
0325500ebf | ||
|
|
b0dfd777fb |
181
.github/workflows/ci.yml
vendored
Normal file
181
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
name: CI
|
||||
|
||||
# env:
|
||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
# ensure_cargo_fmt:
|
||||
# name: Ensure 'cargo fmt' has been run
|
||||
# runs-on: ubuntu-20.04
|
||||
# steps:
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: stable
|
||||
# default: true
|
||||
# profile: minimal
|
||||
# components: rustfmt
|
||||
# - uses: actions/checkout@v2
|
||||
# - run: cargo fmt -- --check
|
||||
|
||||
# min_version:
|
||||
# name: Minimum supported rust version
|
||||
# runs-on: ubuntu-20.04
|
||||
# steps:
|
||||
# - name: Checkout source code
|
||||
# uses: actions/checkout@v2
|
||||
|
||||
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
||||
# uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
|
||||
# default: true
|
||||
# profile: minimal # minimal component installation (ie, no documentation)
|
||||
# components: clippy
|
||||
# - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
||||
# uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: clippy
|
||||
# args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints
|
||||
# - name: Run tests
|
||||
# uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: test
|
||||
# args: --locked
|
||||
|
||||
build:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
case ${{ matrix.job.target }} in
|
||||
x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake ;;
|
||||
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
|
||||
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
||||
esac
|
||||
|
||||
- name: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb'
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
|
||||
shell: bash
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- name: Show version information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
run: |
|
||||
gcc --version || true
|
||||
rustup -V
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.job.use-cross }}
|
||||
command: build
|
||||
args: --locked --release --target=${{ matrix.job.target }}
|
||||
|
||||
# - name: Strip debug information from executable
|
||||
# id: strip
|
||||
# shell: bash
|
||||
# run: |
|
||||
# # Figure out suffix of binary
|
||||
# EXE_suffix=""
|
||||
# case ${{ matrix.job.target }} in
|
||||
# *-pc-windows-*) EXE_suffix=".exe" ;;
|
||||
# esac;
|
||||
|
||||
# # Figure out what strip tool to use if any
|
||||
# STRIP="strip"
|
||||
# case ${{ matrix.job.target }} in
|
||||
# arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;;
|
||||
# aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;;
|
||||
# *-pc-windows-msvc) STRIP="" ;;
|
||||
# esac;
|
||||
|
||||
# # Setup paths
|
||||
# BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/"
|
||||
# mkdir -p "${BIN_DIR}"
|
||||
# BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}"
|
||||
# BIN_PATH="${BIN_DIR}/${BIN_NAME}"
|
||||
|
||||
# # Copy the release build binary to the result location
|
||||
# cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}"
|
||||
|
||||
# # Also strip if possible
|
||||
# if [ -n "${STRIP}" ]; then
|
||||
# "${STRIP}" "${BIN_PATH}"
|
||||
# fi
|
||||
|
||||
# # Let subsequent steps know where to find the (stripped) bin
|
||||
# echo ::set-output name=BIN_PATH::${BIN_PATH}
|
||||
# echo ::set-output name=BIN_NAME::${BIN_NAME}
|
||||
|
||||
- name: Set testing options
|
||||
id: test-options
|
||||
shell: bash
|
||||
run: |
|
||||
# test only library unit tests and binary for arm-type targets
|
||||
unset CARGO_TEST_OPTIONS
|
||||
unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--lib --bin ${PROJECT_NAME}" ;; esac;
|
||||
echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS}
|
||||
|
||||
- name: Build tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.job.use-cross }}
|
||||
command: build
|
||||
args: --locked --tests --target=${{ matrix.job.target }}
|
||||
|
||||
# - name: Run tests
|
||||
# uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# use-cross: ${{ matrix.job.use-cross }}
|
||||
# command: test
|
||||
# args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/target
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
src/ui/inline.rs
|
||||
@@ -6,3 +7,4 @@ extractor
|
||||
__pycache__
|
||||
src/version.rs
|
||||
*dmg
|
||||
sciter.dll
|
||||
|
||||
BIN
128x128.png
Normal file
BIN
128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
BIN
128x128@2x.png
Normal file
BIN
128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -3,7 +3,6 @@
|
||||
RustDesk welcomes contribution from everyone. Here are the guidelines if you are
|
||||
thinking of helping us:
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions to RustDesk or its dependencies should be made in the form of GitHub
|
||||
@@ -23,7 +22,7 @@ efforts from contributors on the same issue.
|
||||
master you may be asked to rebase your changes.
|
||||
|
||||
- Commits should be as small as possible, while ensuring that each commit is
|
||||
correct independently (i.e., each commit should compile and pass tests).
|
||||
correct independently (i.e., each commit should compile and pass tests).
|
||||
|
||||
- Commits should be accompanied by a Developer Certificate of Origin
|
||||
(http://developercertificate.org) sign-off, which indicates that you (and
|
||||
@@ -40,10 +39,8 @@ For specific git instructions, see [GitHub workflow 101](https://github.com/serv
|
||||
|
||||
## Conduct
|
||||
|
||||
We follow the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct).
|
||||
|
||||
We follow the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct).
|
||||
|
||||
## Communication
|
||||
|
||||
RustDesk contributors frequent the [Discord](https://discord.gg/nDceKgxnkV).
|
||||
|
||||
|
||||
1518
Cargo.lock
generated
1518
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.1.6"
|
||||
version = "1.1.8"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2018"
|
||||
build= "build.rs"
|
||||
@@ -17,30 +17,32 @@ default = ["use_dasp"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
whoami = "1.1"
|
||||
whoami = "1.2"
|
||||
scrap = { path = "libs/scrap" }
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
enigo = { path = "libs/enigo" }
|
||||
sys-locale = "0.1"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
cfg-if = "1.0"
|
||||
lazy_static = "1.4"
|
||||
sha2 = "0.9"
|
||||
sha2 = "0.10"
|
||||
repng = "0.2"
|
||||
libc = "0.2"
|
||||
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
|
||||
flexi_logger = "0.17"
|
||||
flexi_logger = "0.22"
|
||||
runas = "0.2"
|
||||
magnum-opus = { git = "https://github.com/open-trade/magnum-opus" }
|
||||
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
|
||||
rubato = { version = "0.8", optional = true }
|
||||
rubato = { version = "0.10", optional = true }
|
||||
samplerate = { version = "0.2", optional = true }
|
||||
async-trait = "0.1"
|
||||
crc32fast = "1.2"
|
||||
crc32fast = "1.3"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
clap = "2.33"
|
||||
clap = "2.34"
|
||||
rpassword = "5.0"
|
||||
base64 = "0.13"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android")))'.dependencies]
|
||||
cpal = { git = "https://github.com/open-trade/cpal" }
|
||||
@@ -51,12 +53,13 @@ mac_address = "1.1"
|
||||
sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
|
||||
ctrlc = "3.2"
|
||||
arboard = "2.0"
|
||||
clipboard-master = "3.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
#systray = { git = "https://github.com/open-trade/systray-rs" }
|
||||
systray = { git = "https://github.com/liyue201/systray-rs" }
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
winreg = "0.7"
|
||||
windows-service = { git = 'https://github.com/mullvad/windows-service-rs.git' }
|
||||
winreg = "0.10"
|
||||
windows-service = "0.4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
@@ -66,15 +69,15 @@ core-foundation = "0.9"
|
||||
core-graphics = "0.22"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libpulse-simple-binding = "2.16"
|
||||
libpulse-binding = "2.16"
|
||||
libpulse-simple-binding = "2.24"
|
||||
libpulse-binding = "2.25"
|
||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||
|
||||
[target.'cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))'.dependencies]
|
||||
psutil = "3.2"
|
||||
psutil = { version = "3.2", features = [ "process" ], git = "https://github.com/open-trade/rust-psutil" }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.9"
|
||||
android_logger = "0.10"
|
||||
|
||||
[workspace]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo"]
|
||||
|
||||
@@ -7,7 +7,7 @@ RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134
|
||||
RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics
|
||||
RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus
|
||||
|
||||
RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user
|
||||
RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user
|
||||
WORKDIR /home/user
|
||||
RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
USER user
|
||||
|
||||
49
README-DE.md
49
README-DE.md
@@ -5,7 +5,7 @@
|
||||
<a href="#auf-docker-kompilieren">Docker</a> •
|
||||
<a href="#dateistruktur">Dateistruktur</a> •
|
||||
<a href="#screenshots">Screenshots</a><br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren</b>
|
||||
</p>
|
||||
|
||||
@@ -13,61 +13,67 @@ Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.re
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Das hier ist ein Programm was man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Es ist eine gute Alternative zu Programmen wie TeamViewer und AnyDesk! Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren rendezvous/relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/blog/id-relay-set/) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Das hier ist ein Programm was man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren rendezvous/relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/blog/id-relay-set/) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start.
|
||||
|
||||
[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Kostenlose öffentliche Server
|
||||
|
||||
Hier sind die Server die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
|
||||
|
||||
| Standort | Serverart | Spezifikationen | Kommentare |
|
||||
| --------- | ------------- | ------------------ | ---------------------------------------- |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM | |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
| Standort | Serverart | Spezifikationen | Kommentare |
|
||||
| --------- | ------------- | ------------------ | ---------- |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM | |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
Die Desktop Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Die groben Schritte zum Kompilieren
|
||||
* Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor
|
||||
|
||||
* Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu
|
||||
- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor
|
||||
|
||||
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
|
||||
- Linux/MacOS: `vcpkg install libvpx libyuv opus`
|
||||
- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu
|
||||
|
||||
* Nutze `cargo run`
|
||||
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
|
||||
- Linux/MacOS: `vcpkg install libvpx libyuv opus`
|
||||
|
||||
- Nutze `cargo run`
|
||||
|
||||
## Kompilieren auf Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### vcpkg installieren
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
@@ -75,6 +81,7 @@ vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### libvpx reparieren (Für Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
@@ -87,6 +94,7 @@ cd
|
||||
```
|
||||
|
||||
### Kompilieren
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
@@ -98,6 +106,10 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Ändere Wayland zu X11 (Xorg)
|
||||
|
||||
RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen.
|
||||
|
||||
## Auf Docker Kompilieren
|
||||
|
||||
Beginne damit das Repository zu klonen und den Docker Container zu bauen:
|
||||
@@ -109,6 +121,7 @@ docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
@@ -127,9 +140,6 @@ target/release/rustdesk
|
||||
|
||||
Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z.B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
|
||||
|
||||
### Ändere Wayland zu X11 (Xorg)
|
||||
RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen.
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen
|
||||
@@ -142,10 +152,11 @@ RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.o
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
74
README-ES.md
74
README-ES.md
@@ -5,7 +5,7 @@
|
||||
<a href="#como-compilar-con-docker">Docker</a> •
|
||||
<a href="#estructura-de-archivos">Estructura</a> •
|
||||
<a href="#captura-de-pantalla">Captura de pantalla</a><br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>Necesitamos tu ayuda para traducir este README a tu idioma</b>
|
||||
</p>
|
||||
|
||||
@@ -13,77 +13,86 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.re
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Gran alternativa a TeamViewer o AnyDesk. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/blog/id-relay-set/), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/blog/id-relay-set/), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk agradece la contribución de todo el mundo. Ve [`CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda inicial.
|
||||
RustDesk agradece la contribución de todo el mundo. Ve [`CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda inicial.
|
||||
|
||||
[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Servidores gratis de uso público
|
||||
|
||||
A continuación se muestran los servidores que está utilizando de forma gratuita, puede cambiar en algún momento. Si no estás cerca de uno de ellos, tu red puede ser lenta.
|
||||
|
||||
- Seoul, AWS lightsail, 1 VCPU/0.5G RAM
|
||||
- Singapore, Vultr, 1 VCPU/1G RAM
|
||||
- Dallas, Vultr, 1 VCPU/1G RAM
|
||||
|
||||
## Dependencies
|
||||
|
||||
La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo..
|
||||
La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo..
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Pasos para compilar desde el inicio
|
||||
* Prepara el entono de desarrollode Rust y el entorno de compilación de C++ y Rust.
|
||||
|
||||
* Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente.
|
||||
- Prepara el entono de desarrollode Rust y el entorno de compilación de C++ y Rust.
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/Osx: vcpkg install libvpx libyuv opus
|
||||
|
||||
* run `cargo run`
|
||||
- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente.
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/Osx: vcpkg install libvpx libyuv opus
|
||||
|
||||
- run `cargo run`
|
||||
|
||||
## Como compilar en linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Install vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
|
||||
cd ..
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Soluciona libvpx (For Fedora)
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Soluciona libvpx (For Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Compila
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
@@ -95,6 +104,10 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Cambia Wayland a X11 (Xorg)
|
||||
|
||||
RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME.
|
||||
|
||||
## Como compilar con Docker
|
||||
|
||||
Empieza clonando el repositorio y compilando el contenedor de docker:
|
||||
@@ -104,6 +117,7 @@ git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando:
|
||||
|
||||
```sh
|
||||
@@ -124,9 +138,6 @@ target/release/rustdesk
|
||||
|
||||
Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También hay que tener en cuenta que otros subcomandos de carga como `install` o `run` no estan actualmente soportados via este metodo y podrían requerir ser instalados dentro del contenedor y no en el host.
|
||||
|
||||
### Cambia Wayland a X11 (Xorg)
|
||||
RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME.
|
||||
|
||||
## Estructura de archivos
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, configuración, tcp/udp wrapper, protobuf, fs funciones para transferencia de ficheros, y alguna función de utilidad.
|
||||
@@ -139,10 +150,11 @@ RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma
|
||||
|
||||
## Captura de pantalla
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
161
README-FI.md
Normal file
161
README-FI.md
Normal file
@@ -0,0 +1,161 @@
|
||||
<p align="center">
|
||||
<img src="logo-header.svg" alt="RustDesk - Etätyöpöytäsi"><br>
|
||||
<a href="#free-public-servers">Palvelimet</a> •
|
||||
<a href="#raw-steps-to-build">Rakenna</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Rakenne</a> •
|
||||
<a href="#snapshot">Tilannevedos</a><br>
|
||||
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>] | [<a href="README-FI.md">Suomi</a>]<br>
|
||||
<b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b>
|
||||
</p>
|
||||
|
||||
Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetuksia. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/blog/id-relay-set/), tai [kirjoita oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi.
|
||||
|
||||
[**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Vapaita julkisia palvelimia
|
||||
|
||||
Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas.
|
||||
| Sijainti | Myyjä | Määrittely |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## Riippuvuudet
|
||||
|
||||
Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Rakentamisaskeleet harppoen
|
||||
|
||||
- Valmistele Rust-kehitysympäristö ja C++-rakentamisympäristö
|
||||
|
||||
- Asenna [vcpkg](https://github.com/microsoft/vcpkg), ja aseta `VCPKG_ROOT`-ympäristömuuttuja oikein
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- aja `cargo run`
|
||||
|
||||
## Kuinka rakentaa Linuxissa
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Asenna vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Korjaa libvpx (Fedora-linux-versiota varten)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Rakenna
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Vaihda Wayland-ympäristö X11 (Xorg)-ympäristöön
|
||||
|
||||
RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamaan Xorg oletus GNOME-istuntona.
|
||||
|
||||
## Kuinka rakennetaan Dockerin kanssa
|
||||
|
||||
Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Sitten, joka kerta kun sinun on rakennettava sovellus, aja seuraava komento:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Huomaa, että ensimmäinen rakentaminen saattaa kestää pitempään ennen kuin riippuvuudet on siirretty välimuistiin, seuraavat rakentamiset ovat nopeampia. Lisäksi, jos sinun on määritettävä eri argumentteja rakentamiskomennolle, saatat tehdä sen niin, että komennon lopussa <OPTIONAL-ARGS>`-kohdassa. Esimerkiksi, jos haluat rakentaa optimoidun julkaisuversion, sinun on ajettava komento yllä siten, että sitä seuraa argumentti `---release`. Suoritettava tiedosto on saatavilla järjestelmäsi kohdehakemistossa, ja se voidaan suorittaa seuraavan kera:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Tai, jos olet suorittamassa jakeluversion suoritettavaa tiedostoa:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Varmista, että suoritat näitä komentoja RustDesktop-tietovaraston juurihakemistossa, muutoin sovellus ei ehkä löydä vaadittuja resursseja. Huomaa myös, että muita cargo-alikomentoja kuten `install` tai `run` ei nykyisin tueta tässä menetelmässä, koska ne asentavat tai suorittavat ohjelman säiliön sisällä eikä isäntäohjelman sisällä.
|
||||
|
||||
## Tiedostorakenne
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs-funktiot tiedostosiirtoon, ja jotkut muut apuohjelmafunktiot
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: näyttökaappaukset
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Graafinen käyttöliittymä
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||
|
||||
## Tilannekuvat
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
44
README-FR.md
44
README-FR.md
@@ -5,7 +5,7 @@
|
||||
<a href="#comment-construire-avec-docker">Docker</a> -
|
||||
<a href="#structure-du-projet">Structure</a> -
|
||||
<a href="#images">Images</a><br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-ES.md">Española</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-ES.md">Española</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>.
|
||||
</p>
|
||||
|
||||
@@ -13,14 +13,16 @@ Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Une excellente alternative à TeamViewer et AnyDesk ! Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/blog/id-relay-set/), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/blog/id-relay-set/), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk accueille les contributions de tout le monde. Voir [`CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations.
|
||||
RustDesk accueille les contributions de tout le monde. Voir [`CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations.
|
||||
|
||||
[**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Serveurs publics libres
|
||||
|
||||
Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent.
|
||||
|
||||
- Séoul, AWS lightsail, 1 VCPU/0.5G RAM
|
||||
- Singapour, Vultr, 1 VCPU/1G RAM
|
||||
- Dallas, Vultr, 1 VCPU/1G RAM
|
||||
@@ -29,42 +31,47 @@ Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut ch
|
||||
|
||||
Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so)
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Étapes brutes de la compilation/build
|
||||
* Préparez votre environnement de développement Rust et votre environnement de compilation C++.
|
||||
|
||||
* Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
|
||||
- Préparez votre environnement de développement Rust et votre environnement de compilation C++.
|
||||
|
||||
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/Osx : vcpkg install libvpx libyuv opus
|
||||
|
||||
* Exécuter `cargo run`
|
||||
- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
|
||||
|
||||
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/Osx : vcpkg install libvpx libyuv opus
|
||||
|
||||
- Exécuter `cargo run`
|
||||
|
||||
## Comment compiler/build sous Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Installer vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
@@ -72,6 +79,7 @@ vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Corriger libvpx (Pour Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
@@ -84,6 +92,7 @@ cd
|
||||
```
|
||||
|
||||
### Construire
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
@@ -95,6 +104,10 @@ mv libsciter-gtk.so target/debug
|
||||
Exécution du cargo
|
||||
```
|
||||
|
||||
### Changer Wayland en X11 (Xorg)
|
||||
|
||||
RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) pour configurer Xorg comme la session GNOME par défaut.
|
||||
|
||||
## Comment construire avec Docker
|
||||
|
||||
Commencez par cloner le dépôt et construire le conteneur Docker :
|
||||
@@ -116,6 +129,7 @@ Notez que le premier build peut prendre plus de temps avant que les dépendances
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Ou, si vous exécutez un exécutable provenant d'une release :
|
||||
|
||||
```sh
|
||||
@@ -124,9 +138,6 @@ target/release/rustdesk
|
||||
|
||||
Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du référentiel RustDesk, sinon l'application ne pourra pas trouver les ressources requises. Notez également que les autres sous-commandes de cargo telles que `install` ou `run` ne sont pas actuellement supportées par cette méthode car elles installeraient ou exécuteraient le programme à l'intérieur du conteneur au lieu de l'hôte.
|
||||
|
||||
### Changer Wayland en X11 (Xorg)
|
||||
RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) pour configurer Xorg comme la session GNOME par défaut.
|
||||
|
||||
## Structure du projet
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)** : codec vidéo, config, wrapper tcp/udp, protobuf, fonctions fs pour le transfert de fichiers, et quelques autres fonctions utilitaires.
|
||||
@@ -139,10 +150,11 @@ RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme
|
||||
|
||||
## Images
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
164
README-JP.md
Normal file
164
README-JP.md
Normal file
@@ -0,0 +1,164 @@
|
||||
<p align="center">
|
||||
<img src="logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
|
||||
</p>
|
||||
|
||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/blog/id-relay-set/) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## 無料のパブリックサーバー
|
||||
|
||||
下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## 依存関係
|
||||
|
||||
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## ビルド手順
|
||||
|
||||
- Rust開発環境とC ++ビルド環境を準備します
|
||||
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- run `cargo run`
|
||||
|
||||
## Linuxでのビルド手順
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Install vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Fix libvpx (For Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wayland の場合、X11(Xorg)に変更します
|
||||
|
||||
RustDeskはWaylandをサポートしていません。
|
||||
[こちら](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) を確認して、XorgをデフォルトのGNOMEセッションとして構成します。
|
||||
|
||||
## Dockerでビルドする方法
|
||||
|
||||
リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。
|
||||
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します。
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に
|
||||
`---release` を実行します。できあがった実行ファイルは、システムのターゲット・フォルダに格納され、次のコマンドで実行できます。
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
あるいは、リリース用の実行ファイルを実行している場合:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
|
||||
|
||||
## File Structure
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
||||
|
||||
## Snapshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
161
README-ML.md
Normal file
161
README-ML.md
Normal file
@@ -0,0 +1,161 @@
|
||||
<p align="center">
|
||||
<img src="logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b>
|
||||
</p>
|
||||
|
||||
ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്വെയർ. ബോക്സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/blog/id-relay-set/), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
എല്ലാവരുടെയും സംഭാവനയെ RustDesk സ്വാഗതം ചെയ്യുന്നു. ആരംഭിക്കുന്നതിനുള്ള സഹായത്തിന് [`CONTRIBUTING.md`](CONTRIBUTING.md) കാണുക.
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## സൗജന്യ പൊതു സെർവറുകൾ
|
||||
|
||||
നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്വർക്ക് സ്ലോ ആയേക്കാം.
|
||||
| സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## ഡിപെൻഡൻസികൾ
|
||||
|
||||
ഡെസ്ക്ടോപ്പ് പതിപ്പുകൾ GUI-യ്ക്കായി [sciter](https://sciter.com/) ഉപയോഗിക്കുന്നു, ദയവായി സ്സൈറ്റർ ഡൈനാമിക് ലൈബ്രറി സ്വയം ഡൗൺലോഡ് ചെയ്യുക.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## നിർമ്മിക്കാനുള്ള അസംസ്കൃത പടികൾ
|
||||
|
||||
- നിങ്ങളുടെ Rust development envയും and C++ build envയും തയ്യാറാക്കുക
|
||||
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg) ഇൻസ്റ്റാൾ ചെയ്ത് `VCPKG_ROOT` env വേരിയബിൾ ശരിയായി സജ്ജമാക്കുക
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- run `cargo run`
|
||||
|
||||
## ലിനക്സിൽ എങ്ങനെ നിർമ്മിക്കാം
|
||||
|
||||
### ഉബുണ്ടു 18 (ഡെബിയൻ 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### ഫെഡോറ 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### ആർച് (മഞ്ചാരോ)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### vcpkg ഇൻസ്റ്റാൾ ചെയ്യുക
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### libvpx പരിഹരിക്കുക (ഫെഡോറയ്ക്ക്)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### നിർമാണം
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### വേലാൻഡ് X11 (Xorg) ആയി മാറ്റുക
|
||||
|
||||
RustDesk Wayland-നെ പിന്തുണയ്ക്കുന്നില്ല. സ്ഥിരസ്ഥിതി ഗ്നോം സെഷനായി Xorg കോൺഫിഗർ ചെയ്യുന്നതിന് [ഇത്](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) പരിശോധിക്കുക.
|
||||
|
||||
## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം
|
||||
|
||||
റെപ്പോസിറ്റോറി ക്ലോണുചെയ്ത് ഡോക്കർ കണ്ടെയ്നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
തുടർന്ന്, ഓരോ തവണയും നിങ്ങൾ ആപ്ലിക്കേഷൻ നിർമ്മിക്കേണ്ടതുണ്ട്, ഇനിപ്പറയുന്ന കമാൻഡ് പ്രവർത്തിപ്പിക്കുക:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
ഡിപൻഡൻസികൾ കാഷെ ചെയ്യുന്നതിനുമുമ്പ് ആദ്യ ബിൽഡ് കൂടുതൽ സമയമെടുത്തേക്കാം, തുടർന്നുള്ള ബിൽഡുകൾ വേഗത്തിലാകും. കൂടാതെ, നിങ്ങൾക്ക് ബിൽഡ് കമാൻഡിലേക്ക് വ്യത്യസ്ത ആർഗ്യുമെന്റുകൾ വ്യക്തമാക്കണമെങ്കിൽ, കമാൻഡിന്റെ അവസാനം `<OPTIONAL-ARGS>` സ്ഥാനത്ത് നിങ്ങൾക്ക് അങ്ങനെ ചെയ്യാം. ഉദാഹരണത്തിന്, നിങ്ങൾ ഒരു ഒപ്റ്റിമൈസ് ചെയ്ത റിലീസ് പതിപ്പ് നിർമ്മിക്കാൻ ആഗ്രഹിക്കുന്നുവെങ്കിൽ, മുകളിലുള്ള കമാൻഡ് തുടർന്ന് `---release` നിങ്ങൾ പ്രവർത്തിപ്പിക്കും. തത്ഫലമായുണ്ടാകുന്ന എക്സിക്യൂട്ടബിൾ നിങ്ങളുടെ സിസ്റ്റത്തിലെ ടാർഗെറ്റ് ഫോൾഡറിൽ ലഭ്യമാകും, കൂടാതെ ഇത് ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കാം:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
അല്ലെങ്കിൽ, നിങ്ങൾ ഒരു റിലീസ് എക്സിക്യൂട്ടബിൾ പ്രവർത്തിപ്പിക്കുകയാണെങ്കിൽ:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
RustDesk റിപ്പോസിറ്ററിയുടെ റൂട്ടിൽ നിന്നാണ് നിങ്ങൾ ഈ കമാൻഡുകൾ പ്രവർത്തിപ്പിക്കുന്നതെന്ന് ദയവായി ഉറപ്പാക്കുക, അല്ലാത്തപക്ഷം ആപ്ലിക്കേഷന് ആവശ്യമായ ഉറവിടങ്ങൾ കണ്ടെത്താൻ കഴിഞ്ഞേക്കില്ല. ഹോസ്റ്റിന് പകരം കണ്ടെയ്നറിനുള്ളിൽ പ്രോഗ്രാം ഇൻസ്റ്റാൾ ചെയ്യുകയോ പ്രവർത്തിപ്പിക്കുകയോ ചെയ്യുന്നതിനാൽ, `install` അല്ലെങ്കിൽ `run` പോലുള്ള മറ്റ് കാർഗോ സബ്കമാൻഡുകൾ നിലവിൽ ഈ രീതിയെ പിന്തുണയ്ക്കുന്നില്ല എന്നതും ശ്രദ്ധിക്കുക.
|
||||
|
||||
## ഫയൽ ഘടന
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||
|
||||
## സ്നാപ്പ്ഷോട്ടുകൾ
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
163
README-NL.md
Normal file
163
README-NL.md
Normal file
@@ -0,0 +1,163 @@
|
||||
<p align="center">
|
||||
<img src="logo-header.svg" alt="RustDesk - Jouw verbinding op afstand"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Bouwen</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structuur</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal</b>
|
||||
</p>
|
||||
|
||||
Praat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Nog weer een applicatie voor toegang op afstand, geschreven in Rust. Werkt meteen, geen configuratie nodig. Je hebt volledig beheer over je data, zonder na te hoeven denken over veiligheid. Je kunt onze rendez-vous/relay-server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk verwelkomt bijdragen van iedereen. Zie [`CONTRIBUTING.md`](CONTRIBUTING.md) om te lezen hoe je van start kunt gaan.
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Gratis openbare servers
|
||||
|
||||
Onderstaande servers zijn de servers die je gratis kunt gebruiken, ze kunnen op termijn veranderen. Als je niet fysiek dichtbij een van deze servers bent, kan je verbinding traag werken.
|
||||
| Locatie | Aanbieder | Specificaties |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## Afhankelijkheden
|
||||
|
||||
Desktopversies gebruiken [sciter](https://sciter.com/) voor de grafische schil. Gelieve zelf de sciter-library te downloaden.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Handmatige bouwinstructies
|
||||
|
||||
- Bereid je Rust-ontwikkelomgeving en C++-bouwomgeving voor.
|
||||
|
||||
- Installeer [vcpkg](https://github.com/microsoft/vcpkg) en configureer de `VCPKG_ROOT` omgevingsvariabele op de juiste manier:
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- Voer uit: `cargo run`
|
||||
|
||||
## Bouwen op Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Installatie van vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Fix voor libvpx (voor Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Bouwen
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wissel van Wayland naar X11 (Xorg)
|
||||
|
||||
RustDesk ondersteunt Wayland niet. Lees [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) hoe je Xorg als standaardsessie kunt instellen voor GNOME.
|
||||
|
||||
## Bouwen met Docker
|
||||
|
||||
Kloon eerst deze repository en bouw de Docker-container:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Voer vervolgens de volgende commando's uit iedere keer dat je de applicatie opnieuw moet bouwen:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Let op dat de eerste build langer kan duren omdat de dependencies nog niet zijn gecached; latere builds zullen sneller zijn. Als je extra command line arguments wilt toevoegen aan het build-commando, dan kun je dat doen aan het einde van de opdrachtregel in plaats van `<OPTIONAL-ARGS>`. Bijvoorbeeld: als je een geoptimaliseerde releaseversie wilt bouwen, draai dan het bovenstaande commando gevolgd door `---release`.
|
||||
|
||||
Het uitvoerbare bestand, in debug-modus, zal verschijnen in de target-map, en kan als volgt worden uitgevoerd:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Als je een release-versie hebt gebouwd, is het commando als volgt:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Zorg ervoor dat je deze commando's van de root van de RustDesk-repository uitvoert, anders kan het programma de nodige afhankelijkheden mogelijk niet vinden. Let ook op dat andere cargo-subcommando's zoals `install` en `run` zijn momenteel niet ondersteund, aangezien deze zouden worden uitgevoerd in een container in plaats van op de host.
|
||||
|
||||
## Bestandsstructuur
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: videocodec, configuratie, TCP/UDP-wrapper, protobuf, bestandssysteemfuncties voor bestandsoverdracht en nog wat andere nuttige functies
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: schermopname
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platformspecifieke muis- en toetsenbordbeheer
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: geluids-, klembord-, invoer- en video-services, netwerkverbindingen
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: voor het opzetten van peer-verbindingen
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicatie met [rustdesk-server](https://github.com/rustdesk/rustdesk-server), afwachten van redirect op afstand (TCP hole punching) of een relayed verbinding
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platformspecifieke code
|
||||
|
||||
## Snapshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
51
README-PL.md
51
README-PL.md
@@ -5,7 +5,7 @@
|
||||
<a href="#jak-kompilować-za-pomocą-dockera">Docker</a> •
|
||||
<a href="#struktura-plików">Struktura</a> •
|
||||
<a href="#migawkisnapshoty">Snapshot</a><br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-ES.md">Española</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-ES.md">Española</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b>
|
||||
</p>
|
||||
|
||||
@@ -13,60 +13,66 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Świetna alternatywa dla TeamViewera i AnyDesk! Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/blog/id-relay-set/), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/blog/id-relay-set/), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk zaprasza do współpracy każdego. Zobacz [`CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu.
|
||||
RustDesk zaprasza do współpracy każdego. Zobacz [`CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu.
|
||||
|
||||
[**POBIERZ KOMPILACJE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Darmowe Serwery Publiczne
|
||||
|
||||
Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
|
||||
| Lokalizacja | Dostawca | Specyfikacja |
|
||||
| Lokalizacja | Dostawca | Specyfikacja |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapur | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapur | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## Zależności
|
||||
|
||||
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać bibliotekę dynamiczną sciter samodzielnie.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Podstawowe kroki do kompilacji.
|
||||
* Przygotuj środowisko programistyczne Rust i środowisko programowania C++
|
||||
|
||||
* Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw `VCPKG_ROOT` env zmienną prawidłowo
|
||||
- Przygotuj środowisko programistyczne Rust i środowisko programowania C++
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
* uruchom `cargo run`
|
||||
- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw `VCPKG_ROOT` env zmienną prawidłowo
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- uruchom `cargo run`
|
||||
|
||||
## Jak Kompilować na Linuxie
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Zainstaluj vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
@@ -74,6 +80,7 @@ vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Fix libvpx (For Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
@@ -86,6 +93,7 @@ cd
|
||||
```
|
||||
|
||||
### Kompilacja
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
@@ -97,6 +105,10 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Zmień Wayland na X11 (Xorg)
|
||||
|
||||
RustDesk nie obsługuje Waylanda. Sprawdź [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) by skonfigurować Xorg jako domyślną sesję GNOME.
|
||||
|
||||
## Jak kompilować za pomocą Dockera
|
||||
|
||||
Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker:
|
||||
@@ -115,7 +127,6 @@ docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user
|
||||
|
||||
Zauważ, że pierwsza kompilacja może potrwać dłużej zanim zależności zostaną zbuforowane, kolejne będą szybsze. Dodatkowo, jeśli potrzebujesz określić inne argumenty dla polecenia budowania, możesz to zrobić na końcu komendy w miejscu `<OPTIONAL-ARGS>`. Na przykład, jeśli chciałbyś zbudować zoptymalizowaną wersję wydania, uruchomiłbyś powyższą komendę a następnie `---release`. Powstały plik wykonywalny będzie dostępny w folderze docelowym w twoim systemie, i może być uruchomiony z:
|
||||
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
@@ -128,9 +139,6 @@ target/release/rustdesk
|
||||
|
||||
Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium RustDesk, w przeciwnym razie aplikacja może nie być w stanie znaleźć wymaganych zasobów. Należy również pamiętać, że inne podpolecenia ładowania, takie jak `install` lub `run` nie są obecnie obsługiwane za pomocą tej metody, ponieważ instalowałyby lub uruchamiały program wewnątrz kontenera zamiast na hoście.
|
||||
|
||||
### Zmień Wayland na X11 (Xorg)
|
||||
RustDesk nie obsługuje Waylanda. Sprawdź [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) by skonfigurować Xorg jako domyślną sesję GNOME.
|
||||
|
||||
## Struktura plików
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, config, wrapper tcp/udp, protobuf, funkcje fs do transferu plików i kilka innych funkcji użytkowych
|
||||
@@ -143,10 +151,11 @@ RustDesk nie obsługuje Waylanda. Sprawdź [this](https://docs.fedoraproject.org
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: specyficzny dla danej platformy kod
|
||||
|
||||
## Migawki(Snapshoty)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
111
README-ZH.md
111
README-ZH.md
@@ -5,23 +5,25 @@
|
||||
<a href="#使用Docker编译">Docker</a> •
|
||||
<a href="#文件结构">结构</a> •
|
||||
<a href="#截图">截图</a><br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
[<a href="README.md">English</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
</p>
|
||||
|
||||
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
远程桌面软件,开箱即用,无需任何配置,替代TeamViewer和AnyDesk。您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器,
|
||||
远程桌面软件,开箱即用,无需任何配置。您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器,
|
||||
或者[自己设置](https://rustdesk.com/blog/id-relay-set/),
|
||||
亦或者[开发您的版本](https://github.com/rustdesk/rustdesk-server-demo)。
|
||||
|
||||
欢迎大家贡献代码, 请看 [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
欢迎大家贡献代码, 请看 [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
[**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## 免费公共服务器
|
||||
|
||||
以下是您免费使用的服务器,它可能会随着时间的推移而变化。如果您不靠近其中之一,您的网络可能会很慢。
|
||||
|
||||
- 首尔, AWS lightsail, 1 VCPU/0.5G RAM
|
||||
- 新加坡, Vultr, 1 VCPU/1G RAM
|
||||
- 达拉斯, Vultr, 1 VCPU/1G RAM
|
||||
@@ -30,49 +32,55 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
|
||||
|
||||
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## 基本构建步骤
|
||||
* 请准备好Rust开发环境和C++编译环境
|
||||
|
||||
* 安装[vcpkg](https://github.com/microsoft/vcpkg), 正确设置`VCPKG_ROOT`环境变量
|
||||
- 请准备好 Rust 开发环境和 C++编译环境
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/Osx: vcpkg install libvpx libyuv opus
|
||||
|
||||
* 运行 `cargo run`
|
||||
- 安装[vcpkg](https://github.com/microsoft/vcpkg), 正确设置`VCPKG_ROOT`环境变量
|
||||
|
||||
## 在Linux上编译
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/Osx: vcpkg install libvpx libyuv opus
|
||||
|
||||
- 运行 `cargo run`
|
||||
|
||||
## 在 Linux 上编译
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### 安装vcpkg
|
||||
### 安装 vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### 修复libvpx (仅仅针对Fedora)
|
||||
### 修复 libvpx (仅仅针对 Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
@@ -85,6 +93,7 @@ cd
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
@@ -96,7 +105,12 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
## 使用Docker编译
|
||||
### 把 Wayland 修改成 X11 (Xorg)
|
||||
|
||||
RustDesk 暂时不支持 Wayland,不过正在积极开发中.
|
||||
请查看[this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)配置 X11.
|
||||
|
||||
## 使用 Docker 编译
|
||||
|
||||
首先克隆存储库并构建 docker 容器:
|
||||
|
||||
@@ -106,16 +120,64 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
针对国内网络访问问题,可以做以下几点优化:
|
||||
|
||||
1. Dockerfile 中修改系统的源到国内镜像
|
||||
|
||||
```
|
||||
在Dockerfile的RUN apt update之前插入两行:
|
||||
|
||||
RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list
|
||||
RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list
|
||||
```
|
||||
|
||||
2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码:
|
||||
|
||||
```
|
||||
RUN echo '[source.crates-io]' > ~/.cargo/config \
|
||||
&& echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \
|
||||
&& echo '# 替换成你偏好的镜像源' >> ~/.cargo/config \
|
||||
&& echo "replace-with = 'sjtu'" >> ~/.cargo/config \
|
||||
&& echo '# 上海交通大学' >> ~/.cargo/config \
|
||||
&& echo '[source.sjtu]' >> ~/.cargo/config \
|
||||
&& echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \
|
||||
&& echo '' >> ~/.cargo/config
|
||||
```
|
||||
|
||||
3. Dockerfile 中加入代理的 env
|
||||
|
||||
```
|
||||
在User root后插入两行
|
||||
|
||||
ENV http_proxy=http://host:port
|
||||
ENV https_proxy=http://host:port
|
||||
```
|
||||
|
||||
4. docker build 命令后面加上 proxy 参数
|
||||
```
|
||||
docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port
|
||||
```
|
||||
|
||||
然后,每次需要构建应用程序时,运行以下命令:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
运行若遇到无权限问题,出现以下提示:
|
||||
|
||||
请注意,第一次构建可能需要比较长的时间,因为需要缓存依赖项,后续构建会更快。此外,如果您需要为构建命令指定不同的参数,
|
||||
```
|
||||
usermod: user user is currently used by process 1
|
||||
groupmod: Permission denied.
|
||||
groupmod: cannot lock /etc/group; try again later.
|
||||
```
|
||||
|
||||
可以尝试把`-e PUID="$(id -u)" -e PGID="$(id -g)"`参数去掉。(出现这一问题的原因是容器中的 entrypoint 脚本中判定 uid 和 gid 与给定的环境变量不一致时会修改 user 的 uid 和 gid 重新运行,但是重新运行时取不到环境变量中的 uid 和 gid 了,会再次进入 uid 与 gid 与给定值不一致的逻辑分支)
|
||||
|
||||
请注意,第一次构建可能需要比较长的时间,因为需要缓存依赖项(国内网络经常出现拉取失败,可多尝试几次),后续构建会更快。此外,如果您需要为构建命令指定不同的参数,
|
||||
您可以在命令末尾的 `<OPTIONAL-ARGS>` 位置执行此操作。例如,如果你想构建一个优化的发布版本,你可以在命令后跟 `---release`。
|
||||
将在target下产生可执行程序,请通过以下方式运行调试版本:
|
||||
将在 target 下产生可执行程序,请通过以下方式运行调试版本:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
@@ -129,26 +191,23 @@ target/release/rustdesk
|
||||
请确保您从 RustDesk 存储库的根目录运行这些命令,否则应用程序可能无法找到所需的资源。另请注意,此方法当前不支持其他`Cargo`子命令,
|
||||
例如 `install` 或 `run`,因为运行在容器里,而不是宿主机上。
|
||||
|
||||
### 把Wayland修改成X11 (Xorg)
|
||||
RustDesk暂时不支持Wayland,不过正在积极开发中.
|
||||
请查看[this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)配置X11.
|
||||
|
||||
## 文件结构
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video服务, 已经连接实现
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video 服务, 已经连接实现
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继)
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
||||
|
||||
## 截图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
54
README.md
54
README.md
@@ -5,68 +5,74 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>We need your help to translate this README to your native language</b>
|
||||
[<a href="README-ZH.md">中文</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PT.md">Português</a>]<br>
|
||||
<b>We need your help to translate this README and <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> to your native language</b>
|
||||
</p>
|
||||
|
||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. Great alternative to TeamViewer and AnyDesk! You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/blog/id-relay-set/), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/blog/id-relay-set/), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for help getting started.
|
||||
RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for help getting started.
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Free Public Servers
|
||||
|
||||
Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
|
||||
| Location | Vendor | Specification |
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
||||
| Dallas | Vultr | 1 VCPU / 1GB RAM | |
|
||||
|
||||
## Dependencies
|
||||
|
||||
Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Raw steps to build
|
||||
* Prepare your Rust development env and C++ build env
|
||||
|
||||
* Install [vcpkg](https://github.com/microsoft/vcpkg), and set `VCPKG_ROOT` env variable correctly
|
||||
- Prepare your Rust development env and C++ build env
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
* run `cargo run`
|
||||
- Install [vcpkg](https://github.com/microsoft/vcpkg), and set `VCPKG_ROOT` env variable correctly
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
|
||||
- run `cargo run`
|
||||
|
||||
## How to build on Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio
|
||||
```
|
||||
|
||||
### Install vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
|
||||
git checkout 2021.12.01
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
@@ -74,6 +80,7 @@ vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Fix libvpx (For Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
@@ -86,6 +93,7 @@ cd
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
@@ -94,9 +102,13 @@ cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Change Wayland to X11 (Xorg)
|
||||
|
||||
RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session.
|
||||
|
||||
## How to build with Docker
|
||||
|
||||
Begin by cloning the repository and building the docker container:
|
||||
@@ -127,9 +139,6 @@ target/release/rustdesk
|
||||
|
||||
Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application may be unable to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
|
||||
|
||||
### Change Wayland to X11 (Xorg)
|
||||
RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session.
|
||||
|
||||
## File Structure
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
|
||||
@@ -142,10 +151,11 @@ RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/e
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||
|
||||
## Snapshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
10
SECURITY.md
10
SECURITY.md
@@ -2,11 +2,11 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.1.x | :white_check_mark: |
|
||||
| 1.x | :white_check_mark: |
|
||||
| Below 1.0 | :x: |
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| 1.1.x | :white_check_mark: |
|
||||
| 1.x | :white_check_mark: |
|
||||
| Below 1.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$(id -u)" != "${PUID:-1000}" ] || [ "$(id -g)" != "${PGID:-1000}" ]; then
|
||||
usermod -o -u "${PUID:-1000}" user
|
||||
groupmod -o -g "${PGID:-1000}" user
|
||||
chown -R user /home/user
|
||||
sudo -u user /entrypoint $@
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd $HOME/rustdesk
|
||||
. $HOME/.cargo/env
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ fn main() {
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
let mut enigo = Enigo::new();
|
||||
|
||||
enigo.key_down(Key::Layout('a'));
|
||||
enigo.key_down(Key::Layout('a')).ok();
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
enigo.key_up(Key::Layout('a'));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ fn main() {
|
||||
enigo.key_sequence("Hello World! here is a lot of text ❤️");
|
||||
|
||||
// select all
|
||||
enigo.key_down(Key::Control);
|
||||
enigo.key_down(Key::Control).ok();
|
||||
enigo.key_click(Key::Layout('a'));
|
||||
enigo.key_up(Key::Control);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ fn main() {
|
||||
enigo.mouse_move_to(500, 200);
|
||||
thread::sleep(wait_time);
|
||||
|
||||
enigo.mouse_down(MouseButton::Left);
|
||||
enigo.mouse_down(MouseButton::Left).ok();
|
||||
thread::sleep(wait_time);
|
||||
|
||||
enigo.mouse_move_relative(100, 100);
|
||||
|
||||
@@ -16,7 +16,7 @@ fn main() {
|
||||
println!("{:?}", time);
|
||||
|
||||
// select all
|
||||
enigo.key_down(Key::Control);
|
||||
enigo.key_down(Key::Control).ok();
|
||||
enigo.key_click(Key::Layout('a'));
|
||||
enigo.key_up(Key::Control);
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
protobuf = { version = "3.0.0-pre", git = "https://github.com/stepancheg/rust-protobuf" }
|
||||
tokio = { version = "1.10", features = ["full"] }
|
||||
protobuf = "3.0.0-alpha.2"
|
||||
tokio = { version = "1.15", features = ["full"] }
|
||||
tokio-util = { version = "0.6", features = ["full"] }
|
||||
futures = "0.3"
|
||||
bytes = "1.0"
|
||||
bytes = "1.1"
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||
zstd = "0.9"
|
||||
quinn = {version = "0.6", optional = true }
|
||||
quinn = {version = "0.8", optional = true }
|
||||
anyhow = "1.0"
|
||||
futures-util = "0.3"
|
||||
directories-next = "2.0"
|
||||
@@ -28,6 +28,8 @@ confy = { git = "https://github.com/open-trade/confy" }
|
||||
dirs-next = "2.0"
|
||||
filetime = "0.2"
|
||||
sodiumoxide = "0.2"
|
||||
regex = "1.4"
|
||||
tokio-socks = { git = "https://github.com/fufesou/tokio-socks" }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
@@ -36,7 +38,7 @@ mac_address = "1.1"
|
||||
quic = ["quinn"]
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen-pure = { version = "3.0.0-pre", git = "https://github.com/stepancheg/rust-protobuf" }
|
||||
protobuf-codegen-pure = "3.0.0-alpha.2"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
|
||||
@@ -359,7 +359,6 @@ message PublicKey {
|
||||
|
||||
message SignedId {
|
||||
bytes id = 1;
|
||||
bytes pk = 2;
|
||||
}
|
||||
|
||||
message AudioFormat {
|
||||
|
||||
@@ -48,6 +48,7 @@ message PunchHoleSent {
|
||||
string id = 2;
|
||||
string relay_server = 3;
|
||||
NatType nat_type = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message RegisterPk {
|
||||
@@ -75,8 +76,8 @@ message PunchHoleResponse {
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 1;
|
||||
OFFLINE = 2;
|
||||
LICENCE_MISMATCH = 3;
|
||||
LICENCE_OVERUSE = 4;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
@@ -110,6 +111,7 @@ message RelayResponse {
|
||||
bytes pk = 5;
|
||||
}
|
||||
string refuse_reason = 6;
|
||||
string version = 7;
|
||||
}
|
||||
|
||||
message SoftwareUpdate { string url = 1; }
|
||||
@@ -128,6 +130,7 @@ message LocalAddr {
|
||||
bytes local_addr = 2;
|
||||
string relay_server = 3;
|
||||
string id = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message RendezvousMessage {
|
||||
|
||||
@@ -6,14 +6,13 @@ use sodiumoxide::crypto::sign;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
net::SocketAddr,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub const APP_NAME: &str = "RustDesk";
|
||||
pub const BIND_INTERFACE: &str = "0.0.0.0";
|
||||
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
|
||||
pub const CONNECT_TIMEOUT: u64 = 18_000;
|
||||
pub const COMPRESS_LEVEL: i32 = 3;
|
||||
@@ -55,6 +54,12 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[
|
||||
pub const RENDEZVOUS_PORT: i32 = 21116;
|
||||
pub const RELAY_PORT: i32 = 21117;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum NetworkType {
|
||||
Direct,
|
||||
ProxySocks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
@@ -71,6 +76,16 @@ pub struct Config {
|
||||
keys_confirmed: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub struct Socks5Server {
|
||||
#[serde(default)]
|
||||
pub proxy: String,
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// more variable configs
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct Config2 {
|
||||
@@ -85,6 +100,9 @@ pub struct Config2 {
|
||||
#[serde(default)]
|
||||
serial: i32,
|
||||
|
||||
#[serde(default)]
|
||||
socks: Option<Socks5Server>,
|
||||
|
||||
// the other scalar value must before this
|
||||
#[serde(default)]
|
||||
pub options: HashMap<String, String>,
|
||||
@@ -153,7 +171,10 @@ fn patch(path: PathBuf) -> PathBuf {
|
||||
{
|
||||
if _tmp == "/root" {
|
||||
if let Ok(output) = std::process::Command::new("whoami").output() {
|
||||
let user = String::from_utf8_lossy(&output.stdout).to_string().trim().to_owned();
|
||||
let user = String::from_utf8_lossy(&output.stdout)
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_owned();
|
||||
if user != "root" {
|
||||
return format!("/home/{}", user).into();
|
||||
}
|
||||
@@ -271,8 +292,8 @@ impl Config {
|
||||
return "".into();
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
pub fn log_path() -> PathBuf {
|
||||
#[allow(unreachable_code)]
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(path) = dirs_next::home_dir().as_mut() {
|
||||
@@ -324,10 +345,10 @@ impl Config {
|
||||
|
||||
#[inline]
|
||||
pub fn get_any_listen_addr() -> SocketAddr {
|
||||
format!("{}:0", BIND_INTERFACE).parse().unwrap()
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)
|
||||
}
|
||||
|
||||
pub fn get_rendezvous_server() -> SocketAddr {
|
||||
pub fn get_rendezvous_server() -> String {
|
||||
let mut rendezvous_server = Self::get_option("custom-rendezvous-server");
|
||||
if rendezvous_server.is_empty() {
|
||||
rendezvous_server = CONFIG2.write().unwrap().rendezvous_server.clone();
|
||||
@@ -341,11 +362,7 @@ impl Config {
|
||||
if !rendezvous_server.contains(":") {
|
||||
rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT);
|
||||
}
|
||||
if let Ok(addr) = crate::to_socket_addr(&rendezvous_server) {
|
||||
addr
|
||||
} else {
|
||||
Self::get_any_listen_addr()
|
||||
}
|
||||
rendezvous_server
|
||||
}
|
||||
|
||||
pub fn get_rendezvous_servers() -> Vec<String> {
|
||||
@@ -485,6 +502,9 @@ impl Config {
|
||||
|
||||
pub fn set_key_pair(pair: (Vec<u8>, Vec<u8>)) {
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if config.key_pair == pair {
|
||||
return;
|
||||
}
|
||||
config.key_pair = pair;
|
||||
config.store();
|
||||
}
|
||||
@@ -517,6 +537,9 @@ impl Config {
|
||||
|
||||
pub fn set_options(v: HashMap<String, String>) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if config.options == v {
|
||||
return;
|
||||
}
|
||||
config.options = v;
|
||||
config.store();
|
||||
}
|
||||
@@ -616,6 +639,26 @@ impl Config {
|
||||
pub fn get_remote_id() -> String {
|
||||
CONFIG2.read().unwrap().remote_id.clone()
|
||||
}
|
||||
|
||||
pub fn set_socks(socks: Option<Socks5Server>) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if config.socks == socks {
|
||||
return;
|
||||
}
|
||||
config.socks = socks;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_socks() -> Option<Socks5Server> {
|
||||
CONFIG2.read().unwrap().socks.clone()
|
||||
}
|
||||
|
||||
pub fn get_network_type() -> NetworkType {
|
||||
match &CONFIG2.read().unwrap().socks {
|
||||
None => NetworkType::Direct,
|
||||
Some(_) => NetworkType::ProxySocks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PEERS: &str = "peers";
|
||||
@@ -685,6 +728,32 @@ impl PeerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct Fav {
|
||||
#[serde(default)]
|
||||
pub peers: Vec<String>,
|
||||
}
|
||||
|
||||
impl Fav {
|
||||
pub fn load() -> Fav {
|
||||
let _ = CONFIG.read().unwrap(); // for lock
|
||||
match confy::load_path(&Config::file_("_fav")) {
|
||||
Ok(fav) => fav,
|
||||
Err(err) => {
|
||||
log::error!("Failed to load fav: {}", err);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(peers: Vec<String>) {
|
||||
let f = Fav { peers };
|
||||
if let Err(err) = confy::store_path(Config::file_("_fav"), f) {
|
||||
log::error!("Failed to store fav: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -193,7 +193,7 @@ pub struct TransferJob {
|
||||
file: Option<File>,
|
||||
total_size: u64,
|
||||
finished_size: u64,
|
||||
transfered: u64,
|
||||
transferred: u64,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -268,8 +268,8 @@ impl TransferJob {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transfered(&self) -> u64 {
|
||||
self.transfered
|
||||
pub fn transferred(&self) -> u64 {
|
||||
self.transferred
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -332,7 +332,7 @@ impl TransferJob {
|
||||
self.file.as_mut().unwrap().write_all(&block.data).await?;
|
||||
self.finished_size += block.data.len() as u64;
|
||||
}
|
||||
self.transfered += block.data.len() as u64;
|
||||
self.transferred += block.data.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ impl TransferJob {
|
||||
compressed = true;
|
||||
}
|
||||
}
|
||||
self.transfered += buf.len() as u64;
|
||||
self.transferred += buf.len() as u64;
|
||||
}
|
||||
Ok(Some(FileTransferBlock {
|
||||
id: self.id,
|
||||
|
||||
@@ -9,12 +9,13 @@ pub use protobuf;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead},
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs},
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
path::Path,
|
||||
time::{self, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
pub use tokio;
|
||||
pub use tokio_util;
|
||||
pub mod socket_client;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
pub use env_logger;
|
||||
@@ -26,7 +27,11 @@ pub use anyhow::{self, bail};
|
||||
pub use futures_util;
|
||||
pub mod config;
|
||||
pub mod fs;
|
||||
pub use regex;
|
||||
pub use sodiumoxide;
|
||||
pub use tokio_socks;
|
||||
pub use tokio_socks::IntoTargetAddr;
|
||||
pub use tokio_socks::TargetAddr;
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
pub type Stream = quic::Connection;
|
||||
@@ -147,14 +152,6 @@ pub fn get_version_from_url(url: &str) -> String {
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
pub fn to_socket_addr(host: &str) -> ResultType<SocketAddr> {
|
||||
let addrs: Vec<SocketAddr> = host.to_socket_addrs()?.collect();
|
||||
if addrs.is_empty() {
|
||||
bail!("Failed to solve {}", host);
|
||||
}
|
||||
Ok(addrs[0])
|
||||
}
|
||||
|
||||
pub fn gen_version() {
|
||||
let mut file = File::create("./src/version.rs").unwrap();
|
||||
for line in read_lines("Cargo.toml").unwrap() {
|
||||
@@ -179,6 +176,14 @@ where
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
pub fn get_version_number(v: &str) -> i64 {
|
||||
let mut n = 0;
|
||||
for x in v.split(".") {
|
||||
n = n * 1000 + x.parse::<i64>().unwrap_or(0);
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
91
libs/hbb_common/src/socket_client.rs
Normal file
91
libs/hbb_common/src/socket_client.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::{
|
||||
config::{Config, NetworkType},
|
||||
tcp::FramedStream,
|
||||
udp::FramedSocket,
|
||||
ResultType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
use tokio_socks::{IntoTargetAddr, TargetAddr};
|
||||
|
||||
fn to_socket_addr(host: &str) -> ResultType<SocketAddr> {
|
||||
use std::net::ToSocketAddrs;
|
||||
host.to_socket_addrs()?.next().context("Failed to solve")
|
||||
}
|
||||
|
||||
pub fn get_target_addr(host: &str) -> ResultType<TargetAddr<'static>> {
|
||||
let addr = match Config::get_network_type() {
|
||||
NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?,
|
||||
NetworkType::ProxySocks => host.into_target_addr()?,
|
||||
}
|
||||
.to_owned();
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: &str) -> String {
|
||||
let mut host = host.to_owned();
|
||||
if !host.contains(":") {
|
||||
host = format!("{}:{}", host, 0);
|
||||
}
|
||||
|
||||
match Config::get_network_type() {
|
||||
NetworkType::Direct => match to_socket_addr(&host) {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
},
|
||||
NetworkType::ProxySocks => match &host.into_target_addr() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>(
|
||||
target: T,
|
||||
local: SocketAddr,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<FramedStream> {
|
||||
let target_addr = target.into_target_addr()?;
|
||||
|
||||
if let Some(conf) = Config::get_socks() {
|
||||
FramedStream::connect(
|
||||
conf.proxy.as_str(),
|
||||
target_addr,
|
||||
local,
|
||||
conf.username.as_str(),
|
||||
conf.password.as_str(),
|
||||
ms_timeout,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)?
|
||||
.next()
|
||||
.context("Invalid target addr")?;
|
||||
Ok(FramedStream::new(addr, local, ms_timeout).await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
|
||||
match Config::get_socks() {
|
||||
None => Ok(FramedSocket::new(local).await?),
|
||||
Some(conf) => {
|
||||
let socket = FramedSocket::new_proxy(
|
||||
conf.proxy.as_str(),
|
||||
local,
|
||||
conf.username.as_str(),
|
||||
conf.password.as_str(),
|
||||
ms_timeout,
|
||||
)
|
||||
.await?;
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rebind_udp<T: ToSocketAddrs>(local: T) -> ResultType<Option<FramedSocket>> {
|
||||
match Config::get_network_type() {
|
||||
NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,31 @@ use futures::{SinkExt, StreamExt};
|
||||
use protobuf::Message;
|
||||
use sodiumoxide::crypto::secretbox::{self, Key, Nonce};
|
||||
use std::{
|
||||
io::{Error, ErrorKind},
|
||||
io::{self, Error, ErrorKind},
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::net::{lookup_host, TcpListener, TcpSocket, TcpStream, ToSocketAddrs};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
||||
};
|
||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs};
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
pub struct FramedStream(Framed<TcpStream, BytesCodec>, Option<(Key, u64, u64)>);
|
||||
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
||||
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send>);
|
||||
|
||||
pub struct FramedStream(
|
||||
Framed<DynTcpStream, BytesCodec>,
|
||||
SocketAddr,
|
||||
Option<(Key, u64, u64)>,
|
||||
u64,
|
||||
);
|
||||
|
||||
impl Deref for FramedStream {
|
||||
type Target = Framed<TcpStream, BytesCodec>;
|
||||
type Target = Framed<DynTcpStream, BytesCodec>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@@ -26,6 +41,20 @@ impl DerefMut for FramedStream {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DynTcpStream {
|
||||
type Target = Box<dyn TcpStreamTrait + Send>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DynTcpStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
||||
@@ -34,7 +63,7 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std:
|
||||
if reuse {
|
||||
// windows has no reuse_port, but it's reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic bahavior
|
||||
// though may introduce nondeterministic behavior
|
||||
#[cfg(unix)]
|
||||
socket.set_reuseport(true)?;
|
||||
socket.set_reuseaddr(true)?;
|
||||
@@ -44,8 +73,8 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std:
|
||||
}
|
||||
|
||||
impl FramedStream {
|
||||
pub async fn new<T: ToSocketAddrs, T2: ToSocketAddrs>(
|
||||
remote_addr: T,
|
||||
pub async fn new<T1: ToSocketAddrs, T2: ToSocketAddrs>(
|
||||
remote_addr: T1,
|
||||
local_addr: T2,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
@@ -56,23 +85,86 @@ impl FramedStream {
|
||||
new_socket(local_addr, true)?.connect(remote_addr),
|
||||
)
|
||||
.await??;
|
||||
return Ok(Self(Framed::new(stream, BytesCodec::new()), None));
|
||||
let addr = stream.local_addr()?;
|
||||
return Ok(Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
bail!("could not resolve to any address");
|
||||
}
|
||||
|
||||
pub fn from(stream: TcpStream) -> Self {
|
||||
Self(Framed::new(stream, BytesCodec::new()), None)
|
||||
pub async fn connect<'a, 't, P, T1, T2>(
|
||||
proxy: P,
|
||||
target: T1,
|
||||
local: T2,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self>
|
||||
where
|
||||
P: ToProxyAddrs,
|
||||
T1: IntoTargetAddr<'t>,
|
||||
T2: ToSocketAddrs,
|
||||
{
|
||||
if let Some(local) = lookup_host(&local).await?.next() {
|
||||
if let Some(proxy) = proxy.to_proxy_addrs().next().await {
|
||||
let stream =
|
||||
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??;
|
||||
let stream = if username.trim().is_empty() {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5Stream::connect_with_socket(stream, target),
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5Stream::connect_with_password_and_socket(
|
||||
stream, target, username, password,
|
||||
),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
let addr = stream.local_addr()?;
|
||||
return Ok(Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
));
|
||||
};
|
||||
};
|
||||
bail!("could not resolve to any address");
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
self.3 = ms;
|
||||
}
|
||||
|
||||
pub fn from(stream: impl TcpStreamTrait + Send + 'static, addr: SocketAddr) -> Self {
|
||||
Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.0.codec_mut().set_raw();
|
||||
self.1 = None;
|
||||
self.2 = None;
|
||||
}
|
||||
|
||||
pub fn is_secured(&self) -> bool {
|
||||
self.1.is_some()
|
||||
self.2.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -83,24 +175,29 @@ impl FramedStream {
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
|
||||
let mut msg = msg;
|
||||
if let Some(key) = self.1.as_mut() {
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
key.1 += 1;
|
||||
let nonce = Self::get_nonce(key.1);
|
||||
msg = secretbox::seal(&msg, &nonce, &key.0);
|
||||
}
|
||||
self.0.send(bytes::Bytes::from(msg)).await?;
|
||||
self.send_bytes(bytes::Bytes::from(msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
|
||||
self.0.send(bytes).await?;
|
||||
if self.3 > 0 {
|
||||
super::timeout(self.3, self.0.send(bytes)).await??;
|
||||
} else {
|
||||
self.0.send(bytes).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
let mut res = self.0.next().await;
|
||||
if let Some(key) = self.1.as_mut() {
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
if let Some(Ok(bytes)) = res.as_mut() {
|
||||
key.2 += 1;
|
||||
let nonce = Self::get_nonce(key.2);
|
||||
@@ -128,7 +225,7 @@ impl FramedStream {
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, key: Key) {
|
||||
self.1 = Some((key, 0, 0));
|
||||
self.2 = Some((key, 0, 0));
|
||||
}
|
||||
|
||||
fn get_nonce(seqnum: u64) -> Nonce {
|
||||
@@ -152,3 +249,35 @@ pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<
|
||||
bail!("could not resolve to any address");
|
||||
}
|
||||
}
|
||||
|
||||
impl Unpin for DynTcpStream {}
|
||||
|
||||
impl AsyncRead for DynTcpStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for DynTcpStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + AsyncWrite + Unpin> TcpStreamTrait for R {}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
use crate::{bail, ResultType};
|
||||
use bytes::BytesMut;
|
||||
use anyhow::anyhow;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use protobuf::Message;
|
||||
use socket2::{Domain, Socket, Type};
|
||||
use std::{
|
||||
io::Error,
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use tokio::{net::ToSocketAddrs, net::UdpSocket};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::{ToSocketAddrs, UdpSocket};
|
||||
use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs};
|
||||
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
|
||||
|
||||
pub struct FramedSocket(UdpFramed<BytesCodec>);
|
||||
|
||||
impl Deref for FramedSocket {
|
||||
type Target = UdpFramed<BytesCodec>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
pub enum FramedSocket {
|
||||
Direct(UdpFramed<BytesCodec>),
|
||||
ProxySocks(Socks5UdpFramed),
|
||||
}
|
||||
|
||||
fn new_socket(addr: SocketAddr, reuse: bool) -> Result<Socket, std::io::Error> {
|
||||
@@ -29,7 +22,7 @@ fn new_socket(addr: SocketAddr, reuse: bool) -> Result<Socket, std::io::Error> {
|
||||
if reuse {
|
||||
// windows has no reuse_port, but it's reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic bahavior
|
||||
// though may introduce nondeterministic behavior
|
||||
#[cfg(unix)]
|
||||
socket.set_reuse_port(true)?;
|
||||
socket.set_reuse_address(true)?;
|
||||
@@ -38,52 +31,110 @@ fn new_socket(addr: SocketAddr, reuse: bool) -> Result<Socket, std::io::Error> {
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl DerefMut for FramedSocket {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FramedSocket {
|
||||
pub async fn new<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
|
||||
let socket = UdpSocket::bind(addr).await?;
|
||||
Ok(Self(UdpFramed::new(socket, BytesCodec::new())))
|
||||
Ok(Self::Direct(UdpFramed::new(socket, BytesCodec::new())))
|
||||
}
|
||||
|
||||
#[allow(clippy::never_loop)]
|
||||
pub async fn new_reuse<T: std::net::ToSocketAddrs>(addr: T) -> ResultType<Self> {
|
||||
for addr in addr.to_socket_addrs()? {
|
||||
return Ok(Self(UdpFramed::new(
|
||||
UdpSocket::from_std(new_socket(addr, true)?.into_udp_socket())?,
|
||||
let socket = new_socket(addr, true)?.into_udp_socket();
|
||||
return Ok(Self::Direct(UdpFramed::new(
|
||||
UdpSocket::from_std(socket)?,
|
||||
BytesCodec::new(),
|
||||
)));
|
||||
}
|
||||
bail!("could not resolve to any address");
|
||||
}
|
||||
|
||||
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
|
||||
proxy: P,
|
||||
local: T,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
let framed = if username.trim().is_empty() {
|
||||
super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await??
|
||||
} else {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
log::trace!(
|
||||
"Socks5 udp connected, local addr: {:?}, target addr: {}",
|
||||
framed.local_addr(),
|
||||
framed.socks_addr()
|
||||
);
|
||||
Ok(Self::ProxySocks(framed))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl Message, addr: SocketAddr) -> ResultType<()> {
|
||||
self.0
|
||||
.send((bytes::Bytes::from(msg.write_to_bytes().unwrap()), addr))
|
||||
.await?;
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
msg: &impl Message,
|
||||
addr: impl IntoTargetAddr<'_>,
|
||||
) -> ResultType<()> {
|
||||
let addr = addr.into_target_addr()?.to_owned();
|
||||
let send_data = Bytes::from(msg.write_to_bytes()?);
|
||||
let _ = match self {
|
||||
Self::Direct(f) => match addr {
|
||||
TargetAddr::Ip(addr) => f.send((send_data, addr)).await?,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::ProxySocks(f) => f.send((send_data, addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/68733302/1926020
|
||||
#[inline]
|
||||
pub async fn send_raw(
|
||||
&mut self,
|
||||
msg: &'static [u8],
|
||||
addr: impl IntoTargetAddr<'static>,
|
||||
) -> ResultType<()> {
|
||||
let addr = addr.into_target_addr()?.to_owned();
|
||||
|
||||
let _ = match self {
|
||||
Self::Direct(f) => match addr {
|
||||
TargetAddr::Ip(addr) => f.send((Bytes::from(msg), addr)).await?,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: &'static [u8], addr: SocketAddr) -> ResultType<()> {
|
||||
self.0.send((bytes::Bytes::from(msg), addr)).await?;
|
||||
Ok(())
|
||||
pub async fn next(&mut self) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
match self {
|
||||
Self::Direct(f) => match f.next().await {
|
||||
Some(Ok((data, addr))) => {
|
||||
Some(Ok((data, addr.into_target_addr().ok()?.to_owned())))
|
||||
}
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
Self::ProxySocks(f) => match f.next().await {
|
||||
Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))),
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<(BytesMut, SocketAddr), Error>> {
|
||||
self.0.next().await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<(BytesMut, SocketAddr), Error>> {
|
||||
pub async fn next_timeout(
|
||||
&mut self,
|
||||
ms: u64,
|
||||
) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
if let Ok(res) =
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), self.0.next()).await
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await
|
||||
{
|
||||
res
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,6 @@ struct Args {
|
||||
flag_time: Option<u64>,
|
||||
flag_fps: u64,
|
||||
flag_bv: u32,
|
||||
flag_ba: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
|
||||
@@ -9,8 +9,8 @@ extern "C" {
|
||||
src_stride_argb: c_int,
|
||||
dst_argb: *mut u8,
|
||||
dst_stride_argb: c_int,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
mode: c_int,
|
||||
) -> c_int;
|
||||
|
||||
|
||||
@@ -3,32 +3,20 @@ pub mod gdi;
|
||||
pub use gdi::CapturerGDI;
|
||||
|
||||
use winapi::{
|
||||
shared::dxgi::{
|
||||
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIResource, IDXGISurface,
|
||||
IID_IDXGIFactory1, IID_IDXGISurface, DXGI_MAP_READ, DXGI_OUTPUT_DESC,
|
||||
DXGI_RESOURCE_PRIORITY_MAXIMUM,
|
||||
shared::{
|
||||
dxgi::*,
|
||||
dxgi1_2::*,
|
||||
dxgitype::*,
|
||||
minwindef::{DWORD, FALSE, TRUE, UINT},
|
||||
ntdef::LONG,
|
||||
windef::HMONITOR,
|
||||
winerror::*,
|
||||
// dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
|
||||
},
|
||||
shared::dxgi1_2::IDXGIOutputDuplication,
|
||||
// shared::dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE},
|
||||
shared::dxgi1_2::{IDXGIOutput1, IID_IDXGIOutput1},
|
||||
shared::dxgitype::DXGI_MODE_ROTATION,
|
||||
shared::minwindef::{DWORD, FALSE, TRUE, UINT},
|
||||
shared::ntdef::LONG,
|
||||
shared::windef::HMONITOR,
|
||||
shared::winerror::{
|
||||
DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_INVALID_CALL, DXGI_ERROR_NOT_CURRENTLY_AVAILABLE,
|
||||
DXGI_ERROR_SESSION_DISCONNECTED, DXGI_ERROR_UNSUPPORTED, DXGI_ERROR_WAIT_TIMEOUT,
|
||||
E_ACCESSDENIED, E_INVALIDARG, S_OK,
|
||||
um::{
|
||||
d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*,
|
||||
winnt::HRESULT, winuser::*,
|
||||
},
|
||||
um::d3d11::{
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, IID_ID3D11Texture2D,
|
||||
D3D11_CPU_ACCESS_READ, D3D11_SDK_VERSION, D3D11_USAGE_STAGING,
|
||||
},
|
||||
um::d3dcommon::D3D_DRIVER_TYPE_UNKNOWN,
|
||||
um::unknwnbase::IUnknown,
|
||||
um::wingdi::*,
|
||||
um::winnt::HRESULT,
|
||||
um::winuser::*,
|
||||
};
|
||||
|
||||
pub struct ComPtr<T>(*mut T);
|
||||
@@ -54,12 +42,11 @@ pub struct Capturer {
|
||||
duplication: ComPtr<IDXGIOutputDuplication>,
|
||||
fastlane: bool,
|
||||
surface: ComPtr<IDXGISurface>,
|
||||
data: *const u8,
|
||||
len: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
use_yuv: bool,
|
||||
yuv: Vec<u8>,
|
||||
rotated: Vec<u8>,
|
||||
gdi_capturer: Option<CapturerGDI>,
|
||||
gdi_buffer: Vec<u8>,
|
||||
}
|
||||
@@ -158,10 +145,9 @@ impl Capturer {
|
||||
width: display.width() as usize,
|
||||
height: display.height() as usize,
|
||||
display,
|
||||
data: ptr::null(),
|
||||
len: 0,
|
||||
use_yuv,
|
||||
yuv: Vec::new(),
|
||||
rotated: Vec::new(),
|
||||
gdi_capturer,
|
||||
gdi_buffer: Vec::new(),
|
||||
})
|
||||
@@ -181,10 +167,9 @@ impl Capturer {
|
||||
self.gdi_capturer.take();
|
||||
}
|
||||
|
||||
unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<()> {
|
||||
unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> {
|
||||
let mut frame = ptr::null_mut();
|
||||
let mut info = mem::MaybeUninit::uninit().assume_init();
|
||||
self.data = ptr::null();
|
||||
|
||||
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
|
||||
let frame = ComPtr(frame);
|
||||
@@ -200,9 +185,7 @@ impl Capturer {
|
||||
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
|
||||
wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?;
|
||||
}
|
||||
self.data = rect.pBits;
|
||||
self.len = self.height * rect.Pitch as usize;
|
||||
Ok(())
|
||||
Ok((rect.pBits, rect.Pitch))
|
||||
}
|
||||
|
||||
// copy from GPU memory to system memory
|
||||
@@ -257,8 +240,42 @@ impl Capturer {
|
||||
}
|
||||
} else {
|
||||
self.unmap();
|
||||
self.load_frame(timeout)?;
|
||||
slice::from_raw_parts(self.data, self.len)
|
||||
let r = self.load_frame(timeout)?;
|
||||
let rotate = match self.display.rotation() {
|
||||
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0,
|
||||
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Unknown roration".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if rotate == 0 {
|
||||
slice::from_raw_parts(r.0, r.1 as usize * self.height)
|
||||
} else {
|
||||
self.rotated.resize(self.width * self.height * 4, 0);
|
||||
crate::common::ARGBRotate(
|
||||
r.0,
|
||||
r.1,
|
||||
self.rotated.as_mut_ptr(),
|
||||
4 * self.width as i32,
|
||||
if rotate == 180 {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
} as _,
|
||||
if rotate != 180 {
|
||||
self.width
|
||||
} else {
|
||||
self.height
|
||||
} as _,
|
||||
rotate,
|
||||
);
|
||||
&self.rotated[..]
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok({
|
||||
@@ -478,7 +495,10 @@ pub struct Display {
|
||||
gdi: bool,
|
||||
}
|
||||
|
||||
// https://github.com/dchapyshev/aspia/blob/59233c5d01a4d03ed6de19b03ce77d61a6facf79/source/base/desktop/win/screen_capture_utils.cc
|
||||
// optimized for updated region
|
||||
// https://github.com/dchapyshev/aspia/blob/master/source/base/desktop/win/dxgi_output_duplicator.cc
|
||||
// rotation
|
||||
// https://github.com/bryal/dxgcap-rs/blob/master/src/lib.rs
|
||||
|
||||
impl Display {
|
||||
pub fn width(&self) -> LONG {
|
||||
|
||||
127
src/client.rs
127
src/client.rs
@@ -13,8 +13,8 @@ use hbb_common::{
|
||||
message_proto::*,
|
||||
protobuf::Message as _,
|
||||
rendezvous_proto::*,
|
||||
socket_client,
|
||||
sodiumoxide::crypto::{box_, secretbox, sign},
|
||||
tcp::FramedStream,
|
||||
timeout,
|
||||
tokio::time::Duration,
|
||||
AddrMangle, ResultType, Stream,
|
||||
@@ -34,6 +34,8 @@ pub const SEC30: Duration = Duration::from_secs(30);
|
||||
|
||||
pub struct Client;
|
||||
|
||||
pub use super::lang::*;
|
||||
|
||||
#[cfg(not(any(target_os = "android")))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AUDIO_HOST: Host = cpal::default_host();
|
||||
@@ -101,14 +103,40 @@ impl Drop for OboePlayer {
|
||||
|
||||
impl Client {
|
||||
pub async fn start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> {
|
||||
match Self::_start(peer, conn_type).await {
|
||||
Err(err) => {
|
||||
let err_str = err.to_string();
|
||||
if err_str.starts_with("Failed") {
|
||||
bail!(err_str + ": Please try later");
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(x) => Ok(x),
|
||||
}
|
||||
}
|
||||
|
||||
async fn _start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> {
|
||||
// to-do: remember the port for each peer, so that we can retry easier
|
||||
let any_addr = Config::get_any_listen_addr();
|
||||
if crate::is_ip(peer) {
|
||||
return Ok((
|
||||
socket_client::connect_tcp(
|
||||
crate::check_port(peer, RELAY_PORT + 1),
|
||||
any_addr,
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await?,
|
||||
true,
|
||||
));
|
||||
}
|
||||
let rendezvous_server = crate::get_rendezvous_server(1_000).await;
|
||||
log::info!("rendezvous server: {}", rendezvous_server);
|
||||
let mut socket = FramedStream::new(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT)
|
||||
.await
|
||||
.with_context(|| "Failed to connect to rendezvous server")?;
|
||||
let my_addr = socket.get_ref().local_addr()?;
|
||||
|
||||
let mut socket =
|
||||
socket_client::connect_tcp(&*rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT)
|
||||
.await?;
|
||||
let my_addr = socket.local_addr();
|
||||
let mut pk = Vec::new();
|
||||
let mut relay_server = "".to_owned();
|
||||
|
||||
@@ -141,7 +169,7 @@ impl Client {
|
||||
punch_hole_response::Failure::OFFLINE => {
|
||||
bail!("Remote desktop is offline");
|
||||
}
|
||||
punch_hole_response::Failure::LICENCE_MISMATCH => {
|
||||
punch_hole_response::Failure::LICENSE_MISMATCH => {
|
||||
bail!("Key mismatch");
|
||||
}
|
||||
_ => {
|
||||
@@ -203,7 +231,7 @@ impl Client {
|
||||
peer,
|
||||
pk,
|
||||
&relay_server,
|
||||
rendezvous_server,
|
||||
&rendezvous_server,
|
||||
time_used,
|
||||
peer_nat_type,
|
||||
my_nat_type,
|
||||
@@ -219,7 +247,7 @@ impl Client {
|
||||
peer_id: &str,
|
||||
pk: Vec<u8>,
|
||||
relay_server: &str,
|
||||
rendezvous_server: SocketAddr,
|
||||
rendezvous_server: &str,
|
||||
punch_time_used: u64,
|
||||
peer_nat_type: NatType,
|
||||
my_nat_type: i32,
|
||||
@@ -260,7 +288,8 @@ impl Client {
|
||||
}
|
||||
log::info!("peer address: {}, timeout: {}", peer, connect_timeout);
|
||||
let start = std::time::Instant::now();
|
||||
let mut conn = FramedStream::new(peer, local_addr, connect_timeout).await;
|
||||
// NOTICE: Socks5 is be used event in intranet. Which may be not a good way.
|
||||
let mut conn = socket_client::connect_tcp(peer, local_addr, connect_timeout).await;
|
||||
let direct = !conn.is_err();
|
||||
if conn.is_err() {
|
||||
if !relay_server.is_empty() {
|
||||
@@ -295,28 +324,51 @@ impl Client {
|
||||
}
|
||||
|
||||
async fn secure_connection(peer_id: &str, pk: Vec<u8>, conn: &mut Stream) -> ResultType<()> {
|
||||
let mut pk = pk;
|
||||
const RS_PK: &[u8; 32] = &[
|
||||
177, 155, 15, 73, 116, 147, 172, 11, 55, 38, 92, 168, 30, 116, 213, 196, 12, 134, 130,
|
||||
170, 181, 161, 192, 176, 132, 229, 139, 178, 17, 165, 150, 51,
|
||||
];
|
||||
if !pk.is_empty() {
|
||||
let tmp = sign::PublicKey(*RS_PK);
|
||||
if let Ok(data) = sign::verify(&pk, &tmp) {
|
||||
pk = data;
|
||||
} else {
|
||||
log::error!("Handshake failed: invalid public key from rendezvous server");
|
||||
pk.clear();
|
||||
}
|
||||
}
|
||||
if pk.len() != sign::PUBLICKEYBYTES {
|
||||
// send an empty message out in case server is setting up secure and waiting for first message
|
||||
conn.send(&Message::new()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut pk_ = [0u8; sign::PUBLICKEYBYTES];
|
||||
pk_[..].copy_from_slice(&pk);
|
||||
let pk = sign::PublicKey(pk_);
|
||||
let mut tmp = [0u8; sign::PUBLICKEYBYTES];
|
||||
tmp[..].copy_from_slice(&pk);
|
||||
let sign_pk = sign::PublicKey(tmp);
|
||||
match timeout(CONNECT_TIMEOUT, conn.next()).await? {
|
||||
Some(res) => {
|
||||
let bytes = res?;
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
if let Some(message::Union::signed_id(si)) = msg_in.union {
|
||||
let their_pk_b = if si.pk.len() == box_::PUBLICKEYBYTES {
|
||||
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
|
||||
pk_[..].copy_from_slice(&si.pk);
|
||||
box_::PublicKey(pk_)
|
||||
} else {
|
||||
bail!("Handshake failed: invalid public box key length from peer");
|
||||
};
|
||||
if let Ok(id) = sign::verify(&si.id, &pk) {
|
||||
if id == peer_id.as_bytes() {
|
||||
if let Ok(data) = sign::verify(&si.id, &sign_pk) {
|
||||
let s = String::from_utf8_lossy(&data);
|
||||
let mut it = s.split("\0");
|
||||
let id = it.next().unwrap_or_default();
|
||||
let pk =
|
||||
base64::decode(it.next().unwrap_or_default()).unwrap_or_default();
|
||||
let their_pk_b = if pk.len() == box_::PUBLICKEYBYTES {
|
||||
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
|
||||
pk_[..].copy_from_slice(&pk);
|
||||
box_::PublicKey(pk_)
|
||||
} else {
|
||||
log::error!(
|
||||
"Handshake failed: invalid public box key length from peer"
|
||||
);
|
||||
conn.send(&Message::new()).await?;
|
||||
return Ok(());
|
||||
};
|
||||
if id == peer_id {
|
||||
let (our_pk_b, out_sk_b) = box_::gen_keypair();
|
||||
let key = secretbox::gen_key();
|
||||
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||
@@ -330,7 +382,8 @@ impl Client {
|
||||
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
||||
conn.set_key(key);
|
||||
} else {
|
||||
bail!("Handshake failed: sign failure");
|
||||
log::error!("Handshake failed: sign failure");
|
||||
conn.send(&Message::new()).await?;
|
||||
}
|
||||
} else {
|
||||
// fall back to non-secure connection in case pk mismatch
|
||||
@@ -340,10 +393,12 @@ impl Client {
|
||||
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
||||
}
|
||||
} else {
|
||||
bail!("Handshake failed: invalid message type");
|
||||
log::error!("Handshake failed: invalid message type");
|
||||
conn.send(&Message::new()).await?;
|
||||
}
|
||||
} else {
|
||||
bail!("Handshake failed: invalid message format");
|
||||
log::error!("Handshake failed: invalid message format");
|
||||
conn.send(&Message::new()).await?;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -356,7 +411,7 @@ impl Client {
|
||||
async fn request_relay(
|
||||
peer: &str,
|
||||
relay_server: String,
|
||||
rendezvous_server: SocketAddr,
|
||||
rendezvous_server: &str,
|
||||
secure: bool,
|
||||
conn_type: ConnType,
|
||||
) -> ResultType<Stream> {
|
||||
@@ -365,9 +420,11 @@ impl Client {
|
||||
let mut uuid = "".to_owned();
|
||||
for i in 1..=3 {
|
||||
// use different socket due to current hbbs implement requiring different nat address for each attempt
|
||||
let mut socket = FramedStream::new(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT)
|
||||
.await
|
||||
.with_context(|| "Failed to connect to rendezvous server")?;
|
||||
let mut socket =
|
||||
socket_client::connect_tcp(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT)
|
||||
.await
|
||||
.with_context(|| "Failed to connect to rendezvous server")?;
|
||||
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
uuid = Uuid::new_v4().to_string();
|
||||
log::info!(
|
||||
@@ -410,7 +467,7 @@ impl Client {
|
||||
relay_server: String,
|
||||
conn_type: ConnType,
|
||||
) -> ResultType<Stream> {
|
||||
let mut conn = FramedStream::new(
|
||||
let mut conn = socket_client::connect_tcp(
|
||||
crate::check_port(relay_server, RELAY_PORT),
|
||||
Config::get_any_listen_addr(),
|
||||
CONNECT_TIMEOUT,
|
||||
@@ -547,7 +604,8 @@ impl AudioHandler {
|
||||
device: &Device,
|
||||
) -> ResultType<()> {
|
||||
let err_fn = move |err| {
|
||||
log::error!("an error occurred on stream: {}", err);
|
||||
// too many errors, will improve later
|
||||
log::trace!("an error occurred on stream: {}", err);
|
||||
};
|
||||
let audio_buffer = self.audio_buffer.clone();
|
||||
let stream = device.build_output_stream(
|
||||
@@ -582,7 +640,7 @@ pub struct VideoHandler {
|
||||
impl VideoHandler {
|
||||
pub fn new() -> Self {
|
||||
VideoHandler {
|
||||
decoder: Decoder::new(VideoCodecId::VP9, 1).unwrap(),
|
||||
decoder: Decoder::new(VideoCodecId::VP9, 0).unwrap(),
|
||||
rgb: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -658,6 +716,12 @@ impl LoginConfigHandler {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn set_option(&mut self, k: String, v: String) {
|
||||
let mut config = self.load_config();
|
||||
config.options.insert(k, v);
|
||||
self.save_config(config);
|
||||
}
|
||||
|
||||
pub fn save_view_style(&mut self, value: String) {
|
||||
let mut config = self.load_config();
|
||||
config.view_style = value;
|
||||
@@ -1086,7 +1150,6 @@ lazy_static::lazy_static! {
|
||||
("VK_F10", Key::ControlKey(ControlKey::F10)),
|
||||
("VK_F11", Key::ControlKey(ControlKey::F11)),
|
||||
("VK_F12", Key::ControlKey(ControlKey::F12)),
|
||||
("VK_F12", Key::ControlKey(ControlKey::F12)),
|
||||
("VK_ENTER", Key::ControlKey(ControlKey::Return)),
|
||||
("VK_CANCEL", Key::ControlKey(ControlKey::Cancel)),
|
||||
("VK_BACK", Key::ControlKey(ControlKey::Backspace)),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use arboard::Clipboard as ClipboardContext;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
anyhow::bail,
|
||||
compress::{compress as compress_func, decompress},
|
||||
config::{Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT},
|
||||
log,
|
||||
@@ -8,9 +9,7 @@ use hbb_common::{
|
||||
protobuf::Message as _,
|
||||
protobuf::ProtobufEnum,
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
tcp::FramedStream,
|
||||
tokio, ResultType,
|
||||
sleep, socket_client, tokio, ResultType,
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all};
|
||||
@@ -95,7 +94,10 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>)
|
||||
let side = if old.is_none() { "host" } else { "client" };
|
||||
let old = if let Some(old) = old { old } else { &CONTENT };
|
||||
*old.lock().unwrap() = content.clone();
|
||||
allow_err!(ctx.set_text(content));
|
||||
if !content.is_empty() {
|
||||
// empty content make ctx.set_text crash
|
||||
allow_err!(ctx.set_text(content));
|
||||
}
|
||||
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -236,11 +238,19 @@ pub fn test_nat_type() {
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn test_nat_type_() -> ResultType<bool> {
|
||||
log::info!("Testing nat ...");
|
||||
let is_direct = crate::ipc::get_socks_async(1_000).await.is_none(); // sync socks BTW
|
||||
let start = std::time::Instant::now();
|
||||
let rendezvous_server = get_rendezvous_server(100).await;
|
||||
let rendezvous_server = get_rendezvous_server(1_000).await;
|
||||
let server1 = rendezvous_server;
|
||||
let mut server2 = server1;
|
||||
server2.set_port(server1.port() - 1);
|
||||
let tmp: Vec<&str> = server1.split(":").collect();
|
||||
if tmp.len() != 2 {
|
||||
bail!("Invalid server address: {}", server1);
|
||||
}
|
||||
let port: u16 = tmp[1].parse()?;
|
||||
if port == 0 {
|
||||
bail!("Invalid server address: {}", server1);
|
||||
}
|
||||
let server2 = format!("{}:{}", tmp[0], port - 1);
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
let serial = Config::get_serial();
|
||||
msg_out.set_test_nat_request(TestNatRequest {
|
||||
@@ -249,15 +259,24 @@ async fn test_nat_type_() -> ResultType<bool> {
|
||||
});
|
||||
let mut port1 = 0;
|
||||
let mut port2 = 0;
|
||||
let server1 = socket_client::get_target_addr(&server1)?;
|
||||
let server2 = socket_client::get_target_addr(&server2)?;
|
||||
let mut addr = Config::get_any_listen_addr();
|
||||
for i in 0..2 {
|
||||
let mut socket = FramedStream::new(
|
||||
if i == 0 { &server1 } else { &server2 },
|
||||
let mut socket = socket_client::connect_tcp(
|
||||
if i == 0 {
|
||||
server1.clone()
|
||||
} else {
|
||||
server2.clone()
|
||||
},
|
||||
addr,
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
addr = socket.get_ref().local_addr()?;
|
||||
if is_direct {
|
||||
// to-do: should set NatType::UNKNOWN for proxy
|
||||
addr = socket.local_addr();
|
||||
}
|
||||
socket.send(&msg_out).await?;
|
||||
if let Some(Ok(bytes)) = socket.next_timeout(3000).await {
|
||||
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
|
||||
@@ -294,12 +313,12 @@ async fn test_nat_type_() -> ResultType<bool> {
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub async fn get_rendezvous_server(_ms_timeout: u64) -> std::net::SocketAddr {
|
||||
pub async fn get_rendezvous_server(_ms_timeout: u64) -> String {
|
||||
Config::get_rendezvous_server()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> std::net::SocketAddr {
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> String {
|
||||
crate::ipc::get_rendezvous_server(ms_timeout).await
|
||||
}
|
||||
|
||||
@@ -322,8 +341,8 @@ async fn test_rendezvous_server_() {
|
||||
for host in servers {
|
||||
futs.push(tokio::spawn(async move {
|
||||
let tm = std::time::Instant::now();
|
||||
if FramedStream::new(
|
||||
&crate::check_port(&host, RENDEZVOUS_PORT),
|
||||
if socket_client::connect_tcp(
|
||||
crate::check_port(&host, RENDEZVOUS_PORT),
|
||||
Config::get_any_listen_addr(),
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
@@ -403,17 +422,6 @@ pub fn is_modifier(evt: &KeyEvent) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: String) -> String {
|
||||
let mut host = host;
|
||||
if !host.contains(":") {
|
||||
host = format!("{}:{}", host, 0);
|
||||
}
|
||||
match hbb_common::to_socket_addr(&host) {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version_number(v: &str) -> i64 {
|
||||
let mut n = 0;
|
||||
for x in v.split(".") {
|
||||
@@ -429,8 +437,11 @@ pub fn check_software_update() {
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn _check_software_update() -> hbb_common::ResultType<()> {
|
||||
sleep(3.).await;
|
||||
let rendezvous_server = get_rendezvous_server(1_000).await;
|
||||
let mut socket = hbb_common::udp::FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
|
||||
let rendezvous_server = socket_client::get_target_addr(&get_rendezvous_server(1_000).await)?;
|
||||
let mut socket =
|
||||
socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?;
|
||||
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_software_update(SoftwareUpdate {
|
||||
url: crate::VERSION.to_owned(),
|
||||
@@ -450,3 +461,10 @@ async fn _check_software_update() -> hbb_common::ResultType<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_ip(id: &str) -> bool {
|
||||
hbb_common::regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+$")
|
||||
.unwrap()
|
||||
.is_match(id)
|
||||
}
|
||||
|
||||
|
||||
70
src/ipc.rs
70
src/ipc.rs
@@ -13,7 +13,7 @@ use parity_tokio_ipc::{
|
||||
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, net::SocketAddr};
|
||||
use std::collections::HashMap;
|
||||
#[cfg(not(windows))]
|
||||
use std::{fs::File, io::prelude::*};
|
||||
|
||||
@@ -89,6 +89,7 @@ pub enum Data {
|
||||
NatType(Option<i32>),
|
||||
ConfirmedKey(Option<(Vec<u8>, Vec<u8>)>),
|
||||
RawMessage(Vec<u8>),
|
||||
Socks(Option<config::Socks5Server>),
|
||||
FS(FS),
|
||||
Test,
|
||||
}
|
||||
@@ -147,7 +148,7 @@ pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Faild to start ipc{} server at path {}: {}",
|
||||
"Failed to start ipc{} server at path {}: {}",
|
||||
postfix,
|
||||
path,
|
||||
err
|
||||
@@ -192,6 +193,20 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
};
|
||||
allow_err!(stream.send(&Data::ConfirmedKey(out)).await);
|
||||
}
|
||||
Data::Socks(s) => match s {
|
||||
None => {
|
||||
allow_err!(stream.send(&Data::Socks(Config::get_socks())).await);
|
||||
}
|
||||
Some(data) => {
|
||||
if data.proxy.is_empty() {
|
||||
Config::set_socks(None);
|
||||
} else {
|
||||
Config::set_socks(Some(data));
|
||||
}
|
||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||
log::info!("socks updated");
|
||||
}
|
||||
},
|
||||
Data::Config((name, value)) => match value {
|
||||
None => {
|
||||
let value;
|
||||
@@ -202,7 +217,9 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
} else if name == "salt" {
|
||||
value = Some(Config::get_salt());
|
||||
} else if name == "rendezvous_server" {
|
||||
value = Some(Config::get_rendezvous_server().to_string());
|
||||
value = Some(Config::get_rendezvous_server());
|
||||
} else if name == "rendezvous_servers" {
|
||||
value = Some(Config::get_rendezvous_servers().join(","));
|
||||
} else {
|
||||
value = None;
|
||||
}
|
||||
@@ -210,6 +227,7 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
}
|
||||
Some(value) => {
|
||||
if name == "id" {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&value);
|
||||
} else if name == "password" {
|
||||
Config::set_password(&value);
|
||||
@@ -374,7 +392,7 @@ pub fn set_password(v: String) -> ResultType<()> {
|
||||
|
||||
pub fn get_id() -> String {
|
||||
if let Ok(Some(v)) = get_config("id") {
|
||||
// update salt also, so that nexttime reinstallation not causing first-time auto-login failure
|
||||
// update salt also, so that next time reinstallation not causing first-time auto-login failure
|
||||
if let Ok(Some(v2)) = get_config("salt") {
|
||||
Config::set_salt(&v2);
|
||||
}
|
||||
@@ -397,13 +415,12 @@ pub fn get_password() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> SocketAddr {
|
||||
pub async fn get_rendezvous_server(ms_timeout: u64) -> String {
|
||||
if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await {
|
||||
if let Ok(v) = v.parse() {
|
||||
return v;
|
||||
}
|
||||
v
|
||||
} else {
|
||||
Config::get_rendezvous_server()
|
||||
}
|
||||
return Config::get_rendezvous_server();
|
||||
}
|
||||
|
||||
async fn get_options_(ms_timeout: u64) -> ResultType<HashMap<String, String>> {
|
||||
@@ -468,6 +485,41 @@ pub async fn get_nat_type(ms_timeout: u64) -> i32 {
|
||||
.unwrap_or(Config::get_nat_type())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn get_socks_(ms_timeout: u64) -> ResultType<Option<config::Socks5Server>> {
|
||||
let mut c = connect(ms_timeout, "").await?;
|
||||
c.send(&Data::Socks(None)).await?;
|
||||
if let Some(Data::Socks(value)) = c.next_timeout(ms_timeout).await? {
|
||||
Config::set_socks(value.clone());
|
||||
Ok(value)
|
||||
} else {
|
||||
Ok(Config::get_socks())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_socks_async(ms_timeout: u64) -> Option<config::Socks5Server> {
|
||||
get_socks_(ms_timeout).await.unwrap_or(Config::get_socks())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn get_socks() -> Option<config::Socks5Server> {
|
||||
get_socks_async(1_000).await
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> {
|
||||
Config::set_socks(if value.proxy.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.clone())
|
||||
});
|
||||
connect(1_000, "")
|
||||
.await?
|
||||
.send(&Data::Socks(Some(value)))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
static mut SHARED_MEMORY: *mut i64 = std::ptr::null_mut();
|
||||
|
||||
|
||||
37
src/lang.rs
Normal file
37
src/lang.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use hbb_common::{config::Config, log};
|
||||
use std::ops::Deref;
|
||||
|
||||
mod cn;
|
||||
mod en;
|
||||
mod fr;
|
||||
mod it;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn translate(name: String) -> String {
|
||||
let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase();
|
||||
log::debug!("The current locale is {}", locale);
|
||||
translate_locale(name, &locale)
|
||||
}
|
||||
|
||||
pub fn translate_locale(name: String, locale: &str) -> String {
|
||||
let mut lang = Config::get_option("lang");
|
||||
if lang.is_empty() {
|
||||
lang = locale
|
||||
.split("-")
|
||||
.last()
|
||||
.map(|x| x.split("_").last().unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
}
|
||||
let m = match lang.to_lowercase().as_str() {
|
||||
"fr" => fr::T.deref(),
|
||||
"cn" => cn::T.deref(),
|
||||
"it" => it::T.deref(),
|
||||
_ => en::T.deref(),
|
||||
};
|
||||
if let Some(v) = m.get(&name as &str) {
|
||||
v.to_string()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
198
src/lang/cn.rs
Normal file
198
src/lang/cn.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "状态"),
|
||||
("Your Desktop", "你的桌面"),
|
||||
("desk_tip", "你的桌面可以通过下面的 ID和密码访问。"),
|
||||
("Password", "密码"),
|
||||
("Ready", "就绪"),
|
||||
("connecting_status", "正在接入RustDesk网络..."),
|
||||
("Enable Service", "允许服务"),
|
||||
("Start Service", "启动服务"),
|
||||
("Service is not running", "服务没有启动"),
|
||||
("not_ready_status", "未就绪,请检查网络连接"),
|
||||
("Control Remote Desktop", "控制远程桌面"),
|
||||
("Transfer File", "传输文件"),
|
||||
("Connect", "连接"),
|
||||
("Recent Sessions", "最近访问过"),
|
||||
("Address Book", "地址簿"),
|
||||
("Confirmation", "确认"),
|
||||
("TCP Tunneling", "TCP隧道"),
|
||||
("Remove", "删除"),
|
||||
("Refresh random password", "刷新随机密码"),
|
||||
("Set your own password", "设置密码"),
|
||||
("Enable Keyboard/Mouse", "允许控制键盘/鼠标"),
|
||||
("Enable Clipboard", "允许同步剪贴板"),
|
||||
("Enable File Transfer", "允许传输文件"),
|
||||
("Enable TCP Tunneling", "允许建立TCP隧道"),
|
||||
("IP Whitelisting", "IP白名单"),
|
||||
("ID/Relay Server", "ID/中继服务器"),
|
||||
("Stop service", "停止服务"),
|
||||
("Change ID", "改变ID"),
|
||||
("Website", "网站"),
|
||||
("About", "关于"),
|
||||
("Mute", "静音"),
|
||||
("Audio Input", "音频输入"),
|
||||
("ID Server", "ID服务器"),
|
||||
("Relay Server", "中继服务器"),
|
||||
("API Server", "API服务器"),
|
||||
("invalid_http", "必须以http://或者https://开头"),
|
||||
("Invalid IP", "无效IP"),
|
||||
("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"),
|
||||
("Invalid format", "无效格式"),
|
||||
("This function is turned off by the server", "服务器关闭了此功能"),
|
||||
("Not available", "已被占用"),
|
||||
("Too frequent", "修改太频繁,请稍后再试"),
|
||||
("Cancel", "取消"),
|
||||
("Skip", "跳过"),
|
||||
("Close", "关闭"),
|
||||
("Retry", "再试"),
|
||||
("OK", "确认"),
|
||||
("Password Required", "需要密码"),
|
||||
("Please enter your password", "请输入密码"),
|
||||
("Remember password", "记住密码"),
|
||||
("Wrong Password", "密码错误"),
|
||||
("Do you want to enter again?", "还想输入一次吗?"),
|
||||
("Connection Error", "连接错误"),
|
||||
("Error", "错误"),
|
||||
("Reset by the peer", "连接被对方关闭"),
|
||||
("Connecting...", "正在连接..."),
|
||||
("Connection in progress. Please wait.", "连接进行中,请稍等。"),
|
||||
("Please try 1 minute later", "一分钟后再试"),
|
||||
("Login Error", "登录错误"),
|
||||
("Successful", "成功"),
|
||||
("Connected, waiting for image...", "已连接,等待画面传输..."),
|
||||
("Name", "文件名"),
|
||||
("Modified", "修改时间"),
|
||||
("Size", "大小"),
|
||||
("Show Hidden Files", "显示隐藏文件"),
|
||||
("Receive", "接受"),
|
||||
("Send", "发送"),
|
||||
("Remote Computer", "远程电脑"),
|
||||
("Local Computer", "本地电脑"),
|
||||
("Confirm Delete", "确认删除"),
|
||||
("Are you sure you want to delete this file?", "是否删除此文件?"),
|
||||
("Do this for all conflicts", "应用于其它冲突"),
|
||||
("Deleting", "正在删除"),
|
||||
("files", "文件"),
|
||||
("Waiting", "等待..."),
|
||||
("Finished", "完成"),
|
||||
("Custom Image Quality", "设置画面质量"),
|
||||
("Privacy mode", "隐私模式"),
|
||||
("Adjust Window", "调节窗口"),
|
||||
("Original", "原始比例"),
|
||||
("Shrink", "收缩"),
|
||||
("Stretch", "伸展"),
|
||||
("Good image quality", "好画质"),
|
||||
("Balanced", "一般画质"),
|
||||
("Optimize reaction time", "优化反应时间"),
|
||||
("Custom", "自定义画质"),
|
||||
("Show remote cursor", "显示远程光标"),
|
||||
("Disable clipboard", "禁止剪贴板"),
|
||||
("Lock after session end", "断开后锁定远程电脑"),
|
||||
("Insert", "插入"),
|
||||
("Insert Lock", "锁定远程电脑"),
|
||||
("Refresh", "刷新画面"),
|
||||
("ID does not exist", "ID不存在"),
|
||||
("Failed to connect to rendezvous server", "连接注册服务器失败"),
|
||||
("Please try later", "请稍后再试"),
|
||||
("Remote desktop is offline", "远程电脑不在线"),
|
||||
("Key mismatch", "Key不匹配"),
|
||||
("Timeout", "连接超时"),
|
||||
("Failed to connect to relay server", "无法连接到中继服务器"),
|
||||
("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"),
|
||||
("Failed to connect via relay server", "无法通过中继服务器建立连接"),
|
||||
("Failed to make direct connection to remote desktop", "无法建立直接连接"),
|
||||
("Set Password", "设置密码"),
|
||||
("OS Password", "操作系统密码"),
|
||||
("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将RustDesk安装到系统,从而规避上述问题。"),
|
||||
("Click to upgrade", "点击这里升级"),
|
||||
("Configuration Permissions", "配置权限"),
|
||||
("Configure", "配置"),
|
||||
("config_acc", "为了能够远程控制你的桌面, 请给予RustDesk\"辅助功能\" 权限。"),
|
||||
("config_screen", "为了能够远程访问你的桌面, 请给予RustDesk\"屏幕录制\" 权限。"),
|
||||
("Installing ...", "安装 ..."),
|
||||
("Install", "安装"),
|
||||
("Installation", "安装"),
|
||||
("Installation Path", "安装路径"),
|
||||
("Create start menu shortcuts", "创建启动菜单快捷方式"),
|
||||
("Create desktop icon", "创建桌面图标"),
|
||||
("agreement_tip", "开始安装即表示接受许可协议。"),
|
||||
("Accept and Install", "同意并安装"),
|
||||
("End-user license agreement", "用户协议"),
|
||||
("Generating ...", "正在产生 ..."),
|
||||
("Your installation is lower version.", "你安装的版本比当前运行的低。"),
|
||||
("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"),
|
||||
("Listening ...", "正在等待隧道连接 ..."),
|
||||
("Remote Host", "远程主机"),
|
||||
("Remote Port", "远程端口"),
|
||||
("Action", "动作"),
|
||||
("Add", "添加"),
|
||||
("Local Port", "本地端口"),
|
||||
("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"),
|
||||
("Too short, at least 6 characters.", "太短了,至少6个字符"),
|
||||
("The confirmation is not identical.", "两次输入不匹配"),
|
||||
("Permissions", "权限"),
|
||||
("Accept", "接受"),
|
||||
("Dismiss", "拒绝"),
|
||||
("Disconnect", "断开连接"),
|
||||
("Allow using keyboard and mouse", "允许使用键盘鼠标"),
|
||||
("Allow using clipboard", "允许使用剪贴板"),
|
||||
("Allow hearing sound", "允许听到声音"),
|
||||
("Connected", "已经连接"),
|
||||
("Direct and encrypted connection", "加密直连"),
|
||||
("Relayed and encrypted connection", "加密中继连接"),
|
||||
("Direct and unencrypted connection", "非加密直连"),
|
||||
("Relayed and unencrypted connection", "非加密中继连接"),
|
||||
("Enter Remote ID", "输入对方ID"),
|
||||
("Enter your password", "输入密码"),
|
||||
("Logging in...", "正在登录..."),
|
||||
("Enable RDP session sharing", "允许RDP会话共享"),
|
||||
("Auto Login", "自动登录(设置断开后锁定才有效)"),
|
||||
("Enable Direct IP Access", "允许IP直接访问"),
|
||||
("Rename", "改名"),
|
||||
("Space", "空格"),
|
||||
("Create Desktop Shortcut", "创建桌面快捷方式"),
|
||||
("Change Path", "改变路径"),
|
||||
("Create Folder", "创建文件夹"),
|
||||
("Please enter the folder name", "请输入文件夹名称"),
|
||||
("Fix it", "修复"),
|
||||
("Warning", "警告"),
|
||||
("Login screen using Wayland is not supported", "不支持使用 Wayland 登录界面"),
|
||||
("Reboot required", "重启后才能生效"),
|
||||
("Unsupported display server ", "不支持当前显示服务器"),
|
||||
("x11 expected", "请切换到 x11"),
|
||||
("Port", "端口"),
|
||||
("Settings", "设置"),
|
||||
("Username", " 用户名"),
|
||||
("Invalid port", "无效端口"),
|
||||
("Closed manually by the peer", "被对方手动关闭"),
|
||||
("Enable remote configuration modification", "允许远程修改配置"),
|
||||
("Run without install", "无安装运行"),
|
||||
("Always connected via relay", "强制走中继连接"),
|
||||
("Always connect via relay", "强制走中继连接"),
|
||||
("whitelist_tip", "只有白名单里的ip才能访问我"),
|
||||
("Login", "登录"),
|
||||
("Logout", "登出"),
|
||||
("Tags", "标签"),
|
||||
("Search ID", "查找ID"),
|
||||
("Current Wayland display server is not supported", "不支持 Wayland 显示服务器"),
|
||||
("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"),
|
||||
("Add ID", "增加ID"),
|
||||
("Add Tag", "增加标签"),
|
||||
("Unselect all tags", "取消选择所有标签"),
|
||||
("Network error", "网络错误"),
|
||||
("Username missed", "用户名没有填写"),
|
||||
("Password missed", "密码没有填写"),
|
||||
("Wrong credentials", "用户名或者密码错误"),
|
||||
("Edit Tag", "修改标签"),
|
||||
("Unremember Password", "忘掉密码"),
|
||||
("Favorites", "收藏"),
|
||||
("Add to Favorites", "加入到收藏"),
|
||||
("Remove from Favorites", "从收藏中删除"),
|
||||
("Empty", "空空如也"),
|
||||
("Invalid folder name", "无效文件夹名称"),
|
||||
("Socks5 Proxy", "Socks5 代理"),
|
||||
("Hostname", "主机名"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
20
src/lang/en.rs
Normal file
20
src/lang/en.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("desk_tip", "Your desktop can be accessed with this ID and password."),
|
||||
("connecting_status", "Connecting to the RustDesk network..."),
|
||||
("not_ready_status", "Not ready. Please check your connection"),
|
||||
("id_change_tip", "Only a-z, A-Z, 0-9 and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."),
|
||||
("install_tip", "Due to UAC, RustDesk can not work properly as the remote side in some cases. To avoid UAC, please click the button below to install RustDesk to the system."),
|
||||
("config_acc", "In order to control your Desktop remotely, you need to grant RustDesk \"Accessibility\" permissions."),
|
||||
("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."),
|
||||
("agreement_tip", "By starting the installation, you accept the license agreement."),
|
||||
("not_close_tcp_tip", "Don't close this window while you are using the tunnel"),
|
||||
("setup_server_tip", "For faster connection, please set up your own server"),
|
||||
("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"),
|
||||
("whitelist_tip", "Only whitelisted IP can access me"),
|
||||
("whitelist_sep", "Seperated by comma, semicolon, spaces or new line"),
|
||||
("Wrong credentials", "Wrong username or password"),
|
||||
("invalid_http", "must start with http:// or https://"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
192
src/lang/fr.rs
Normal file
192
src/lang/fr.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "Statut"),
|
||||
("Your Desktop", "Votre bureau"),
|
||||
("desk_tip", "Votre bureau est accessible via l'identifiant et le mot de passe ci-dessous."),
|
||||
("Password", "Mot de passe"),
|
||||
("Ready", "Prêt"),
|
||||
("connecting_status", "Connexion au réseau RustDesk..."),
|
||||
("Enable Service", "Autoriser le service"),
|
||||
("Start Service", "Démarrer le service"),
|
||||
("Service is not running", "Le service ne fonctionne pas"),
|
||||
("not_ready_status", "Pas prêt, veuillez vérifier la connexion réseau"),
|
||||
("Control Remote Desktop", "Contrôler le bureau à distance"),
|
||||
("Transfer File", "Transférer le fichier"),
|
||||
("Connect", "Connecter"),
|
||||
("Recent Sessions", "Sessions récentes"),
|
||||
("Address Book", "Carnet d'adresses"),
|
||||
("Confirmation", "Confirmation"),
|
||||
("TCP Tunneling", "Tunneling TCP"),
|
||||
("Remove", "Supprimer"),
|
||||
("Refresh random password", "Actualiser le mot de passe aléatoire"),
|
||||
("Set your own password", "Définir votre propre mot de passe"),
|
||||
("Enable Keyboard/Mouse", "Activer le contrôle clavier/souris"),
|
||||
("Enable Clipboard", "Activer la synchronisation du presse-papiers"),
|
||||
("Enable File Transfer", "Activer le transfert de fichiers"),
|
||||
("Enable TCP Tunneling", "Activer le tunneling TCP"),
|
||||
("IP Whitelisting", "Liste blanche IP"),
|
||||
("ID/Relay Server", "ID/Serveur Relais"),
|
||||
("Stop service", "Arrêter service"),
|
||||
("Change ID", "Changer d'ID"),
|
||||
("Website", "Site Web"),
|
||||
("About", "Sur"),
|
||||
("Mute", "Muet"),
|
||||
("Audio Input", "Entrée audio"),
|
||||
("ID Server", "Serveur ID"),
|
||||
("Relay Server", "Serveur Relais"),
|
||||
("API Server", "Serveur API"),
|
||||
("invalid_http", "Doit commencer par http:// ou https://"),
|
||||
("Invalid IP", "IP invalide"),
|
||||
("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur est comprise entre 6 et 16."),
|
||||
("Invalid format", "Format invalide"),
|
||||
("This function is turned off by the server", "Cette fonction est désactivée par le serveur"),
|
||||
("Not available", "Indisponible"),
|
||||
("Too frequent", "Modifier trop fréquemment, veuillez réessayer plus tard"),
|
||||
("Cancel", "Annuler"),
|
||||
("Skip", "Ignorer"),
|
||||
("Close", "Fermer"),
|
||||
("Retry", "Réessayer"),
|
||||
("OK", "Confirmer"),
|
||||
("Password Required", "Mot de passe requis"),
|
||||
("Please enter your password", "Veuillez saisir votre mot de passe"),
|
||||
("Remember password", "Mémoriser le mot de passe"),
|
||||
("Wrong Password", "Mauvais mot de passe"),
|
||||
("Do you want to enter again?", "Voulez-vous participer à nouveau ?"),
|
||||
("Connection Error", "Erreur de connexion"),
|
||||
("Error", "Erreur"),
|
||||
("Reset by the peer", "La connexion a été fermée par le pair"),
|
||||
("Connecting...", "Connexion..."),
|
||||
("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."),
|
||||
("Please try 1 minute later", "Réessayez dans une minute"),
|
||||
("Login Error", "Erreur de connexion"),
|
||||
("Successful", "Succès"),
|
||||
("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."),
|
||||
("Name", "Nom du fichier"),
|
||||
("Modified", "Modifié"),
|
||||
("Size", "Taille"),
|
||||
("Show Hidden Files", "Afficher les fichiers cachés"),
|
||||
("Receive", "Accepter"),
|
||||
("Send", "Envoyer"),
|
||||
("Remote Computer", "Ordinateur distant"),
|
||||
("Local Computer", "Ordinateur local"),
|
||||
("Confirm Delete", "Confirmer la suppression"),
|
||||
("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier ?"),
|
||||
("Do this for all conflicts", "Appliquer à d'autres conflits"),
|
||||
("Deleting", "Suppression"),
|
||||
("files", "fichier"),
|
||||
("Waiting", "En attente en attente..."),
|
||||
("Finished", "Terminé"),
|
||||
("Custom Image Quality", "Définir la qualité d'image"),
|
||||
("Privacy mode", "Mode privé"),
|
||||
("Adjust Window", "Ajuster la fenêtre"),
|
||||
("Original", "Ratio d'origine"),
|
||||
("Shrink", "Rétréci"),
|
||||
("Stretch", "Étirer"),
|
||||
("Good image quality", "Bonne qualité d'image"),
|
||||
("Balanced", "Qualité d'image normale"),
|
||||
("Optimize reaction time", "Optimiser le temps de réaction"),
|
||||
("Custom", "Qualité d'image personnalisée"),
|
||||
("Show remote cursor", "Afficher le curseur distant"),
|
||||
("Disable clipboard", "Désactiver le presse-papiers"),
|
||||
("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"),
|
||||
("Insert", "Insérer"),
|
||||
("Insert Lock", "Verrouiller l'ordinateur distant"),
|
||||
("Refresh", "Rafraîchir l'écran"),
|
||||
("ID does not exist", "L'ID n'existe pas"),
|
||||
("Failed to connect to rendezvous server", "Échec de la connexion au serveur de rendez-vous"),
|
||||
("Please try later", "Veuillez essayer plus tard"),
|
||||
("Remote desktop is offline", "Le bureau à distance est hors ligne"),
|
||||
("Key mismatch", "Discordance de clé"),
|
||||
("Timeout", "Connexion expirée"),
|
||||
("Failed to connect to relay server", "Échec de la connexion au serveur relais"),
|
||||
("Failed to connect via rendezvous server", "Échec de l'établissement d'une connexion via le serveur de rendez-vous"),
|
||||
("Failed to connect via relay server", "Impossible d'établir une connexion via le serveur relais"),
|
||||
("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"),
|
||||
("Set Password", "Définir le mot de passe"),
|
||||
("OS Password", "Mot de passe du système d'exploitation"),
|
||||
("install_tip", "Vous utilisez une version désinstallée. En raison des restrictions UAC, en tant que terminal contrôlé, dans certains cas, il ne sera pas en mesure de contrôler la souris et le clavier ou d'enregistrer l'écran. Veuillez cliquer sur le bouton ci-dessous pour installer RustDesk au système pour éviter la question ci-dessus."),
|
||||
("Click to upgrade", "Cliquez pour mettre à niveau"),
|
||||
("Configuration Permissions", "Autorisations de configuration"),
|
||||
("Configure", "Configurer"),
|
||||
("config_acc", "Afin de pouvoir contrôler votre bureau à distance, veuillez donner l'autorisation\"accessibilité\" à RustDesk."),
|
||||
("config_screen", "Afin de pouvoir accéder à votre bureau à distance, veuillez donner l'autorisation à RustDesk\"enregistrement d'écran\"."),
|
||||
("Installing ...", "Installation ..."),
|
||||
("Install", "Installer"),
|
||||
("Installation", "Installation"),
|
||||
("Installation Path", "Chemin d'installation"),
|
||||
("Create start menu shortcuts", "Créer des raccourcis dans le menu démarrer"),
|
||||
("Create desktop icon", "Créer une icône sur le bureau"),
|
||||
("agreement_tip", "Démarrer l'installation signifie accepter le contrat de licence."),
|
||||
("Accept and Install", "Accepter et installer"),
|
||||
("End-user license agreement", "Contrat d'utilisateur"),
|
||||
("Generating ...", "Génération ..."),
|
||||
("Your installation is lower version.", "La version que vous avez installée est inférieure à la version en cours d'exécution."),
|
||||
("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l'utilisation du tunnel"),
|
||||
("Listening ...", "En attente de connexion tunnel..."),
|
||||
("Remote Host", "Hôte distant"),
|
||||
("Remote Port", "Port distant"),
|
||||
("Action", "Action"),
|
||||
("Add", "Ajouter"),
|
||||
("Local Port", "Port local"),
|
||||
("setup_server_tip", "Si vous avez besoin d'une vitesse de connexion plus rapide, vous pouvez choisir de créer votre propre serveur"),
|
||||
("Too short, at least 6 characters.", "Trop court, au moins 6 caractères."),
|
||||
("The confirmation is not identical.", "Les deux entrées ne correspondent pas"),
|
||||
("Permissions", "Autorisations"),
|
||||
("Accept", "Accepter"),
|
||||
("Dismiss", "Rejeter"),
|
||||
("Disconnect", "Déconnecter"),
|
||||
("Allow using keyboard and mouse", "Autoriser l'utilisation du clavier et de la souris"),
|
||||
("Allow using clipboard", "Autoriser l'utilisation du presse-papiers"),
|
||||
("Allow hearing sound", "Autoriser l'audition du son"),
|
||||
("Connected", "Connecté"),
|
||||
("Direct and encrypted connection", "Connexion directe cryptée"),
|
||||
("Relayed and encrypted connection", "Connexion relais cryptée"),
|
||||
("Direct and unencrypted connection", "Connexion directe non cryptée"),
|
||||
("Relayed and unencrypted connection", "Connexion relais non cryptée"),
|
||||
("Enter Remote ID", "Entrez l'ID à distance"),
|
||||
("Enter your password", "Entrez votre mot de passe"),
|
||||
("Logging in...", "Se connecter..."),
|
||||
("Enable RDP session sharing", "Activer le partage de session RDP"),
|
||||
("Auto Login", "Connexion automatique (le verrouillage ne sera effectif qu'après la déconnexion du paramètre)"),
|
||||
("Enable Direct IP Access", "Autoriser l'accès direct IP"),
|
||||
("Rename", "Renommer"),
|
||||
("Space", "Espace"),
|
||||
("Create Desktop Shortcut", "Créer un raccourci sur le bureau"),
|
||||
("Change Path", "Changer de chemin"),
|
||||
("Create Folder", "Créer un dossier"),
|
||||
("Please enter the folder name", "Veuillez saisir le nom du dossier"),
|
||||
("Fix it", "Réparez-le"),
|
||||
("Warning", "Avertissement"),
|
||||
("Login screen using Wayland is not supported", "L'écran de connexion utilisant Wayland n'est pas pris en charge"),
|
||||
("Reboot required", "Redémarrage pour prendre effet"),
|
||||
("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"),
|
||||
("x11 expected", "Veuillez passer à x11"),
|
||||
("Port", "Port"),
|
||||
("Settings", "Paramètres"),
|
||||
("Username", " Nom d'utilisateur"),
|
||||
("Invalid port", "Port invalide"),
|
||||
("Closed manually by the peer", "Fermé manuellement par le pair"),
|
||||
("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"),
|
||||
("Run without install", "Exécuter sans installer"),
|
||||
("Always connected via relay", "Forcer la connexion relais"),
|
||||
("Always connect via relay", "Forcer la connexion relais"),
|
||||
("whitelist_tip", "Seul l'ip dans la liste blanche peut m'accéder"),
|
||||
("Login", "Connexion"),
|
||||
("Logout", "Déconnexion"),
|
||||
("Tags", "Étiqueter"),
|
||||
("Search ID", "Identifiant de recherche"),
|
||||
("Current Wayland display server is not supported", "Le serveur d'affichage Wayland n'est pas pris en charge"),
|
||||
("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"),
|
||||
("Add ID", "Ajouter ID"),
|
||||
("Add Tag", "Ajouter une balise"),
|
||||
("Unselect all tags", "Désélectionner toutes les balises"),
|
||||
("Network error", "Erreur réseau"),
|
||||
("Username missed", "Nom d'utilisateur manqué"),
|
||||
("Password missed", "Mot de passe manqué"),
|
||||
("Wrong credentials", "Identifiant ou mot de passe erroné"),
|
||||
("Edit Tag", "Modifier la balise"),
|
||||
("Invalid folder name", "Nom de dossier invalide"),
|
||||
("Hostname", "nom d'hôte"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
193
src/lang/it.rs
Normal file
193
src/lang/it.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "Stato"),
|
||||
("Your Desktop", "Il tuo desktop"),
|
||||
("desk_tip", "Puoi accedere al tuo desktop usando l'ID e la password riportati qui."),
|
||||
("Password", "Password"),
|
||||
("Ready", "Pronto"),
|
||||
("connecting_status", "Connessione alla rete RustDesk in corso..."),
|
||||
("Enable Service", "Abilita servizio"),
|
||||
("Start Service", "Avvia servizio"),
|
||||
("Service is not running", "Il servizio non è in esecuzione"),
|
||||
("not_ready_status", "Non pronto. Verifica la tua connessione"),
|
||||
("Control Remote Desktop", "Controlla una scrivania remota"),
|
||||
("Transfer File", "Trasferisci file"),
|
||||
("Connect", "Connetti"),
|
||||
("Recent Sessions", "Sessioni recenti"),
|
||||
("Address Book", "Rubrica"),
|
||||
("Confirmation", "Conferma"),
|
||||
("TCP Tunneling", "Tunnel TCP"),
|
||||
("Remove", "Rimuovi"),
|
||||
("Refresh random password", "Nuova password casuale"),
|
||||
("Set your own password", "Imposta la tua password"),
|
||||
("Enable Keyboard/Mouse", "Abilita tastiera/mouse"),
|
||||
("Enable Clipboard", "Abilita appunti"),
|
||||
("Enable File Transfer", "Abilita trasferimento file"),
|
||||
("Enable TCP Tunneling", "Abilita tunnel TCP"),
|
||||
("IP Whitelisting", "IP autorizzati"),
|
||||
("ID/Relay Server", "Server ID/Relay"),
|
||||
("Stop service", "Arresta servizio"),
|
||||
("Change ID", "Cambia ID"),
|
||||
("Website", "Sito web"),
|
||||
("About", "Informazioni"),
|
||||
("Mute", "Silenzia"),
|
||||
("Audio Input", "Input audio"),
|
||||
("ID Server", "ID server"),
|
||||
("Relay Server", "Server relay"),
|
||||
("API Server", "Server API"),
|
||||
("invalid_http", "deve iniziare con http:// o https://"),
|
||||
("Invalid IP", "Indirizzo IP non valido"),
|
||||
("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."),
|
||||
("Invalid format", "Formato non valido"),
|
||||
("This function is turned off by the server", "Questa funzione è disabilitata sul server"),
|
||||
("Not available", "Non disponibile"),
|
||||
("Too frequent", "Troppo frequente"),
|
||||
("Cancel", "Annulla"),
|
||||
("Skip", "Ignora"),
|
||||
("Close", "Chiudi"),
|
||||
("Retry", "Riprova"),
|
||||
("OK", "OK"),
|
||||
("Password Required", "Password richiesta"),
|
||||
("Please enter your password", "Inserisci la tua password"),
|
||||
("Remember password", "Ricorda password"),
|
||||
("Wrong Password", "Password errata"),
|
||||
("Do you want to enter again?", "Vuoi riprovare?"),
|
||||
("Connection Error", "Errore di connessione"),
|
||||
("Error", "Errore"),
|
||||
("Reset by the peer", "Reimpostata dal peer"),
|
||||
("Connecting...", "Connessione..."),
|
||||
("Connection in progress. Please wait.", "Connessione in corso. Attendi."),
|
||||
("Please try 1 minute later", "Per favore riprova fra 1 minuto"),
|
||||
("Login Error", "Errore di login"),
|
||||
("Successful", "Successo"),
|
||||
("Connected, waiting for image...", "Connesso, in attesa dell'immagine..."),
|
||||
("Name", "Nome"),
|
||||
("Modified", "Modificato"),
|
||||
("Size", "Dimensione"),
|
||||
("Show Hidden Files", "Mostra file nascosti"),
|
||||
("Receive", "Ricevi"),
|
||||
("Send", "Invia"),
|
||||
("Remote Computer", "Computer remoto"),
|
||||
("Local Computer", "Computer locale"),
|
||||
("Confirm Delete", "Conferma cancellazione"),
|
||||
("Are you sure you want to delete this file?", "Vuoi davvero eliminare questo file?"),
|
||||
("Do this for all conflicts", "Ricorca questa scelta per tutti i conflitti"),
|
||||
("Deleting", "Cancellazione di"),
|
||||
("files", "file"),
|
||||
("Waiting", "In attesa"),
|
||||
("Finished", "Terminato"),
|
||||
("Custom Image Quality", "Qualità immagine personalizzata"),
|
||||
("Privacy mode", "Modalità privacy"),
|
||||
("Adjust Window", "Adatta la finestra"),
|
||||
("Original", "Originale"),
|
||||
("Shrink", "Restringi"),
|
||||
("Stretch", "Allarga"),
|
||||
("Good image quality", "Buona qualità immagine"),
|
||||
("Balanced", "Bilanciato"),
|
||||
("Optimize reaction time", "Ottimizza il tempo di reazione"),
|
||||
("Custom", "Personalizzato"),
|
||||
("Show remote cursor", "Mostra il cursore remoto"),
|
||||
("Disable clipboard", "Disabilita appunti"),
|
||||
("Lock after session end", "Blocca al termine della sessione"),
|
||||
("Insert", "Inserisci"),
|
||||
("Insert Lock", "Blocco inserimento"),
|
||||
("Refresh", "Aggiorna"),
|
||||
("ID does not exist", "L'ID non esiste"),
|
||||
("Failed to connect to rendezvous server", "Errore di connessione al server rendezvous"),
|
||||
("Please try later", "Riprova più tardi"),
|
||||
("Remote desktop is offline", "Il desktop remoto è offline"),
|
||||
("Key mismatch", "La chiave non corrisponde"),
|
||||
("Timeout", "Timeout"),
|
||||
("Failed to connect to relay server", "Errore di connessione al server relay"),
|
||||
("Failed to connect via rendezvous server", "Errore di connessione tramite il server rendezvous"),
|
||||
("Failed to connect via relay server", "Errore di connessione tramite il server relay"),
|
||||
("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"),
|
||||
("Set Password", "Imposta password"),
|
||||
("OS Password", "Password del sistema operativo"),
|
||||
("install_tip", "A causa del Controllo Account Utente, RustDesk potrebbe non funzionare correttamente come desktop remoto. Per evitare questo problema, fai click sul tasto qui sotto per installare RustDesk a livello di sistema."),
|
||||
("Click to upgrade", "Fai click per aggiornare"),
|
||||
("Configuration Permissions", "Permessi di configurazione"),
|
||||
("Configure", "Configura"),
|
||||
("config_acc", "Per controllare il tuo desktop dall'esterno, devi fornire a RustDesk il permesso \"Accessibilità\"."),
|
||||
("config_screen", "Per controllare il tuo desktop dall'esterno, devi fornire a RustDesk il permesso \"Registrazione schermo\"."),
|
||||
("Installing ...", "Installazione ..."),
|
||||
("Install", "Installa"),
|
||||
("Installation", "Installazione"),
|
||||
("Installation Path", "Percorso di installazione"),
|
||||
("Create start menu shortcuts", "Crea i collegamenti nel menu di avvio"),
|
||||
("Create desktop icon", "Crea un'icona sul desktop"),
|
||||
("agreement_tip", "Avviando l'installazione, accetti i termini del contratto di licenza."),
|
||||
("Accept and Install", "Accetta e installa"),
|
||||
("End-user license agreement", "Contratto di licenza con l'utente finale"),
|
||||
("Generating ...", "Generazione ..."),
|
||||
("Your installation is lower version.", "La tua installazione non è aggiornata."),
|
||||
("not_close_tcp_tip", "Non chiudere questa finestra mentre stai usando il tunnel"),
|
||||
("Listening ...", "In ascolto ..."),
|
||||
("Remote Host", "Host remoto"),
|
||||
("Remote Port", "Porta remota"),
|
||||
("Action", "Azione"),
|
||||
("Add", "Aggiungi"),
|
||||
("Local Port", "Porta locale"),
|
||||
("setup_server_tip", "Per una connessione più veloce, configura un tuo server"),
|
||||
("Too short, at least 6 characters.", "Troppo breve, almeno 6 caratteri"),
|
||||
("The confirmation is not identical.", "La conferma non corrisponde"),
|
||||
("Permissions", "Permessi"),
|
||||
("Accept", "Accetta"),
|
||||
("Dismiss", "Rifiuta"),
|
||||
("Disconnect", "Disconnetti"),
|
||||
("Allow using keyboard and mouse", "Consenti l'uso di tastiera e mouse"),
|
||||
("Allow using clipboard", "Consenti l'uso degli appunti"),
|
||||
("Allow hearing sound", "Consenti la riproduzione dell'audio"),
|
||||
("Connected", "Connesso"),
|
||||
("Direct and encrypted connection", "Connessione diretta e cifrata"),
|
||||
("Relayed and encrypted connection", "Connessione tramite relay e cifrata"),
|
||||
("Direct and unencrypted connection", "Connessione diretta e non cifrata"),
|
||||
("Relayed and unencrypted connection", "Connessione tramite relay e non cifrata"),
|
||||
("Enter Remote ID", "Inserisci l'ID remoto"),
|
||||
("Enter your password", "Inserisci la tua password"),
|
||||
("Logging in...", "Autenticazione..."),
|
||||
("Enable RDP session sharing", "Abilita la condivisione della sessione RDP"),
|
||||
("Auto Login", "Login automatico"),
|
||||
("Enable Direct IP Access", "Abilita l'accesso diretto tramite IP"),
|
||||
("Rename", "Rinomina"),
|
||||
("Space", "Spazio"),
|
||||
("Create Desktop Shortcut", "Crea collegamento sul desktop"),
|
||||
("Change Path", "Cambia percorso"),
|
||||
("Create Folder", "Crea cartella"),
|
||||
("Please enter the folder name", "Inserisci il nome della cartella"),
|
||||
("Fix it", "Risolvi"),
|
||||
("Warning", "Avviso"),
|
||||
("Login screen using Wayland is not supported", "La schermata di login non è supportata utilizzando Wayland"),
|
||||
("Reboot required", "Riavvio necessario"),
|
||||
("Unsupported display server ", "Display server non supportato"),
|
||||
("x11 expected", "x11 necessario"),
|
||||
("Port", "Porta"),
|
||||
("Settings", "Impostazioni"),
|
||||
("Username", " Nome utente"),
|
||||
("Invalid port", "Porta non valida"),
|
||||
("Closed manually by the peer", "Chiuso manualmente dal peer"),
|
||||
("Enable remote configuration modification", "Abilita la modifica remota della configurazione"),
|
||||
("Run without install", "Avvia senza installare"),
|
||||
("Always connected via relay", "Connesso sempre tramite relay"),
|
||||
("Always connect via relay", "Connetti sempre tramite relay"),
|
||||
("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"),
|
||||
("Login", "Accedi"),
|
||||
("Logout", "Esci"),
|
||||
("Tags", "Tag"),
|
||||
("Search ID", "Cerca ID"),
|
||||
("Current Wayland display server is not supported", "Questo display server Wayland non è supportato"),
|
||||
("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"),
|
||||
("Add ID", "Aggiungi ID"),
|
||||
("Add Tag", "Aggiungi tag"),
|
||||
("Unselect all tags", "Deseleziona tutti i tag"),
|
||||
("Network error", "Errore di rete"),
|
||||
("Username missed", "Nome utente dimenticato"),
|
||||
("Password missed", "Password dimenticata"),
|
||||
("Wrong credentials", "Credenziali errate"),
|
||||
("Edit Tag", "Modifica tag"),
|
||||
("Invalid folder name", "Nome della cartella non valido"),
|
||||
("Hostname", "Nome host"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -27,3 +27,4 @@ use common::*;
|
||||
pub mod cli;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod port_forward;
|
||||
mod lang;
|
||||
|
||||
45
src/main.rs
45
src/main.rs
@@ -43,45 +43,40 @@ fn main() {
|
||||
}
|
||||
}
|
||||
use flexi_logger::*;
|
||||
Logger::with_env_or_str("debug")
|
||||
.log_to_file()
|
||||
.format(opt_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(6),
|
||||
)
|
||||
.directory(path)
|
||||
.start()
|
||||
Logger::try_with_env_or_str("debug")
|
||||
.map(|x| {
|
||||
x.log_to_file(FileSpec::default().directory(path))
|
||||
.format(opt_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(6),
|
||||
)
|
||||
.start()
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
if args.is_empty() {
|
||||
std::thread::spawn(move || start_server(false, false));
|
||||
} else {
|
||||
if args[0] == "--uninstall" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if args[0] == "--uninstall" {
|
||||
if let Err(err) = platform::uninstall_me() {
|
||||
log::error!("Failed to uninstall: {}", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--update" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
} else if args[0] == "--update" {
|
||||
hbb_common::allow_err!(platform::update_me());
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--reinstall" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
} else if args[0] == "--reinstall" {
|
||||
hbb_common::allow_err!(platform::uninstall_me());
|
||||
hbb_common::allow_err!(platform::install_me(
|
||||
"desktopicon startmenu",
|
||||
));
|
||||
hbb_common::allow_err!(platform::install_me("desktopicon startmenu",));
|
||||
return;
|
||||
}
|
||||
} else if args[0] == "--remove" {
|
||||
}
|
||||
if args[0] == "--remove" {
|
||||
if args.len() == 2 {
|
||||
// sleep a while so that process of removed exe exit
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
|
||||
@@ -11,7 +11,8 @@ use std::{
|
||||
};
|
||||
type Xdo = *const c_void;
|
||||
|
||||
pub const PA_SAMPLE_RATE: u32 = 24000;
|
||||
pub const PA_SAMPLE_RATE: u32 = 48000;
|
||||
static mut UNMODIFIED: bool = true;
|
||||
|
||||
thread_local! {
|
||||
static XDO: RefCell<Xdo> = RefCell::new(unsafe { xdo_new(std::ptr::null()) });
|
||||
@@ -416,7 +417,7 @@ pub fn fix_login_wayland() {
|
||||
"sed",
|
||||
"-i",
|
||||
"s/#WaylandEnable=false/WaylandEnable=false/g",
|
||||
&file
|
||||
&file,
|
||||
])
|
||||
.output()
|
||||
{
|
||||
@@ -432,6 +433,80 @@ pub fn fix_login_wayland() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_is_wayland() -> bool {
|
||||
let dtype = get_display_server();
|
||||
return "wayland" == dtype && unsafe { UNMODIFIED };
|
||||
}
|
||||
|
||||
pub fn modify_default_login() -> String {
|
||||
let dsession = std::env::var("DESKTOP_SESSION").unwrap();
|
||||
let user_name = std::env::var("USERNAME").unwrap();
|
||||
if let Ok(Some(x)) =
|
||||
run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION}-xorg.desktop".to_owned())
|
||||
{
|
||||
if x.trim_end().to_string() != "" {
|
||||
match std::process::Command::new("pkexec")
|
||||
.args(vec![
|
||||
"sed",
|
||||
"-i",
|
||||
&format!("s/={0}$/={0}-xorg/g", &dsession),
|
||||
&format!("/var/lib/AccountsService/users/{}", &user_name),
|
||||
])
|
||||
.output()
|
||||
{
|
||||
Ok(x) => {
|
||||
let x = String::from_utf8_lossy(&x.stderr);
|
||||
if !x.is_empty() {
|
||||
log::error!("modify_default_login failed: {}", x);
|
||||
return "Fix failed! Please re-login with X server manually".to_owned();
|
||||
} else {
|
||||
unsafe {
|
||||
UNMODIFIED = false;
|
||||
}
|
||||
return "".to_owned();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("modify_default_login failed: {}", err);
|
||||
return "Fix failed! Please re-login with X server manually".to_owned();
|
||||
}
|
||||
}
|
||||
} else if let Ok(Some(z)) =
|
||||
run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION:0:-8}.desktop".to_owned())
|
||||
{
|
||||
if z.trim_end().to_string() != "" {
|
||||
match std::process::Command::new("pkexec")
|
||||
.args(vec![
|
||||
"sed",
|
||||
"-i",
|
||||
&format!("s/={}$/={}/g", &dsession, &dsession[..dsession.len() - 8]),
|
||||
&format!("/var/lib/AccountsService/users/{}", &user_name),
|
||||
])
|
||||
.output()
|
||||
{
|
||||
Ok(x) => {
|
||||
let x = String::from_utf8_lossy(&x.stderr);
|
||||
if !x.is_empty() {
|
||||
log::error!("modify_default_login failed: {}", x);
|
||||
return "Fix failed! Please re-login with X server manually".to_owned();
|
||||
} else {
|
||||
unsafe {
|
||||
UNMODIFIED = false;
|
||||
}
|
||||
return "".to_owned();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("modify_default_login failed: {}", err);
|
||||
return "Fix failed! Please re-login with X server manually".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Fix failed! Please re-login with X server manually".to_owned();
|
||||
}
|
||||
|
||||
// to-do: test the other display manager
|
||||
fn _get_display_manager() -> String {
|
||||
if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") {
|
||||
@@ -569,7 +644,7 @@ fn get_env_tries(name: &str, uid: &str, n: usize) -> String {
|
||||
}
|
||||
|
||||
fn get_env(name: &str, uid: &str) -> String {
|
||||
let cmd = format!("ps -u {} -o pid= | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep -m1 '^{}=' | sed 's/{}=//g'", uid, name, name);
|
||||
let cmd = format!("ps -u {} -o pid= | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, name, name);
|
||||
log::debug!("Run: {}", &cmd);
|
||||
if let Ok(Some(x)) = run_cmds(cmd) {
|
||||
x.trim_end().to_string()
|
||||
|
||||
@@ -145,7 +145,13 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
if do_outline {
|
||||
let mut outline = Vec::new();
|
||||
outline.resize(((width + 2) * (height + 2) * 4) as _, 0);
|
||||
drawOutline(outline.as_mut_ptr(), cbits.as_ptr(), width, height, outline.len() as _);
|
||||
drawOutline(
|
||||
outline.as_mut_ptr(),
|
||||
cbits.as_ptr(),
|
||||
width,
|
||||
height,
|
||||
outline.len() as _,
|
||||
);
|
||||
cbits = outline;
|
||||
width += 2;
|
||||
height += 2;
|
||||
@@ -357,7 +363,7 @@ fn fix_cursor_mask(
|
||||
let mut alpha = 255;
|
||||
if mask_idx < bm_size {
|
||||
if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 {
|
||||
alpha = 0;
|
||||
alpha = 0;
|
||||
}
|
||||
}
|
||||
let a = cbits[pix_idx + 2];
|
||||
@@ -393,8 +399,14 @@ extern "C" {
|
||||
fn LaunchProcessWin(cmd: *const u16, session_id: DWORD, as_user: BOOL) -> HANDLE;
|
||||
fn selectInputDesktop() -> BOOL;
|
||||
fn inputDesktopSelected() -> BOOL;
|
||||
fn handleMask(out: *mut u8, mask: *const u8, width: i32, height: i32, bmWidthBytes: i32, bmHeight: i32)
|
||||
-> i32;
|
||||
fn handleMask(
|
||||
out: *mut u8,
|
||||
mask: *const u8,
|
||||
width: i32,
|
||||
height: i32,
|
||||
bmWidthBytes: i32,
|
||||
bmHeight: i32,
|
||||
) -> i32;
|
||||
fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32, out_size: i32);
|
||||
fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32;
|
||||
fn blank_screen(v: BOOL);
|
||||
@@ -534,7 +546,7 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
|
||||
let wstr = wstr.as_ptr();
|
||||
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE) };
|
||||
if h.is_null() {
|
||||
log::error!("Failed to luanch server: {}", get_error());
|
||||
log::error!("Failed to launch server: {}", get_error());
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
@@ -593,7 +605,7 @@ fn send_sas() {
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref SUPRESS: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
static ref SUPPRESS: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
}
|
||||
|
||||
pub fn desktop_changed() -> bool {
|
||||
@@ -605,7 +617,7 @@ pub fn try_change_desktop() -> bool {
|
||||
if inputDesktopSelected() == FALSE {
|
||||
let res = selectInputDesktop() == TRUE;
|
||||
if !res {
|
||||
let mut s = SUPRESS.lock().unwrap();
|
||||
let mut s = SUPPRESS.lock().unwrap();
|
||||
if s.elapsed() > std::time::Duration::from_secs(3) {
|
||||
log::error!("Failed to switch desktop: {}", get_error());
|
||||
*s = Instant::now();
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
use crate::server::{check_zombie, new as new_server, ServerPtr};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
anyhow::bail,
|
||||
config::{Config, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT},
|
||||
futures::future::join_all,
|
||||
log,
|
||||
protobuf::Message as _,
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
tcp::FramedStream,
|
||||
sleep, socket_client,
|
||||
tokio::{
|
||||
self, select,
|
||||
time::{interval, Duration},
|
||||
},
|
||||
udp::FramedSocket,
|
||||
AddrMangle, ResultType,
|
||||
AddrMangle, IntoTargetAddr, ResultType, TargetAddr,
|
||||
};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::SystemTime,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
@@ -25,19 +28,25 @@ use uuid::Uuid;
|
||||
type Message = RendezvousMessage;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SOLVING_PK_MISMATCH: Arc<Mutex<String>> = Default::default();
|
||||
static ref SOLVING_PK_MISMATCH: Arc<Mutex<String>> = Default::default();
|
||||
}
|
||||
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
|
||||
const REG_INTERVAL: i64 = 12_000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RendezvousMediator {
|
||||
addr: SocketAddr,
|
||||
addr: TargetAddr<'static>,
|
||||
host: String,
|
||||
host_prefix: String,
|
||||
rendezvous_servers: Vec<String>,
|
||||
last_id_pk_registery: String,
|
||||
last_id_pk_registry: String,
|
||||
}
|
||||
|
||||
impl RendezvousMediator {
|
||||
pub fn restart() {
|
||||
SHOULD_EXIT.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub async fn start_all() {
|
||||
let mut nat_tested = false;
|
||||
check_zombie();
|
||||
@@ -46,6 +55,10 @@ impl RendezvousMediator {
|
||||
crate::common::test_nat_type();
|
||||
nat_tested = true;
|
||||
}
|
||||
let server_cloned = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(direct_server(server_cloned).await);
|
||||
});
|
||||
loop {
|
||||
Config::reset_online();
|
||||
if Config::get_option("stop-service").is_empty() {
|
||||
@@ -55,11 +68,14 @@ impl RendezvousMediator {
|
||||
}
|
||||
let mut futs = Vec::new();
|
||||
let servers = Config::get_rendezvous_servers();
|
||||
SHOULD_EXIT.store(false, Ordering::SeqCst);
|
||||
for host in servers.clone() {
|
||||
let server = server.clone();
|
||||
let servers = servers.clone();
|
||||
futs.push(tokio::spawn(async move {
|
||||
allow_err!(Self::start(server, host, servers).await);
|
||||
// SHOULD_EXIT here is to ensure once one exits, the others also exit.
|
||||
SHOULD_EXIT.store(true, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
join_all(futs).await;
|
||||
@@ -86,18 +102,20 @@ impl RendezvousMediator {
|
||||
})
|
||||
.unwrap_or(host.to_owned());
|
||||
let mut rz = Self {
|
||||
addr: Config::get_any_listen_addr(),
|
||||
addr: Config::get_any_listen_addr().into_target_addr()?,
|
||||
host: host.clone(),
|
||||
host_prefix,
|
||||
rendezvous_servers,
|
||||
last_id_pk_registery: "".to_owned(),
|
||||
last_id_pk_registry: "".to_owned(),
|
||||
};
|
||||
allow_err!(rz.dns_check());
|
||||
let mut socket = FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
|
||||
rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?;
|
||||
let any_addr = Config::get_any_listen_addr();
|
||||
let mut socket = socket_client::new_udp(any_addr, RENDEZVOUS_TIMEOUT).await?;
|
||||
|
||||
const TIMER_OUT: Duration = Duration::from_secs(1);
|
||||
let mut timer = interval(TIMER_OUT);
|
||||
let mut last_timer = SystemTime::UNIX_EPOCH;
|
||||
const REG_INTERVAL: i64 = 12_000;
|
||||
const REG_TIMEOUT: i64 = 3_000;
|
||||
const MAX_FAILS1: i64 = 3;
|
||||
const MAX_FAILS2: i64 = 6;
|
||||
@@ -136,60 +154,68 @@ impl RendezvousMediator {
|
||||
}
|
||||
};
|
||||
select! {
|
||||
Some(Ok((bytes, _))) = socket.next() => {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::register_peer_response(rpr)) => {
|
||||
update_latency();
|
||||
if rpr.request_pk {
|
||||
log::info!("request_pk received from {}", host);
|
||||
allow_err!(rz.register_pk(&mut socket).await);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Some(rendezvous_message::Union::register_pk_response(rpr)) => {
|
||||
update_latency();
|
||||
match rpr.result.enum_value_or_default() {
|
||||
register_pk_response::Result::OK => {
|
||||
Config::set_key_confirmed(true);
|
||||
Config::set_host_key_confirmed(&rz.host_prefix, true);
|
||||
*SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned();
|
||||
n = socket.next() => {
|
||||
match n {
|
||||
Some(Ok((bytes, _))) => {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::register_peer_response(rpr)) => {
|
||||
update_latency();
|
||||
if rpr.request_pk {
|
||||
log::info!("request_pk received from {}", host);
|
||||
allow_err!(rz.register_pk(&mut socket).await);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
register_pk_response::Result::UUID_MISMATCH => {
|
||||
allow_err!(rz.handle_uuid_mismatch(&mut socket).await);
|
||||
Some(rendezvous_message::Union::register_pk_response(rpr)) => {
|
||||
update_latency();
|
||||
match rpr.result.enum_value_or_default() {
|
||||
register_pk_response::Result::OK => {
|
||||
Config::set_key_confirmed(true);
|
||||
Config::set_host_key_confirmed(&rz.host_prefix, true);
|
||||
*SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned();
|
||||
}
|
||||
register_pk_response::Result::UUID_MISMATCH => {
|
||||
allow_err!(rz.handle_uuid_mismatch(&mut socket).await);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(rendezvous_message::Union::punch_hole(ph)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_punch_hole(ph, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::request_relay(rr)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_request_relay(rr, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::fetch_local_addr(fla)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_intranet(fla, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::configure_update(cu)) => {
|
||||
Config::set_option("rendezvous-servers".to_owned(), cu.rendezvous_servers.join(","));
|
||||
Config::set_serial(cu.serial);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
log::debug!("Non-protobuf message bytes received: {:?}", bytes);
|
||||
}
|
||||
Some(rendezvous_message::Union::punch_hole(ph)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_punch_hole(ph, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::request_relay(rr)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_request_relay(rr, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::fetch_local_addr(fla)) => {
|
||||
let rz = rz.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(rz.handle_intranet(fla, server).await);
|
||||
});
|
||||
}
|
||||
Some(rendezvous_message::Union::configure_update(cu)) => {
|
||||
Config::set_option("rendezvous-servers".to_owned(), cu.rendezvous_servers.join(","));
|
||||
Config::set_serial(cu.serial);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
log::debug!("Non-protobuf message bytes received: {:?}", bytes);
|
||||
},
|
||||
Some(Err(e)) => bail!("Failed to receive next {}", e), // maybe socks5 tcp disconnected
|
||||
None => {
|
||||
// unreachable!()
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = timer.tick() => {
|
||||
@@ -199,15 +225,8 @@ impl RendezvousMediator {
|
||||
if !Config::get_option("stop-service").is_empty() {
|
||||
break;
|
||||
}
|
||||
if rz.addr.port() == 0 {
|
||||
allow_err!(rz.dns_check());
|
||||
if rz.addr.port() == 0 {
|
||||
continue;
|
||||
} else {
|
||||
// have to do this for osx, to avoid "Can't assign requested address"
|
||||
// when socket created before OS network ready
|
||||
socket = FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
}
|
||||
if SHOULD_EXIT.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let now = SystemTime::now();
|
||||
if now.duration_since(last_timer).map(|d| d < TIMER_OUT).unwrap_or(false) {
|
||||
@@ -226,10 +245,11 @@ impl RendezvousMediator {
|
||||
Config::update_latency(&host, -1);
|
||||
old_latency = 0;
|
||||
if now.duration_since(last_dns_check).map(|d| d.as_millis() as i64).unwrap_or(0) > DNS_INTERVAL {
|
||||
if let Ok(_) = rz.dns_check() {
|
||||
// in some case of network reconnect (dial IP network),
|
||||
// old UDP socket not work any more after network recover
|
||||
socket = FramedSocket::new(Config::get_any_listen_addr()).await?;
|
||||
rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?;
|
||||
// in some case of network reconnect (dial IP network),
|
||||
// old UDP socket not work any more after network recover
|
||||
if let Some(s) = socket_client::rebind_udp(any_addr).await? {
|
||||
socket = s;
|
||||
}
|
||||
last_dns_check = now;
|
||||
}
|
||||
@@ -245,12 +265,6 @@ impl RendezvousMediator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dns_check(&mut self) -> ResultType<()> {
|
||||
self.addr = hbb_common::to_socket_addr(&crate::check_port(&self.host, RENDEZVOUS_PORT))?;
|
||||
log::debug!("Lookup dns of {}", self.host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> {
|
||||
self.create_relay(
|
||||
rr.socket_addr,
|
||||
@@ -280,11 +294,18 @@ impl RendezvousMediator {
|
||||
uuid,
|
||||
secure,
|
||||
);
|
||||
let mut socket =
|
||||
FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?;
|
||||
|
||||
let mut socket = socket_client::connect_tcp(
|
||||
self.addr.to_owned(),
|
||||
Config::get_any_listen_addr(),
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut msg_out = Message::new();
|
||||
let mut rr = RelayResponse {
|
||||
socket_addr,
|
||||
version: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
if initiate {
|
||||
@@ -302,15 +323,15 @@ impl RendezvousMediator {
|
||||
async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
log::debug!("Handle intranet from {:?}", peer_addr);
|
||||
let (mut socket, port) = {
|
||||
let socket =
|
||||
FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT)
|
||||
.await?;
|
||||
let port = socket.get_ref().local_addr()?.port();
|
||||
(socket, port)
|
||||
};
|
||||
let local_addr = socket.get_ref().local_addr()?;
|
||||
let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), port).parse()?;
|
||||
let mut socket = socket_client::connect_tcp(
|
||||
self.addr.to_owned(),
|
||||
Config::get_any_listen_addr(),
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
let local_addr = socket.local_addr();
|
||||
let local_addr: SocketAddr =
|
||||
format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?;
|
||||
let mut msg_out = Message::new();
|
||||
let mut relay_server = Config::get_option("relay-server");
|
||||
if relay_server.is_empty() {
|
||||
@@ -321,6 +342,7 @@ impl RendezvousMediator {
|
||||
socket_addr: AddrMangle::encode(peer_addr),
|
||||
local_addr: AddrMangle::encode(local_addr),
|
||||
relay_server,
|
||||
version: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = msg_out.write_to_bytes()?;
|
||||
@@ -345,10 +367,14 @@ impl RendezvousMediator {
|
||||
let peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||
log::debug!("Punch hole to {:?}", peer_addr);
|
||||
let mut socket = {
|
||||
let socket =
|
||||
FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT)
|
||||
.await?;
|
||||
allow_err!(FramedStream::new(peer_addr, socket.get_ref().local_addr()?, 300).await);
|
||||
let socket = socket_client::connect_tcp(
|
||||
self.addr.to_owned(),
|
||||
Config::get_any_listen_addr(),
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
let local_addr = socket.local_addr();
|
||||
allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 300).await);
|
||||
socket
|
||||
};
|
||||
let mut msg_out = Message::new();
|
||||
@@ -359,6 +385,7 @@ impl RendezvousMediator {
|
||||
id: Config::get_id(),
|
||||
relay_server,
|
||||
nat_type: nat_type.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = msg_out.write_to_bytes()?;
|
||||
@@ -377,19 +404,19 @@ impl RendezvousMediator {
|
||||
pk.clone()
|
||||
};
|
||||
let id = Config::get_id();
|
||||
self.last_id_pk_registery = id.clone();
|
||||
self.last_id_pk_registry = id.clone();
|
||||
msg_out.set_register_pk(RegisterPk {
|
||||
id,
|
||||
uuid,
|
||||
pk,
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, self.addr).await?;
|
||||
socket.send(&msg_out, self.addr.to_owned()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_uuid_mismatch(&mut self, socket: &mut FramedSocket) -> ResultType<()> {
|
||||
if self.last_id_pk_registery != Config::get_id() {
|
||||
if self.last_id_pk_registry != Config::get_id() {
|
||||
return Ok(());
|
||||
}
|
||||
{
|
||||
@@ -430,7 +457,47 @@ impl RendezvousMediator {
|
||||
serial,
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, self.addr).await?;
|
||||
socket.send(&msg_out, self.addr.to_owned()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn direct_server(server: ServerPtr) -> ResultType<()> {
|
||||
let port = RENDEZVOUS_PORT + 2;
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
let mut listener = None;
|
||||
loop {
|
||||
if !Config::get_option("direct-server").is_empty() && listener.is_none() {
|
||||
listener = Some(hbb_common::tcp::new_listener(&addr, false).await?);
|
||||
log::info!(
|
||||
"Direct server listening on: {}",
|
||||
&listener.as_ref().unwrap().local_addr()?
|
||||
);
|
||||
}
|
||||
if let Some(l) = listener.as_mut() {
|
||||
if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await {
|
||||
if Config::get_option("direct-server").is_empty() {
|
||||
continue;
|
||||
}
|
||||
log::info!("direct access from {}", addr);
|
||||
let local_addr = stream.local_addr()?;
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(
|
||||
crate::server::create_tcp_connection(
|
||||
server,
|
||||
hbb_common::Stream::from(stream, local_addr),
|
||||
addr,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
);
|
||||
});
|
||||
} else {
|
||||
sleep(0.1).await;
|
||||
}
|
||||
} else {
|
||||
sleep(1.).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use hbb_common::{
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
sodiumoxide::crypto::{box_, secretbox, sign},
|
||||
tcp::FramedStream,
|
||||
timeout, tokio, ResultType, Stream,
|
||||
socket_client,
|
||||
};
|
||||
use service::{GenericService, Service, ServiceTmpl, Subscriber};
|
||||
use std::{
|
||||
@@ -61,7 +61,7 @@ pub fn new() -> ServerPtr {
|
||||
}
|
||||
|
||||
async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> {
|
||||
let local_addr = socket.get_ref().local_addr()?;
|
||||
let local_addr = socket.local_addr();
|
||||
drop(socket);
|
||||
// even we drop socket, below still may fail if not use reuse_addr,
|
||||
// there is TIME_WAIT before socket really released, so sometimes we
|
||||
@@ -69,12 +69,13 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) ->
|
||||
let listener = new_listener(local_addr, true).await?;
|
||||
log::info!("Server listening on: {}", &listener.local_addr()?);
|
||||
if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? {
|
||||
create_tcp_connection_(server, Stream::from(stream), addr, secure).await?;
|
||||
let stream_addr = stream.local_addr()?;
|
||||
create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_tcp_connection_(
|
||||
pub async fn create_tcp_connection(
|
||||
server: ServerPtr,
|
||||
stream: Stream,
|
||||
addr: SocketAddr,
|
||||
@@ -92,11 +93,13 @@ async fn create_tcp_connection_(
|
||||
sk_[..].copy_from_slice(&sk);
|
||||
let sk = sign::SecretKey(sk_);
|
||||
let mut msg_out = Message::new();
|
||||
let signed_id = sign::sign(Config::get_id().as_bytes(), &sk);
|
||||
let (our_pk_b, our_sk_b) = box_::gen_keypair();
|
||||
let signed_id = sign::sign(
|
||||
format!("{}\0{}", Config::get_id(), base64::encode(our_pk_b.0)).as_bytes(),
|
||||
&sk,
|
||||
);
|
||||
msg_out.set_signed_id(SignedId {
|
||||
id: signed_id,
|
||||
pk: our_pk_b.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??;
|
||||
@@ -122,8 +125,8 @@ async fn create_tcp_connection_(
|
||||
key[..].copy_from_slice(&symmetric_key);
|
||||
stream.set_key(secretbox::Key(key));
|
||||
} else if pk.asymmetric_value.is_empty() {
|
||||
// force a trial to update_pk to rendezvous server
|
||||
Config::set_key_confirmed(false);
|
||||
log::info!("Force to update pk");
|
||||
} else {
|
||||
bail!("Handshake failed: invalid public sign key length from peer");
|
||||
}
|
||||
@@ -181,8 +184,8 @@ async fn create_relay_connection_(
|
||||
peer_addr: SocketAddr,
|
||||
secure: bool,
|
||||
) -> ResultType<()> {
|
||||
let mut stream = FramedStream::new(
|
||||
&crate::check_port(relay_server, RELAY_PORT),
|
||||
let mut stream = socket_client::connect_tcp(
|
||||
crate::check_port(relay_server, RELAY_PORT),
|
||||
Config::get_any_listen_addr(),
|
||||
CONNECT_TIMEOUT,
|
||||
)
|
||||
@@ -193,7 +196,7 @@ async fn create_relay_connection_(
|
||||
..Default::default()
|
||||
});
|
||||
stream.send(&msg_out).await?;
|
||||
create_tcp_connection_(server, stream, peer_addr, secure).await?;
|
||||
create_tcp_connection(server, stream, peer_addr, secure).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -36,35 +36,34 @@ mod pa_impl {
|
||||
use super::*;
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn run(sp: GenericService) -> ResultType<()> {
|
||||
if let Ok(mut stream) = crate::ipc::connect(1000, "_pa").await {
|
||||
let mut encoder =
|
||||
Encoder::new(crate::platform::linux::PA_SAMPLE_RATE, Stereo, LowDelay)?;
|
||||
allow_err!(
|
||||
stream
|
||||
.send(&crate::ipc::Data::Config((
|
||||
"audio-input".to_owned(),
|
||||
Some(Config::get_option("audio-input"))
|
||||
)))
|
||||
.await
|
||||
);
|
||||
while sp.ok() {
|
||||
sp.snapshot(|sps| {
|
||||
sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2));
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(data) = stream.next_timeout2(1000).await {
|
||||
match data? {
|
||||
Some(crate::ipc::Data::RawMessage(bytes)) => {
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts::<f32>(
|
||||
bytes.as_ptr() as _,
|
||||
bytes.len() / 4,
|
||||
)
|
||||
};
|
||||
send_f32(data, &mut encoder, &sp);
|
||||
}
|
||||
_ => {}
|
||||
hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc
|
||||
let mut stream = crate::ipc::connect(1000, "_pa").await?;
|
||||
unsafe {
|
||||
AUDIO_ZERO_COUNT = 0;
|
||||
}
|
||||
let mut encoder = Encoder::new(crate::platform::linux::PA_SAMPLE_RATE, Stereo, LowDelay)?;
|
||||
allow_err!(
|
||||
stream
|
||||
.send(&crate::ipc::Data::Config((
|
||||
"audio-input".to_owned(),
|
||||
Some(Config::get_option("audio-input"))
|
||||
)))
|
||||
.await
|
||||
);
|
||||
while sp.ok() {
|
||||
sp.snapshot(|sps| {
|
||||
sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2));
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(data) = stream.next_timeout2(1000).await {
|
||||
match data? {
|
||||
Some(crate::ipc::Data::RawMessage(bytes)) => {
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts::<f32>(bytes.as_ptr() as _, bytes.len() / 4)
|
||||
};
|
||||
send_f32(data, &mut encoder, &sp);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,9 +118,6 @@ mod cpal_impl {
|
||||
encoder: &mut Encoder,
|
||||
sp: &GenericService,
|
||||
) {
|
||||
if data.iter().filter(|x| **x != 0.).next().is_none() {
|
||||
return;
|
||||
}
|
||||
let buffer;
|
||||
let data = if sample_rate0 != sample_rate {
|
||||
buffer = crate::common::resample_channels(data, sample_rate0, sample_rate, channels);
|
||||
@@ -195,10 +191,10 @@ mod cpal_impl {
|
||||
let (device, config) = get_device()?;
|
||||
let sp = sp.clone();
|
||||
let err_fn = move |err| {
|
||||
log::error!("an error occurred on stream: {}", err);
|
||||
// too many UnknownErrno, will improve later
|
||||
log::trace!("an error occurred on stream: {}", err);
|
||||
};
|
||||
// Sample rate must be one of 8000, 12000, 16000, 24000, or 48000.
|
||||
// Note: somehow 48000 not work
|
||||
let sample_rate_0 = config.sample_rate().0;
|
||||
let sample_rate = if sample_rate_0 < 12000 {
|
||||
8000
|
||||
@@ -206,9 +202,15 @@ mod cpal_impl {
|
||||
12000
|
||||
} else if sample_rate_0 < 24000 {
|
||||
16000
|
||||
} else {
|
||||
} else if sample_rate_0 < 48000 {
|
||||
24000
|
||||
} else {
|
||||
48000
|
||||
};
|
||||
log::debug!("Audio sample rate : {}", sample_rate);
|
||||
unsafe {
|
||||
AUDIO_ZERO_COUNT = 0;
|
||||
}
|
||||
let mut encoder = Encoder::new(
|
||||
sample_rate,
|
||||
if config.channels() > 1 { Stereo } else { Mono },
|
||||
@@ -282,19 +284,39 @@ fn create_format_msg(sample_rate: u32, channels: u16) -> Message {
|
||||
msg
|
||||
}
|
||||
|
||||
// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time
|
||||
// every audio data length is set to 480
|
||||
// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows)
|
||||
const MAX_AUDIO_ZERO_COUNT: u16 = 800;
|
||||
static mut AUDIO_ZERO_COUNT: u16 = 0;
|
||||
|
||||
fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) {
|
||||
if data.iter().filter(|x| **x != 0.).next().is_some() {
|
||||
match encoder.encode_vec_float(data, data.len() * 6) {
|
||||
Ok(data) => {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_audio_frame(AudioFrame {
|
||||
data,
|
||||
..Default::default()
|
||||
});
|
||||
sp.send(msg_out);
|
||||
}
|
||||
Err(_) => {}
|
||||
unsafe {
|
||||
AUDIO_ZERO_COUNT = 0;
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT {
|
||||
if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 {
|
||||
log::debug!("Audio Zero Gate Attack");
|
||||
AUDIO_ZERO_COUNT += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
AUDIO_ZERO_COUNT += 1;
|
||||
}
|
||||
}
|
||||
match encoder.encode_vec_float(data, data.len() * 6) {
|
||||
Ok(data) => {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_audio_frame(AudioFrame {
|
||||
data,
|
||||
..Default::default()
|
||||
});
|
||||
sp.send(msg_out);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,49 +3,106 @@ pub use crate::common::{
|
||||
check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME,
|
||||
CONTENT,
|
||||
};
|
||||
|
||||
struct State {
|
||||
ctx: Option<ClipboardContext>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
let ctx = match ClipboardContext::new() {
|
||||
Ok(ctx) => Some(ctx),
|
||||
Err(err) => {
|
||||
log::error!("Failed to start {}: {}", NAME, err);
|
||||
None
|
||||
}
|
||||
};
|
||||
Self { ctx }
|
||||
}
|
||||
}
|
||||
|
||||
impl super::service::Reset for State {
|
||||
fn reset(&mut self) {
|
||||
*CONTENT.lock().unwrap() = Default::default();
|
||||
}
|
||||
}
|
||||
use clipboard_master::{CallbackResult, ClipboardHandler, Master};
|
||||
use hbb_common::{anyhow, ResultType};
|
||||
use std::{
|
||||
io, sync,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::SyncSender,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, true);
|
||||
sp.repeat::<State, _>(INTERVAL, run);
|
||||
sp.run::<_>(listen::run);
|
||||
sp
|
||||
}
|
||||
|
||||
fn run(sp: GenericService, state: &mut State) -> ResultType<()> {
|
||||
if let Some(ctx) = state.ctx.as_mut() {
|
||||
if let Some(msg) = check_clipboard(ctx, None) {
|
||||
sp.send(msg);
|
||||
}
|
||||
sp.snapshot(|sps| {
|
||||
let txt = crate::CONTENT.lock().unwrap().clone();
|
||||
if !txt.is_empty() {
|
||||
let msg_out = crate::create_clipboard_msg(txt);
|
||||
sps.send_shared(Arc::new(msg_out));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
mod listen {
|
||||
use super::*;
|
||||
|
||||
static RUNNING: AtomicBool = AtomicBool::new(true);
|
||||
static WAIT: Duration = Duration::from_millis(1500);
|
||||
|
||||
struct ClipHandle {
|
||||
tx: SyncSender<()>,
|
||||
}
|
||||
|
||||
impl ClipboardHandler for ClipHandle {
|
||||
fn on_clipboard_change(&mut self) -> CallbackResult {
|
||||
if !RUNNING.load(Ordering::SeqCst) {
|
||||
return CallbackResult::Stop;
|
||||
}
|
||||
|
||||
let _ = self.tx.send(());
|
||||
CallbackResult::Next
|
||||
}
|
||||
|
||||
fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult {
|
||||
if !RUNNING.load(Ordering::SeqCst) {
|
||||
CallbackResult::Stop
|
||||
} else {
|
||||
CallbackResult::StopWithError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn run(sp: GenericService) -> ResultType<()> {
|
||||
let mut ctx = match ClipboardContext::new() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => {
|
||||
log::error!("Failed to start {}: {}", NAME, err);
|
||||
return Err(anyhow::Error::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
if !RUNNING.load(Ordering::SeqCst) {
|
||||
RUNNING.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let (tx, rx) = sync::mpsc::sync_channel(12);
|
||||
let listener = tokio::spawn(async {
|
||||
log::info!("Clipboard listener running!");
|
||||
let _ = Master::new(ClipHandle { tx }).run();
|
||||
});
|
||||
|
||||
while sp.ok() {
|
||||
if let Ok(_) = rx.recv_timeout(WAIT) {
|
||||
if let Some(msg) = check_clipboard(&mut ctx, None) {
|
||||
sp.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
sp.snapshot(|sps| {
|
||||
let txt = crate::CONTENT.lock().unwrap().clone();
|
||||
if !txt.is_empty() {
|
||||
let msg_out = crate::create_clipboard_msg(txt);
|
||||
sps.send_shared(Arc::new(msg_out));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
RUNNING.store(false, Ordering::SeqCst);
|
||||
trigger(&mut ctx);
|
||||
let _ = listener.await;
|
||||
log::info!("Clipboard listener stopped!");
|
||||
|
||||
*CONTENT.lock().unwrap() = Default::default();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trigger(ctx: &mut ClipboardContext) {
|
||||
let _ = match ctx.get_text() {
|
||||
Ok(text) => {
|
||||
if !text.is_empty() {
|
||||
ctx.set_text(text).ok();
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ lazy_static::lazy_static! {
|
||||
pub struct ConnInner {
|
||||
id: i32,
|
||||
tx: Option<Sender>,
|
||||
tx_video: Option<Sender>,
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
@@ -61,9 +62,18 @@ impl Subscriber for ConnInner {
|
||||
|
||||
#[inline]
|
||||
fn send(&mut self, msg: Arc<Message>) {
|
||||
self.tx.as_mut().map(|tx| {
|
||||
allow_err!(tx.send((Instant::now(), msg)));
|
||||
});
|
||||
match &msg.union {
|
||||
Some(message::Union::video_frame(_)) => {
|
||||
self.tx_video.as_mut().map(|tx| {
|
||||
allow_err!(tx.send((Instant::now(), msg)));
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
self.tx.as_mut().map(|tx| {
|
||||
allow_err!(tx.send((Instant::now(), msg)));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +81,8 @@ const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
const SEC30: Duration = Duration::from_secs(30);
|
||||
const H1: Duration = Duration::from_secs(3600);
|
||||
const MILLI1: Duration = Duration::from_millis(1);
|
||||
const SEND_TIMEOUT_VIDEO: u64 = 12_000;
|
||||
const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10;
|
||||
|
||||
impl Connection {
|
||||
pub async fn start(
|
||||
@@ -87,8 +99,13 @@ impl Connection {
|
||||
let (tx_from_cm, mut rx_from_cm) = mpsc::unbounded_channel::<ipc::Data>();
|
||||
let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::<ipc::Data>();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>();
|
||||
let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>();
|
||||
let mut conn = Self {
|
||||
inner: ConnInner { id, tx: Some(tx) },
|
||||
inner: ConnInner {
|
||||
id,
|
||||
tx: Some(tx),
|
||||
tx_video: Some(tx_video),
|
||||
},
|
||||
stream,
|
||||
server,
|
||||
hash,
|
||||
@@ -120,19 +137,30 @@ impl Connection {
|
||||
return;
|
||||
}
|
||||
if !conn.keyboard {
|
||||
conn.send_permisssion(Permission::Keyboard, false).await;
|
||||
conn.send_permission(Permission::Keyboard, false).await;
|
||||
}
|
||||
if !conn.clipboard {
|
||||
conn.send_permisssion(Permission::Clipboard, false).await;
|
||||
conn.send_permission(Permission::Clipboard, false).await;
|
||||
}
|
||||
if !conn.audio {
|
||||
conn.send_permisssion(Permission::Audio, false).await;
|
||||
conn.send_permission(Permission::Audio, false).await;
|
||||
}
|
||||
let mut test_delay_timer =
|
||||
time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT);
|
||||
let mut last_recv_time = Instant::now();
|
||||
|
||||
conn.stream.set_send_timeout(
|
||||
if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() {
|
||||
SEND_TIMEOUT_OTHER
|
||||
} else {
|
||||
SEND_TIMEOUT_VIDEO
|
||||
},
|
||||
);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased; // video has higher priority
|
||||
|
||||
Some(data) = rx_from_cm.recv() => {
|
||||
match data {
|
||||
ipc::Data::Authorize => {
|
||||
@@ -164,7 +192,7 @@ impl Connection {
|
||||
log::info!("Change permission {} -> {}", name, enabled);
|
||||
if &name == "keyboard" {
|
||||
conn.keyboard = enabled;
|
||||
conn.send_permisssion(Permission::Keyboard, enabled).await;
|
||||
conn.send_permission(Permission::Keyboard, enabled).await;
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
@@ -172,7 +200,7 @@ impl Connection {
|
||||
}
|
||||
} else if &name == "clipboard" {
|
||||
conn.clipboard = enabled;
|
||||
conn.send_permisssion(Permission::Clipboard, enabled).await;
|
||||
conn.send_permission(Permission::Clipboard, enabled).await;
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::NAME,
|
||||
@@ -180,7 +208,7 @@ impl Connection {
|
||||
}
|
||||
} else if &name == "audio" {
|
||||
conn.audio = enabled;
|
||||
conn.send_permisssion(Permission::Audio, enabled).await;
|
||||
conn.send_permission(Permission::Audio, enabled).await;
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::audio_service::NAME,
|
||||
@@ -193,26 +221,6 @@ impl Connection {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some((instant, value)) = rx.recv() => {
|
||||
let latency = instant.elapsed().as_millis() as i64;
|
||||
super::video_service::update_internal_latency(id, latency);
|
||||
let msg: &Message = &value;
|
||||
if latency > 1000 {
|
||||
match &msg.union {
|
||||
Some(message::Union::video_frame(_)) => {
|
||||
continue;
|
||||
}
|
||||
Some(message::Union::audio_frame(_)) => {
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Err(err) = conn.stream.send(msg).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
res = conn.stream.next() => {
|
||||
if let Some(res) = res {
|
||||
@@ -244,7 +252,32 @@ impl Connection {
|
||||
} else {
|
||||
conn.timer = time::interval_at(Instant::now() + SEC30, SEC30);
|
||||
}
|
||||
}
|
||||
},
|
||||
Some((instant, value)) = rx_video.recv() => {
|
||||
video_service::notify_video_frame_feched(id, Some(instant.into()));
|
||||
if let Err(err) = conn.stream.send(&value as &Message).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
Some((instant, value)) = rx.recv() => {
|
||||
let latency = instant.elapsed().as_millis() as i64;
|
||||
let msg: &Message = &value;
|
||||
|
||||
if latency > 1000 {
|
||||
match &msg.union {
|
||||
Some(message::Union::audio_frame(_)) => {
|
||||
// log::info!("audio frame latency {}", instant.elapsed().as_secs_f32());
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Err(err) = conn.stream.send(msg).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = test_delay_timer.tick() => {
|
||||
if last_recv_time.elapsed() >= SEC30 {
|
||||
conn.on_close("Timeout", true);
|
||||
@@ -263,75 +296,61 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
}
|
||||
super::video_service::update_internal_latency(id, 0);
|
||||
|
||||
video_service::notify_video_frame_feched(id, None);
|
||||
super::video_service::update_test_latency(id, 0);
|
||||
super::video_service::update_image_quality(id, None);
|
||||
if let Some(forward) = conn.port_forward_socket.as_mut() {
|
||||
if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_port_forward_loop(
|
||||
&mut self,
|
||||
rx_from_cm: &mut mpsc::UnboundedReceiver<Data>,
|
||||
) -> ResultType<()> {
|
||||
let mut last_recv_time = Instant::now();
|
||||
if let Some(forward) = self.port_forward_socket.as_mut() {
|
||||
log::info!("Running port forwarding loop");
|
||||
conn.stream.set_raw();
|
||||
self.stream.set_raw();
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(data) = rx_from_cm.recv() => {
|
||||
match data {
|
||||
ipc::Data::Close => {
|
||||
conn.on_close("Close requested from connection manager", false);
|
||||
break;
|
||||
bail!("Close requested from selfection manager");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
res = forward.next() => {
|
||||
if let Some(res) = res {
|
||||
match res {
|
||||
Err(err) => {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
},
|
||||
Ok(bytes) => {
|
||||
last_recv_time = Instant::now();
|
||||
if let Err(err) = conn.stream.send_bytes(bytes.into()).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_recv_time = Instant::now();
|
||||
self.stream.send_bytes(res?.into()).await?;
|
||||
} else {
|
||||
conn.on_close("Forward reset by the peer", false);
|
||||
break;
|
||||
bail!("Forward reset by the peer");
|
||||
}
|
||||
},
|
||||
res = conn.stream.next() => {
|
||||
res = self.stream.next() => {
|
||||
if let Some(res) = res {
|
||||
match res {
|
||||
Err(err) => {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
},
|
||||
Ok(bytes) => {
|
||||
last_recv_time = Instant::now();
|
||||
if let Err(err) = forward.send(bytes.into()).await {
|
||||
conn.on_close(&err.to_string(), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_recv_time = Instant::now();
|
||||
timeout(SEND_TIMEOUT_OTHER, forward.send(res?.into())).await??;
|
||||
} else {
|
||||
conn.on_close("Stream reset by the peer", false);
|
||||
break;
|
||||
bail!("Stream reset by the peer");
|
||||
}
|
||||
},
|
||||
_ = conn.timer.tick() => {
|
||||
_ = self.timer.tick() => {
|
||||
if last_recv_time.elapsed() >= H1 {
|
||||
conn.on_close("Timeout", false);
|
||||
break;
|
||||
bail!("Timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_permisssion(&mut self, permission: Permission, enabled: bool) {
|
||||
async fn send_permission(&mut self, permission: Permission, enabled: bool) {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_permission_info(PermissionInfo {
|
||||
permission: permission.into(),
|
||||
@@ -570,7 +589,7 @@ impl Connection {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if lr.username != Config::get_id() {
|
||||
if !crate::is_ip(&lr.username) && lr.username != Config::get_id() {
|
||||
self.send_login_error("Offline").await;
|
||||
} else if lr.password.is_empty() {
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false).await;
|
||||
|
||||
@@ -195,15 +195,20 @@ fn modifier_sleep() {
|
||||
std::thread::sleep(std::time::Duration::from_nanos(1));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[inline]
|
||||
fn get_modifier_state(key: Key, en: &mut Enigo) -> bool {
|
||||
// on Linux, if RightAlt is down, RightAlt status is false, Alt status is true
|
||||
// but on Windows, both are true
|
||||
let x = en.get_key_state(key.clone());
|
||||
match key {
|
||||
Key::Shift => x || en.get_key_state(Key::RightShift),
|
||||
Key::Control => x || en.get_key_state(Key::RightControl),
|
||||
Key::Alt => x || en.get_key_state(Key::RightAlt),
|
||||
Key::Meta => x || en.get_key_state(Key::RWin),
|
||||
Key::RightShift => x || en.get_key_state(Key::Shift),
|
||||
Key::RightControl => x || en.get_key_state(Key::Control),
|
||||
Key::RightAlt => x || en.get_key_state(Key::Alt),
|
||||
Key::RWin => x || en.get_key_state(Key::Meta),
|
||||
_ => x,
|
||||
}
|
||||
}
|
||||
@@ -262,7 +267,7 @@ fn fix_key_down_timeout(force: bool) {
|
||||
if let Some(key) = key {
|
||||
let func = move || {
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
if en.get_key_state(key) {
|
||||
if get_modifier_state(key, &mut en) {
|
||||
en.key_up(key);
|
||||
log::debug!("Fixed {:?} timeout", key);
|
||||
}
|
||||
@@ -284,7 +289,7 @@ fn fix_modifier(
|
||||
key1: Key,
|
||||
en: &mut Enigo,
|
||||
) {
|
||||
if en.get_key_state(key1) && !modifiers.contains(&ProtobufEnumOrUnknown::new(key0)) {
|
||||
if get_modifier_state(key1, en) && !modifiers.contains(&ProtobufEnumOrUnknown::new(key0)) {
|
||||
en.key_up(key1);
|
||||
log::debug!("Fixed {:?}", key1);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
thread::{self, JoinHandle},
|
||||
time,
|
||||
};
|
||||
@@ -34,7 +35,7 @@ pub trait Reset {
|
||||
pub struct ServiceTmpl<T: Subscriber + From<ConnInner>>(Arc<RwLock<ServiceInner<T>>>);
|
||||
pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
|
||||
pub type GenericService = ServiceTmpl<ConnInner>;
|
||||
pub const HIBERATE_TIMEOUT: u64 = 30;
|
||||
pub const HIBERNATE_TIMEOUT: u64 = 30;
|
||||
pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
|
||||
@@ -143,6 +144,20 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_video_frame(&self, msg: Message) -> HashSet<i32> {
|
||||
self.send_video_frame_shared(Arc::new(msg))
|
||||
}
|
||||
|
||||
pub fn send_video_frame_shared(&self, msg: Arc<Message>) -> HashSet<i32> {
|
||||
let mut conn_ids = HashSet::new();
|
||||
let mut lock = self.0.write().unwrap();
|
||||
for s in lock.subscribes.values_mut() {
|
||||
s.send(msg.clone());
|
||||
conn_ids.insert(s.id());
|
||||
}
|
||||
conn_ids
|
||||
}
|
||||
|
||||
pub fn send_without(&self, msg: Message, sub: i32) {
|
||||
let mut lock = self.0.write().unwrap();
|
||||
let msg = Arc::new(msg);
|
||||
@@ -191,7 +206,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
let sp = self.clone();
|
||||
let mut callback = callback;
|
||||
let thread = thread::spawn(move || {
|
||||
let mut error_timeout = HIBERATE_TIMEOUT;
|
||||
let mut error_timeout = HIBERNATE_TIMEOUT;
|
||||
while sp.active() {
|
||||
if sp.has_subscribes() {
|
||||
log::debug!("Enter {} service inner loop", sp.name());
|
||||
@@ -199,7 +214,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
if let Err(err) = callback(sp.clone()) {
|
||||
log::error!("Error of {} service: {}", sp.name(), err);
|
||||
if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) {
|
||||
error_timeout = HIBERATE_TIMEOUT;
|
||||
error_timeout = HIBERNATE_TIMEOUT;
|
||||
} else {
|
||||
error_timeout *= 2;
|
||||
}
|
||||
@@ -213,7 +228,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
log::debug!("Exit {} service inner loop", sp.name());
|
||||
}
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(HIBERATE_TIMEOUT));
|
||||
thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT));
|
||||
}
|
||||
});
|
||||
self.0.write().unwrap().handle = Some(thread);
|
||||
|
||||
@@ -1,395 +1,523 @@
|
||||
// 24FPS (actually 23.976FPS) is what video professionals ages ago determined to be the
|
||||
// slowest playback rate that still looks smooth enough to feel real.
|
||||
// Our eyes can see a slight difference and even though 30FPS actually shows
|
||||
// more information and is more realistic.
|
||||
// 60FPS is commonly used in game, teamviewer 12 support this for video editing user.
|
||||
|
||||
// how to capture with mouse cursor:
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/desktop-dup-api?redirectedfrom=MSDN
|
||||
|
||||
// 实现了硬件编解码和音频抓取,还绘制了鼠标
|
||||
// https://github.com/PHZ76/DesktopSharing
|
||||
|
||||
// dxgi memory leak issue
|
||||
// https://stackoverflow.com/questions/47801238/memory-leak-in-creating-direct2d-device
|
||||
// but per my test, it is more related to AcquireNextFrame,
|
||||
// https://forums.developer.nvidia.com/t/dxgi-outputduplication-memory-leak-when-using-nv-but-not-amd-drivers/108582
|
||||
|
||||
// to-do:
|
||||
// https://slhck.info/video/2017/03/01/rate-control.html
|
||||
|
||||
use super::*;
|
||||
use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, VideoCodecId, STRIDE_ALIGN};
|
||||
use std::{
|
||||
io::ErrorKind::WouldBlock,
|
||||
time::{self, Instant},
|
||||
};
|
||||
|
||||
const WAIT_BASE: i32 = 17;
|
||||
pub const NAME: &'static str = "video";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX));
|
||||
static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
static ref SWITCH: Arc<Mutex<bool>> = Default::default();
|
||||
static ref INTERNAL_LATENCIES: Arc<Mutex<HashMap<i32, i64>>> = Default::default();
|
||||
static ref TEST_LATENCIES: Arc<Mutex<HashMap<i32, i64>>> = Default::default();
|
||||
static ref IMAGE_QUALITIES: Arc<Mutex<HashMap<i32, i32>>> = Default::default();
|
||||
}
|
||||
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, true);
|
||||
sp.run(run);
|
||||
sp
|
||||
}
|
||||
|
||||
fn run(sp: GenericService) -> ResultType<()> {
|
||||
let fps = 30;
|
||||
let spf = time::Duration::from_secs_f32(1. / (fps as f32));
|
||||
let (ndisplay, current, display) = get_current_display()?;
|
||||
let (origin, width, height) = (display.origin(), display.width(), display.height());
|
||||
log::debug!(
|
||||
"#displays={}, current={}, origin: {:?}, width={}, height={}",
|
||||
ndisplay,
|
||||
current,
|
||||
&origin,
|
||||
width,
|
||||
height
|
||||
);
|
||||
// Capturer object is expensive, avoiding to create it frequently.
|
||||
let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?;
|
||||
|
||||
let q = get_image_quality();
|
||||
let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q);
|
||||
log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer);
|
||||
let mut wait = WAIT_BASE;
|
||||
let cfg = Config {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
timebase: [1, 1000], // Output timestamp precision
|
||||
bitrate,
|
||||
codec: VideoCodecId::VP9,
|
||||
rc_min_quantizer,
|
||||
rc_max_quantizer,
|
||||
speed,
|
||||
};
|
||||
let mut vpx;
|
||||
let mut n = ((width * height) as f64 / (1920 * 1080) as f64).round() as u32;
|
||||
if n < 1 {
|
||||
n = 1;
|
||||
}
|
||||
match Encoder::new(&cfg, n) {
|
||||
Ok(x) => vpx = x,
|
||||
Err(err) => bail!("Failed to create encoder: {}", err),
|
||||
}
|
||||
|
||||
if *SWITCH.lock().unwrap() {
|
||||
log::debug!("Broadcasting display switch");
|
||||
let mut misc = Misc::new();
|
||||
misc.set_switch_display(SwitchDisplay {
|
||||
display: current as _,
|
||||
x: origin.0 as _,
|
||||
y: origin.1 as _,
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_misc(misc);
|
||||
*SWITCH.lock().unwrap() = false;
|
||||
sp.send(msg_out);
|
||||
}
|
||||
|
||||
let mut crc = (0, 0);
|
||||
let start = time::Instant::now();
|
||||
let mut last_sent = time::Instant::now();
|
||||
let mut last_check_displays = time::Instant::now();
|
||||
while sp.ok() {
|
||||
if *SWITCH.lock().unwrap() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if current != *CURRENT_DISPLAY.lock().unwrap() {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if get_image_quality() != q {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if crate::platform::windows::desktop_changed() {
|
||||
bail!("Desktop changed");
|
||||
}
|
||||
}
|
||||
let now = time::Instant::now();
|
||||
if last_check_displays.elapsed().as_millis() > 1000 {
|
||||
last_check_displays = now;
|
||||
if ndisplay != get_display_num() {
|
||||
log::info!("Displays changed");
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
}
|
||||
*LAST_ACTIVE.lock().unwrap() = now;
|
||||
if get_latency() < 1000 || last_sent.elapsed().as_millis() > 1000 {
|
||||
match c.frame(wait as _) {
|
||||
Ok(frame) => {
|
||||
let time = now - start;
|
||||
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
|
||||
handle_one_frame(&sp, &frame, ms, &mut crc, &mut vpx)?;
|
||||
last_sent = now;
|
||||
}
|
||||
Err(ref e) if e.kind() == WouldBlock => {
|
||||
// https://github.com/NVIDIA/video-sdk-samples/tree/master/nvEncDXGIOutputDuplicationSample
|
||||
wait = WAIT_BASE - now.elapsed().as_millis() as i32;
|
||||
if wait < 0 {
|
||||
wait = 0
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let elapsed = now.elapsed();
|
||||
// may need to enable frame(timeout)
|
||||
log::trace!("{:?} {:?}", time::Instant::now(), elapsed);
|
||||
if elapsed < spf {
|
||||
std::thread::sleep(spf - elapsed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_msg(vp9s: Vec<VP9>) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut vf = VideoFrame::new();
|
||||
vf.set_vp9s(VP9s {
|
||||
frames: vp9s.into(),
|
||||
..Default::default()
|
||||
});
|
||||
msg_out.set_video_frame(vf);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_frame(frame: &EncodeFrame) -> VP9 {
|
||||
VP9 {
|
||||
data: frame.data.to_vec(),
|
||||
key: frame.key,
|
||||
pts: frame.pts,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn handle_one_frame(
|
||||
sp: &GenericService,
|
||||
frame: &[u8],
|
||||
ms: i64,
|
||||
crc: &mut (u32, u32),
|
||||
vpx: &mut Encoder,
|
||||
) -> ResultType<()> {
|
||||
sp.snapshot(|sps| {
|
||||
// so that new sub and old sub share the same encoder after switch
|
||||
if sps.has_subscribes() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let mut hasher = crc32fast::Hasher::new();
|
||||
hasher.update(frame);
|
||||
let checksum = hasher.finalize();
|
||||
if checksum != crc.0 {
|
||||
crc.0 = checksum;
|
||||
crc.1 = 0;
|
||||
} else {
|
||||
crc.1 += 1;
|
||||
}
|
||||
if crc.1 <= 180 && crc.1 % 5 == 0 {
|
||||
let mut frames = Vec::new();
|
||||
for ref frame in vpx
|
||||
.encode(ms, frame, STRIDE_ALIGN)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(create_frame(frame));
|
||||
}
|
||||
for ref frame in vpx.flush().with_context(|| "Failed to flush")? {
|
||||
frames.push(create_frame(frame));
|
||||
}
|
||||
// to-do: flush periodically, e.g. 1 second
|
||||
if frames.len() > 0 {
|
||||
sp.send(create_msg(frames));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_display_num() -> usize {
|
||||
if let Ok(d) = Display::all() {
|
||||
d.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
|
||||
// switch to primary display if long time (30 seconds) no users
|
||||
if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
|
||||
*CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
|
||||
}
|
||||
let mut displays = Vec::new();
|
||||
let mut primary = 0;
|
||||
for (i, d) in Display::all()?.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
primary = i;
|
||||
}
|
||||
displays.push(DisplayInfo {
|
||||
x: d.origin().0 as _,
|
||||
y: d.origin().1 as _,
|
||||
width: d.width() as _,
|
||||
height: d.height() as _,
|
||||
name: d.name(),
|
||||
online: d.is_online(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let mut lock = CURRENT_DISPLAY.lock().unwrap();
|
||||
if *lock >= displays.len() {
|
||||
*lock = primary
|
||||
}
|
||||
Ok((*lock, displays))
|
||||
}
|
||||
|
||||
pub fn switch_display(i: i32) {
|
||||
let i = i as usize;
|
||||
if let Ok((_, displays)) = get_displays() {
|
||||
if i < displays.len() {
|
||||
*CURRENT_DISPLAY.lock().unwrap() = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh() {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
fn get_primary() -> usize {
|
||||
if let Ok(all) = Display::all() {
|
||||
for (i, d) in all.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub fn switch_to_primary() {
|
||||
switch_display(get_primary() as _);
|
||||
}
|
||||
|
||||
fn get_current_display() -> ResultType<(usize, usize, Display)> {
|
||||
let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize;
|
||||
let mut displays = Display::all()?;
|
||||
if displays.len() == 0 {
|
||||
bail!("No displays");
|
||||
}
|
||||
let n = displays.len();
|
||||
if current >= n {
|
||||
current = 0;
|
||||
for (i, d) in displays.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
*CURRENT_DISPLAY.lock().unwrap() = current;
|
||||
}
|
||||
return Ok((n, current, displays.remove(current)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_latency(id: i32, latency: i64, latencies: &mut HashMap<i32, i64>) {
|
||||
if latency <= 0 {
|
||||
latencies.remove(&id);
|
||||
} else {
|
||||
latencies.insert(id, latency);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_test_latency(id: i32, latency: i64) {
|
||||
update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap());
|
||||
}
|
||||
|
||||
pub fn update_internal_latency(id: i32, latency: i64) {
|
||||
update_latency(id, latency, &mut *INTERNAL_LATENCIES.lock().unwrap());
|
||||
}
|
||||
|
||||
pub fn get_latency() -> i64 {
|
||||
INTERNAL_LATENCIES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.max()
|
||||
.unwrap_or(&0)
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn convert_quality(q: i32) -> i32 {
|
||||
let q = {
|
||||
if q == ImageQuality::Balanced.value() {
|
||||
(100 * 2 / 3, 12)
|
||||
} else if q == ImageQuality::Low.value() {
|
||||
(100 / 2, 18)
|
||||
} else if q == ImageQuality::Best.value() {
|
||||
(100, 12)
|
||||
} else {
|
||||
let bitrate = q >> 8 & 0xFF;
|
||||
let quantizer = q & 0xFF;
|
||||
(bitrate * 2, (100 - quantizer) * 36 / 100)
|
||||
}
|
||||
};
|
||||
if q.0 <= 0 {
|
||||
0
|
||||
} else {
|
||||
q.0 << 8 | q.1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_image_quality(id: i32, q: Option<i32>) {
|
||||
match q {
|
||||
Some(q) => {
|
||||
let q = convert_quality(q);
|
||||
if q > 0 {
|
||||
IMAGE_QUALITIES.lock().unwrap().insert(id, q);
|
||||
} else {
|
||||
IMAGE_QUALITIES.lock().unwrap().remove(&id);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
IMAGE_QUALITIES.lock().unwrap().remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image_quality() -> i32 {
|
||||
IMAGE_QUALITIES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.min()
|
||||
.unwrap_or(&convert_quality(ImageQuality::Balanced.value()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) {
|
||||
// https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
|
||||
let bitrate = q >> 8 & 0xFF;
|
||||
let quantizer = q & 0xFF;
|
||||
let b = ((w * h) / 1000) as u32;
|
||||
(bitrate as u32 * b / 100, quantizer as _, 56, 7)
|
||||
}
|
||||
// 24FPS (actually 23.976FPS) is what video professionals ages ago determined to be the
|
||||
// slowest playback rate that still looks smooth enough to feel real.
|
||||
// Our eyes can see a slight difference and even though 30FPS actually shows
|
||||
// more information and is more realistic.
|
||||
// 60FPS is commonly used in game, teamviewer 12 support this for video editing user.
|
||||
|
||||
// how to capture with mouse cursor:
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/desktop-dup-api?redirectedfrom=MSDN
|
||||
|
||||
// RECORD: The following Project has implemented audio capture, hardware codec and mouse cursor drawn.
|
||||
// https://github.com/PHZ76/DesktopSharing
|
||||
|
||||
// dxgi memory leak issue
|
||||
// https://stackoverflow.com/questions/47801238/memory-leak-in-creating-direct2d-device
|
||||
// but per my test, it is more related to AcquireNextFrame,
|
||||
// https://forums.developer.nvidia.com/t/dxgi-outputduplication-memory-leak-when-using-nv-but-not-amd-drivers/108582
|
||||
|
||||
// to-do:
|
||||
// https://slhck.info/video/2017/03/01/rate-control.html
|
||||
|
||||
use super::*;
|
||||
use hbb_common::tokio::{
|
||||
runtime::Runtime,
|
||||
sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex as TokioMutex,
|
||||
},
|
||||
};
|
||||
use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, VideoCodecId, STRIDE_ALIGN};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::ErrorKind::WouldBlock,
|
||||
time::{self, Duration, Instant},
|
||||
};
|
||||
|
||||
const WAIT_BASE: i32 = 17;
|
||||
pub const NAME: &'static str = "video";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX));
|
||||
static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
static ref SWITCH: Arc<Mutex<bool>> = Default::default();
|
||||
static ref TEST_LATENCIES: Arc<Mutex<HashMap<i32, i64>>> = Default::default();
|
||||
static ref IMAGE_QUALITIES: Arc<Mutex<HashMap<i32, i32>>> = Default::default();
|
||||
static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option<Instant>)>, Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>) = {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
(tx, Arc::new(TokioMutex::new(rx)))
|
||||
};
|
||||
}
|
||||
|
||||
pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option<Instant>) {
|
||||
FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap()
|
||||
}
|
||||
|
||||
struct VideoFrameController {
|
||||
cur: Instant,
|
||||
send_conn_ids: HashSet<i32>,
|
||||
rt: Runtime,
|
||||
}
|
||||
|
||||
impl VideoFrameController {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cur: Instant::now(),
|
||||
send_conn_ids: HashSet::new(),
|
||||
rt: Runtime::new().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.send_conn_ids.clear();
|
||||
}
|
||||
|
||||
fn set_send(&mut self, tm: Instant, conn_ids: HashSet<i32>) {
|
||||
if !conn_ids.is_empty() {
|
||||
self.cur = tm;
|
||||
self.send_conn_ids = conn_ids;
|
||||
}
|
||||
}
|
||||
|
||||
fn blocking_wait_next(&mut self, timeout_millis: u128) {
|
||||
if self.send_conn_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let send_conn_ids = self.send_conn_ids.clone();
|
||||
self.rt.block_on(async move {
|
||||
let mut fetched_conn_ids = HashSet::new();
|
||||
let begin = Instant::now();
|
||||
while begin.elapsed().as_millis() < timeout_millis {
|
||||
let timeout_dur =
|
||||
Duration::from_millis((timeout_millis - begin.elapsed().as_millis()) as u64);
|
||||
match tokio::time::timeout(
|
||||
timeout_dur,
|
||||
FRAME_FETCHED_NOTIFIER.1.lock().await.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(_) => {
|
||||
// break if timeout
|
||||
// log::error!("blocking wait frame receiving timeout {}", timeout_millis);
|
||||
break;
|
||||
}
|
||||
Ok(Some((id, instant))) => {
|
||||
if let Some(tm) = instant {
|
||||
log::trace!("channel recv latency: {}", tm.elapsed().as_secs_f32());
|
||||
}
|
||||
fetched_conn_ids.insert(id);
|
||||
|
||||
// break if all connections have received current frame
|
||||
if fetched_conn_ids.len() >= send_conn_ids.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// this branch would nerver be reached
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> GenericService {
|
||||
let sp = GenericService::new(NAME, true);
|
||||
sp.run(run);
|
||||
sp
|
||||
}
|
||||
|
||||
fn check_display_changed(
|
||||
last_n: usize,
|
||||
last_current: usize,
|
||||
last_width: usize,
|
||||
last_hegiht: usize,
|
||||
) -> bool {
|
||||
let displays = match Display::all() {
|
||||
Ok(d) => d,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let n = displays.len();
|
||||
if n != last_n {
|
||||
return true;
|
||||
};
|
||||
|
||||
for (i, d) in displays.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
if i != last_current {
|
||||
return true;
|
||||
};
|
||||
if d.width() != last_width || d.height() != last_hegiht {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn run(sp: GenericService) -> ResultType<()> {
|
||||
let fps = 30;
|
||||
let spf = time::Duration::from_secs_f32(1. / (fps as f32));
|
||||
let (ndisplay, current, display) = get_current_display()?;
|
||||
let (origin, width, height) = (display.origin(), display.width(), display.height());
|
||||
log::debug!(
|
||||
"#displays={}, current={}, origin: {:?}, width={}, height={}",
|
||||
ndisplay,
|
||||
current,
|
||||
&origin,
|
||||
width,
|
||||
height
|
||||
);
|
||||
// Capturer object is expensive, avoiding to create it frequently.
|
||||
let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?;
|
||||
|
||||
let q = get_image_quality();
|
||||
let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q);
|
||||
log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer);
|
||||
let mut wait = WAIT_BASE;
|
||||
let cfg = Config {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
timebase: [1, 1000], // Output timestamp precision
|
||||
bitrate,
|
||||
codec: VideoCodecId::VP9,
|
||||
rc_min_quantizer,
|
||||
rc_max_quantizer,
|
||||
speed,
|
||||
};
|
||||
let mut vpx;
|
||||
match Encoder::new(&cfg, 0) {
|
||||
Ok(x) => vpx = x,
|
||||
Err(err) => bail!("Failed to create encoder: {}", err),
|
||||
}
|
||||
|
||||
if *SWITCH.lock().unwrap() {
|
||||
log::debug!("Broadcasting display switch");
|
||||
let mut misc = Misc::new();
|
||||
misc.set_switch_display(SwitchDisplay {
|
||||
display: current as _,
|
||||
x: origin.0 as _,
|
||||
y: origin.1 as _,
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_misc(misc);
|
||||
*SWITCH.lock().unwrap() = false;
|
||||
sp.send(msg_out);
|
||||
}
|
||||
|
||||
let mut frame_controller = VideoFrameController::new();
|
||||
|
||||
let mut crc = (0, 0);
|
||||
let start = time::Instant::now();
|
||||
let mut last_check_displays = time::Instant::now();
|
||||
#[cfg(windows)]
|
||||
let mut try_gdi = 1;
|
||||
#[cfg(windows)]
|
||||
log::info!("gdi: {}", c.is_gdi());
|
||||
while sp.ok() {
|
||||
if *SWITCH.lock().unwrap() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if current != *CURRENT_DISPLAY.lock().unwrap() {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if get_image_quality() != q {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if crate::platform::windows::desktop_changed() {
|
||||
bail!("Desktop changed");
|
||||
}
|
||||
}
|
||||
let now = time::Instant::now();
|
||||
if last_check_displays.elapsed().as_millis() > 1000 {
|
||||
last_check_displays = now;
|
||||
if ndisplay != get_display_num() {
|
||||
log::info!("Displays changed");
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
}
|
||||
*LAST_ACTIVE.lock().unwrap() = now;
|
||||
|
||||
frame_controller.reset();
|
||||
|
||||
match c.frame(wait as _) {
|
||||
Ok(frame) => {
|
||||
let time = now - start;
|
||||
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
|
||||
let send_conn_ids = handle_one_frame(&sp, &frame, ms, &mut crc, &mut vpx)?;
|
||||
frame_controller.set_send(now, send_conn_ids);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
try_gdi = 0;
|
||||
}
|
||||
}
|
||||
Err(ref e) if e.kind() == WouldBlock => {
|
||||
// https://github.com/NVIDIA/video-sdk-samples/tree/master/nvEncDXGIOutputDuplicationSample
|
||||
wait = WAIT_BASE - now.elapsed().as_millis() as i32;
|
||||
if wait < 0 {
|
||||
wait = 0
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if try_gdi > 0 && !c.is_gdi() {
|
||||
if try_gdi > 3 {
|
||||
c.set_gdi();
|
||||
try_gdi = 0;
|
||||
log::info!("No image, fall back to gdi");
|
||||
}
|
||||
try_gdi += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
if check_display_changed(ndisplay, current, width, height) {
|
||||
log::info!("Displays changed");
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
|
||||
// i love 3, 6, 8
|
||||
frame_controller.blocking_wait_next(3_000);
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
// may need to enable frame(timeout)
|
||||
log::trace!("{:?} {:?}", time::Instant::now(), elapsed);
|
||||
if elapsed < spf {
|
||||
std::thread::sleep(spf - elapsed);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_msg(vp9s: Vec<VP9>) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut vf = VideoFrame::new();
|
||||
vf.set_vp9s(VP9s {
|
||||
frames: vp9s.into(),
|
||||
..Default::default()
|
||||
});
|
||||
msg_out.set_video_frame(vf);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_frame(frame: &EncodeFrame) -> VP9 {
|
||||
VP9 {
|
||||
data: frame.data.to_vec(),
|
||||
key: frame.key,
|
||||
pts: frame.pts,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn handle_one_frame(
|
||||
sp: &GenericService,
|
||||
frame: &[u8],
|
||||
ms: i64,
|
||||
crc: &mut (u32, u32),
|
||||
vpx: &mut Encoder,
|
||||
) -> ResultType<HashSet<i32>> {
|
||||
sp.snapshot(|sps| {
|
||||
// so that new sub and old sub share the same encoder after switch
|
||||
if sps.has_subscribes() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let mut hasher = crc32fast::Hasher::new();
|
||||
hasher.update(frame);
|
||||
let checksum = hasher.finalize();
|
||||
if checksum != crc.0 {
|
||||
crc.0 = checksum;
|
||||
crc.1 = 0;
|
||||
} else {
|
||||
crc.1 += 1;
|
||||
}
|
||||
|
||||
let mut send_conn_ids: HashSet<i32> = Default::default();
|
||||
if crc.1 <= 180 && crc.1 % 5 == 0 {
|
||||
let mut frames = Vec::new();
|
||||
for ref frame in vpx
|
||||
.encode(ms, frame, STRIDE_ALIGN)
|
||||
.with_context(|| "Failed to encode")?
|
||||
{
|
||||
frames.push(create_frame(frame));
|
||||
}
|
||||
for ref frame in vpx.flush().with_context(|| "Failed to flush")? {
|
||||
frames.push(create_frame(frame));
|
||||
}
|
||||
|
||||
// to-do: flush periodically, e.g. 1 second
|
||||
if frames.len() > 0 {
|
||||
send_conn_ids = sp.send_video_frame(create_msg(frames));
|
||||
}
|
||||
}
|
||||
Ok(send_conn_ids)
|
||||
}
|
||||
|
||||
fn get_display_num() -> usize {
|
||||
if let Ok(d) = Display::all() {
|
||||
d.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
|
||||
// switch to primary display if long time (30 seconds) no users
|
||||
if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 {
|
||||
*CURRENT_DISPLAY.lock().unwrap() = usize::MAX;
|
||||
}
|
||||
let mut displays = Vec::new();
|
||||
let mut primary = 0;
|
||||
for (i, d) in Display::all()?.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
primary = i;
|
||||
}
|
||||
displays.push(DisplayInfo {
|
||||
x: d.origin().0 as _,
|
||||
y: d.origin().1 as _,
|
||||
width: d.width() as _,
|
||||
height: d.height() as _,
|
||||
name: d.name(),
|
||||
online: d.is_online(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let mut lock = CURRENT_DISPLAY.lock().unwrap();
|
||||
if *lock >= displays.len() {
|
||||
*lock = primary
|
||||
}
|
||||
Ok((*lock, displays))
|
||||
}
|
||||
|
||||
pub fn switch_display(i: i32) {
|
||||
let i = i as usize;
|
||||
if let Ok((_, displays)) = get_displays() {
|
||||
if i < displays.len() {
|
||||
*CURRENT_DISPLAY.lock().unwrap() = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh() {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
fn get_primary() -> usize {
|
||||
if let Ok(all) = Display::all() {
|
||||
for (i, d) in all.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub fn switch_to_primary() {
|
||||
switch_display(get_primary() as _);
|
||||
}
|
||||
|
||||
fn get_current_display() -> ResultType<(usize, usize, Display)> {
|
||||
let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize;
|
||||
let mut displays = Display::all()?;
|
||||
if displays.len() == 0 {
|
||||
bail!("No displays");
|
||||
}
|
||||
let n = displays.len();
|
||||
if current >= n {
|
||||
current = 0;
|
||||
for (i, d) in displays.iter().enumerate() {
|
||||
if d.is_primary() {
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
*CURRENT_DISPLAY.lock().unwrap() = current;
|
||||
}
|
||||
return Ok((n, current, displays.remove(current)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_latency(id: i32, latency: i64, latencies: &mut HashMap<i32, i64>) {
|
||||
if latency <= 0 {
|
||||
latencies.remove(&id);
|
||||
} else {
|
||||
latencies.insert(id, latency);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_test_latency(id: i32, latency: i64) {
|
||||
update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap());
|
||||
}
|
||||
|
||||
fn convert_quality(q: i32) -> i32 {
|
||||
let q = {
|
||||
if q == ImageQuality::Balanced.value() {
|
||||
(100 * 2 / 3, 12)
|
||||
} else if q == ImageQuality::Low.value() {
|
||||
(100 / 2, 18)
|
||||
} else if q == ImageQuality::Best.value() {
|
||||
(100, 12)
|
||||
} else {
|
||||
let bitrate = q >> 8 & 0xFF;
|
||||
let quantizer = q & 0xFF;
|
||||
(bitrate * 2, (100 - quantizer) * 36 / 100)
|
||||
}
|
||||
};
|
||||
if q.0 <= 0 {
|
||||
0
|
||||
} else {
|
||||
q.0 << 8 | q.1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_image_quality(id: i32, q: Option<i32>) {
|
||||
match q {
|
||||
Some(q) => {
|
||||
let q = convert_quality(q);
|
||||
if q > 0 {
|
||||
IMAGE_QUALITIES.lock().unwrap().insert(id, q);
|
||||
} else {
|
||||
IMAGE_QUALITIES.lock().unwrap().remove(&id);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
IMAGE_QUALITIES.lock().unwrap().remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image_quality() -> i32 {
|
||||
IMAGE_QUALITIES
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.min()
|
||||
.unwrap_or(&convert_quality(ImageQuality::Balanced.value()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) {
|
||||
// https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
|
||||
let bitrate = q >> 8 & 0xFF;
|
||||
let quantizer = q & 0xFF;
|
||||
let b = ((w * h) / 1000) as u32;
|
||||
(bitrate as u32 * b / 100, quantizer as _, 56, 7)
|
||||
}
|
||||
|
||||
154
src/ui.rs
154
src/ui.rs
@@ -8,7 +8,7 @@ use crate::common::SOFTWARE_UPDATE_URL;
|
||||
use crate::ipc;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
config::{Config, PeerConfig, APP_NAME, ICON},
|
||||
config::{self, Config, Fav, PeerConfig, APP_NAME, ICON},
|
||||
log, sleep,
|
||||
tokio::{self, time},
|
||||
};
|
||||
@@ -29,6 +29,8 @@ struct UI(
|
||||
Arc<Mutex<HashMap<String, String>>>,
|
||||
);
|
||||
|
||||
struct UIHostHandler;
|
||||
|
||||
fn get_msgbox() -> String {
|
||||
#[cfg(feature = "inline")]
|
||||
return inline::get_msgbox();
|
||||
@@ -86,10 +88,12 @@ pub fn start(args: &mut [String]) {
|
||||
std::thread::spawn(move || check_zombie(cloned));
|
||||
crate::common::check_software_update();
|
||||
frame.event_handler(UI::new(childs));
|
||||
frame.sciter_handler(UIHostHandler {});
|
||||
page = "index.html";
|
||||
} else if args[0] == "--install" {
|
||||
let childs: Childs = Default::default();
|
||||
frame.event_handler(UI::new(childs));
|
||||
frame.sciter_handler(UIHostHandler {});
|
||||
page = "install.html";
|
||||
} else if args[0] == "--cm" {
|
||||
frame.register_behavior("connection-manager", move || {
|
||||
@@ -141,7 +145,6 @@ pub fn start(args: &mut [String]) {
|
||||
|
||||
#[cfg(windows)]
|
||||
fn start_tray() -> hbb_common::ResultType<()> {
|
||||
/*
|
||||
let mut app = systray::Application::new()?;
|
||||
let icon = include_bytes!("./tray-icon.ico");
|
||||
app.set_icon_from_buffer(icon, 32, 32).unwrap();
|
||||
@@ -180,7 +183,7 @@ fn start_tray() -> hbb_common::ResultType<()> {
|
||||
Ok::<_, systray::Error>(())
|
||||
})?;
|
||||
allow_err!(app.wait_for_message());
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -274,6 +277,20 @@ impl UI {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_local_option(&self, key: String) -> String {
|
||||
Config::get_option(&key)
|
||||
}
|
||||
|
||||
fn peer_has_password(&self, id: String) -> bool {
|
||||
!PeerConfig::load(&id).password.is_empty()
|
||||
}
|
||||
|
||||
fn forget_password(&self, id: String) {
|
||||
let mut c = PeerConfig::load(&id);
|
||||
c.password.clear();
|
||||
c.store(&id);
|
||||
}
|
||||
|
||||
fn get_peer_option(&self, id: String, name: String) -> String {
|
||||
let c = PeerConfig::load(&id);
|
||||
c.options.get(&name).unwrap_or(&"".to_owned()).to_owned()
|
||||
@@ -298,7 +315,7 @@ impl UI {
|
||||
}
|
||||
|
||||
fn test_if_valid_server(&self, host: String) -> String {
|
||||
crate::common::test_if_valid_server(host)
|
||||
hbb_common::socket_client::test_if_valid_server(&host)
|
||||
}
|
||||
|
||||
fn get_sound_inputs(&self) -> Value {
|
||||
@@ -333,9 +350,21 @@ impl UI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*self.2.lock().unwrap() = m.clone();
|
||||
ipc::set_options(m).ok();
|
||||
}
|
||||
|
||||
fn set_option(&self, key: String, value: String) {
|
||||
let mut options = self.2.lock().unwrap();
|
||||
if value.is_empty() {
|
||||
options.remove(&key);
|
||||
} else {
|
||||
options.insert(key, value);
|
||||
}
|
||||
ipc::set_options(options.clone()).ok();
|
||||
}
|
||||
|
||||
fn install_path(&mut self) -> String {
|
||||
#[cfg(windows)]
|
||||
return crate::platform::windows::get_install_info().1;
|
||||
@@ -343,6 +372,29 @@ impl UI {
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
fn get_socks(&self) -> Value {
|
||||
let s = ipc::get_socks();
|
||||
match s {
|
||||
None => Value::null(),
|
||||
Some(s) => {
|
||||
let mut v = Value::array(0);
|
||||
v.push(s.proxy);
|
||||
v.push(s.username);
|
||||
v.push(s.password);
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_socks(&self, proxy: String, username: String, password: String) {
|
||||
ipc::set_socks(config::Socks5Server {
|
||||
proxy,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn is_installed(&mut self) -> bool {
|
||||
crate::platform::is_installed()
|
||||
}
|
||||
@@ -353,8 +405,8 @@ impl UI {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let installed_version = crate::platform::windows::get_installed_version();
|
||||
let a = crate::common::get_version_number(crate::VERSION);
|
||||
let b = crate::common::get_version_number(&installed_version);
|
||||
let a = hbb_common::get_version_number(crate::VERSION);
|
||||
let b = hbb_common::get_version_number(&installed_version);
|
||||
return a > b;
|
||||
}
|
||||
}
|
||||
@@ -382,22 +434,43 @@ impl UI {
|
||||
v
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_peer_value(id: String, p: PeerConfig) -> Value {
|
||||
let values = vec![
|
||||
id,
|
||||
p.info.username.clone(),
|
||||
p.info.hostname.clone(),
|
||||
p.info.platform.clone(),
|
||||
p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(),
|
||||
];
|
||||
Value::from_iter(values)
|
||||
}
|
||||
|
||||
fn get_peer(&self, id: String) -> Value {
|
||||
let c = PeerConfig::load(&id);
|
||||
Self::get_peer_value(id, c)
|
||||
}
|
||||
|
||||
fn get_fav(&self) -> Value {
|
||||
Value::from_iter(Fav::load().peers)
|
||||
}
|
||||
|
||||
fn store_fav(&self, fav: Value) {
|
||||
let mut tmp = vec![];
|
||||
fav.values().for_each(|v| {
|
||||
if let Some(v) = v.as_string() {
|
||||
if !v.is_empty() {
|
||||
tmp.push(v);
|
||||
}
|
||||
}
|
||||
});
|
||||
Fav::store(tmp);
|
||||
}
|
||||
|
||||
fn get_recent_sessions(&mut self) -> Value {
|
||||
let peers: Vec<Value> = PeerConfig::peers()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let values = vec![
|
||||
p.0.clone(),
|
||||
p.2.info.username.clone(),
|
||||
p.2.info.hostname.clone(),
|
||||
p.2.info.platform.clone(),
|
||||
p.2.options
|
||||
.get("alias")
|
||||
.unwrap_or(&"".to_owned())
|
||||
.to_owned(),
|
||||
];
|
||||
Value::from_iter(values)
|
||||
})
|
||||
.drain(..)
|
||||
.map(|p| Self::get_peer_value(p.0, p.2))
|
||||
.collect();
|
||||
Value::from_iter(peers)
|
||||
}
|
||||
@@ -456,6 +529,9 @@ impl UI {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let dtype = crate::platform::linux::get_display_server();
|
||||
if "wayland" == dtype {
|
||||
return "".to_owned();
|
||||
}
|
||||
if dtype != "x11" {
|
||||
return format!("Unsupported display server type {}, x11 expected!", dtype);
|
||||
}
|
||||
@@ -475,6 +551,20 @@ impl UI {
|
||||
return crate::platform::linux::fix_login_wayland();
|
||||
}
|
||||
|
||||
fn current_is_wayland(&mut self) -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
return crate::platform::linux::current_is_wayland();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return false;
|
||||
}
|
||||
|
||||
fn modify_default_login(&mut self) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
return crate::platform::linux::modify_default_login();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
fn get_software_update_url(&self) -> String {
|
||||
SOFTWARE_UPDATE_URL.lock().unwrap().clone()
|
||||
}
|
||||
@@ -529,6 +619,10 @@ impl UI {
|
||||
allow_err!(std::process::Command::new(p).arg(url).spawn());
|
||||
}
|
||||
|
||||
fn t(&self, name: String) -> String {
|
||||
crate::client::translate(name)
|
||||
}
|
||||
|
||||
fn is_xfce(&self) -> bool {
|
||||
crate::platform::is_xfce()
|
||||
}
|
||||
@@ -536,6 +630,7 @@ impl UI {
|
||||
|
||||
impl sciter::EventHandler for UI {
|
||||
sciter::dispatch_script_call! {
|
||||
fn t(String);
|
||||
fn is_xfce();
|
||||
fn get_id();
|
||||
fn get_password();
|
||||
@@ -548,11 +643,16 @@ impl sciter::EventHandler for UI {
|
||||
fn remove_peer(String);
|
||||
fn get_connect_status();
|
||||
fn get_recent_sessions();
|
||||
fn get_peer(String);
|
||||
fn get_fav();
|
||||
fn store_fav(Value);
|
||||
fn recent_sessions_updated();
|
||||
fn get_icon();
|
||||
fn get_msgbox();
|
||||
fn install_me(String);
|
||||
fn is_installed();
|
||||
fn set_socks(String, String, String);
|
||||
fn get_socks();
|
||||
fn is_installed_lower_version();
|
||||
fn install_path();
|
||||
fn goto_install();
|
||||
@@ -561,13 +661,19 @@ impl sciter::EventHandler for UI {
|
||||
fn get_error();
|
||||
fn is_login_wayland();
|
||||
fn fix_login_wayland();
|
||||
fn current_is_wayland();
|
||||
fn modify_default_login();
|
||||
fn get_options();
|
||||
fn get_option(String);
|
||||
fn get_local_option(String);
|
||||
fn get_peer_option(String, String);
|
||||
fn peer_has_password(String);
|
||||
fn forget_password(String);
|
||||
fn set_peer_option(String, String, String);
|
||||
fn test_if_valid_server(String);
|
||||
fn get_sound_inputs();
|
||||
fn set_options(Value);
|
||||
fn set_option(String, String);
|
||||
fn get_software_update_url();
|
||||
fn get_new_version();
|
||||
fn get_version();
|
||||
@@ -580,6 +686,12 @@ impl sciter::EventHandler for UI {
|
||||
}
|
||||
}
|
||||
|
||||
impl sciter::host::HostHandler for UIHostHandler {
|
||||
fn on_graphics_critical_failure(&mut self) {
|
||||
log::error!("Critical rendering error: e.g. DirectX gfx driver error. Most probably bad gfx drivers.");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_zombie(childs: Childs) {
|
||||
let mut deads = Vec::new();
|
||||
loop {
|
||||
@@ -602,7 +714,7 @@ pub fn check_zombie(childs: Childs) {
|
||||
}
|
||||
}
|
||||
|
||||
// notice: avoiding create ipc connecton repeatly,
|
||||
// notice: avoiding create ipc connection repeatedly,
|
||||
// because windows named pipe has serious memory leak issue.
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn check_connect_status_(
|
||||
|
||||
283
src/ui/ab.js
Normal file
283
src/ui/ab.js
Normal file
@@ -0,0 +1,283 @@
|
||||
import { handler, string2RGB, platformSvg, msgbox,translate,is_win,OS } from "./common.js";
|
||||
import { app, formatId, createNewConnect,svg_menu } from "./index.js"; // TODO check app obj
|
||||
// TODO transform
|
||||
const svg_tile = <svg id="session-tile" viewBox="0 0 158.6 158.6"><path style="stroke-width:.309756" d="M5.4 157.7c-1-.3-2-1-3.2-2.1-2.8-2.8-2.6-1-2.5-32 0-26.7 0-27 .7-28.3a9.3 9.3 0 0 1 4-4.2c1.2-.6 2.3-.6 29-.7 27.5 0 27.6 0 29.1.6.8.4 2 1.2 2.7 2 2.4 2.5 2.3.7 2.2 31.6-.1 26.5-.1 27.6-.7 28.8a9.3 9.3 0 0 1-4.2 4c-1.4.6-1.6.6-28.5.7a235 235 0 0 1-28.6-.4zm91 0a8.5 8.5 0 0 1-5.7-5.4c-.2-.7-.3-8.3-.3-28.3V96.7l.7-1.6a8.9 8.9 0 0 1 4.6-4.3c1.2-.4 3.8-.5 28.9-.4 26.6.1 27.6.1 28.8.7 1.6.8 3.2 2.5 4 4.2.7 1.4.7 1.6.7 28.3.1 31 .3 29.2-2.5 32-2.8 2.7-1 2.6-31.4 2.6-21.4 0-26.8-.1-27.9-.5zM5.3 67a8.7 8.7 0 0 1-4-3C-.5 61.6-.5 62.3-.5 33.6-.4 3.2-.5 5 2.2 2.2 5-.6 3.2-.4 34.2-.3c26.7 0 27 0 28.3.7 1.7.8 3.4 2.4 4.2 4 .6 1.2.6 2.2.7 28.8 0 25.1 0 27.7-.4 29a9 9 0 0 1-4.3 4.5l-1.6.7H33.7c-20.2 0-27.7-.1-28.4-.4Zm89.8-.3a9 9 0 0 1-4.3-4.6c-.5-1.2-.5-3.8-.5-28.9.1-26.6.2-27.6.7-28.8a9.3 9.3 0 0 1 4.2-4c1.4-.7 1.6-.7 28.3-.7 31-.1 29.2-.3 32 2.5 2.8 2.8 2.6 1 2.5 32 0 26.7 0 26.9-.7 28.3a9.3 9.3 0 0 1-4 4.2c-1.2.5-2.2.6-29 .6l-27.7.1z" transform="translate(.4 .4)" /></svg>;
|
||||
const svg_list = <svg id="session-list" viewBox="0 0 246.8 185.8"><path style="stroke-width:.482473" d="M-69.2 102.7A16.5 16.5 0 0 1-67 70.4c7.3-1 15 4 17.3 11 1 3 1 8 0 10.8a16.7 16.7 0 0 1-19.5 10.5zm53-3.4a12.3 12.3 0 0 1-7-16.8c1.3-3 3.1-4.7 6-6 2.2-1 2.8-1 87.2-1 92.4 0 87-.2 90.6 2.6.9.7 2.2 2.4 3 3.7 1.2 2.2 1.4 3.1 1.4 6 0 4.8-2.3 8.6-6.8 11l-1.9 1-85.2.1c-71.9 0-85.5 0-87.3-.6zm-53.5-73c-4.7-1.5-8.6-5-10.6-9.1-1.8-4-1.8-9.8 0-13.7 1.6-3.3 4.4-6.2 7.8-8 2.2-1.2 3-1.3 7.1-1.3 4 0 5 .1 7.3 1.3a16.6 16.6 0 0 1 0 29.6c-2 1-3.4 1.4-6.5 1.5-2.2 0-4.5 0-5.1-.3zm52.3-4.8c-2.4-1.1-5.3-4-6.2-6.5-1-2.4-1-7.3.1-9.7.5-1.1 1.8-2.8 2.8-3.8 3.7-3.5-4-3.2 91-3.2h85.5l2.5 1.1a12 12 0 0 1 0 21.8l-2.5 1.2H70.2c-82.5 0-85.7 0-87.6-1zm-52.1-71.6a18 18 0 0 1-10-7.7 17 17 0 0 1-.7-15c2.3-5 5.8-7.9 11.4-9.3 9-2.3 18.3 4 19.8 13.4a16.4 16.4 0 0 1-15.2 19c-2.1.1-4.1 0-5.3-.4zm52.1-5.9c-1.3-.6-3-1.7-3.7-2.5-4.7-5-4.2-13.7 1-18 3.7-3.1-1.8-3 91.5-2.8l84.9.1 2 1a12 12 0 0 1 6.7 11c0 3-.2 3.9-1.4 6-.8 1.4-2.1 3-3 3.8-3.7 2.7 1.8 2.6-90.6 2.6h-85l-2.4-1.2z" transform="translate(81.7 82.6)" /></svg>;
|
||||
const search_icon = <svg viewBox="0 0 655.278 655.024"><g transform="translate(-24.497 -195.01)"><path d="m649.96 847.92c-2.9592-1.3629-27.183-24.243-63.36-59.846-32.213-31.702-70.814-69.663-85.78-84.357l-27.21-26.717-4.7897 3.5287c-66.337 48.872-145.32 66.878-224.31 51.138-72.966-14.539-136.58-58.184-178.47-122.44-15.945-24.462-30.723-61.471-36.413-91.191-8.9404-46.696-6.2422-90.39 8.3388-135.04 13.39-41.003 34.756-75.42 66.479-107.09 74.506-74.377 183.71-99.89 284.22-66.397 62.352 20.777 117.67 65.579 150.79 122.12 38.716 66.101 46.59 147.55 21.43 221.66-9.9038 29.171-29.788 63.725-49.916 86.743l-7.0583 8.0717 3.0992 2.919c1.7046 1.6054 40.675 39.928 86.602 85.161 89.007 87.664 87.558 86.034 85.619 96.293-1.2888 6.8209-5.2313 12.041-11.321 14.989-6.7901 3.287-11.55 3.4093-17.952 0.46117zm-316.64-154.63c32.373-5.0481 61.075-15.115 86.553-30.358 47.942-28.683 83.505-72.09 100.89-123.14 35.043-102.91-6.4362-214.07-100.89-270.37-52.514-31.302-117.76-40.564-178.06-25.277-81.183 20.579-145.19 82.918-166.86 162.52-5.5757 20.478-7.445 35.423-7.445 59.52s1.8693 39.042 7.445 59.52c21.409 78.63 85.366 141.52 164.81 162.05 29.22 7.5511 66.493 9.756 93.564 5.5347z" stroke-width="1.28" /></g></svg>;
|
||||
const clear_icon = <svg viewBox="0 0 478.94 479.03"><path d="M217.488 478.45c-30.264-3.146-55.348-10.265-82.714-23.477C62.54 420.1 14.214 353.763 1.824 272.463c-2.412-15.82-2.434-50.027-.043-66.058 16.004-107.32 97.008-188.28 204.71-204.6 14.33-2.172 49.054-2.447 63-.498C323.95 8.915 371.3 32.2 409.03 69.927c37.697 37.698 61.125 85.349 68.605 139.54 1.943 14.08 1.68 48.804-.478 63-6.616 43.533-24.01 83.859-50.468 117-37.556 47.046-92.812 78.608-153.26 87.54-12.553 1.855-44.144 2.671-55.936 1.445zm42.144-32.045c15.649-1.602 29.895-4.63 44.856-9.531 78.146-25.604 133.49-94.718 141.94-177.26 6.245-60.993-16.1-123.3-59.94-167.14-55.797-55.797-139.4-75.365-213.52-49.98-77.69 26.609-131.51 94.14-140.42 176.19-4.761 43.843 6.392 91.899 30.274 130.44 41.468 66.926 119.01 105.26 196.82 97.29zm-138.69-80.346c-4.096-1.784-8.225-6.874-9.022-11.123-1.676-8.935-3.495-6.761 52.877-63.221l52.17-52.25-52.17-52.25c-56.544-56.632-54.56-54.249-52.834-63.451.924-4.923 6.905-10.904 11.828-11.828 9.201-1.726 6.819-3.71 63.451 52.834l52.25 52.169 52.25-52.169c56.632-56.544 54.25-54.56 63.451-52.834 4.923.923 10.904 6.905 11.828 11.828 1.726 9.201 3.71 6.818-52.834 63.451l-52.169 52.25 52.17 52.25c56.543 56.632 54.56 54.249 52.833 63.451-.923 4.923-6.905 10.904-11.828 11.828-9.201 1.726-6.818 3.71-63.455-52.838l-52.255-52.173-51.745 51.696c-28.496 28.469-53.01 52.166-54.56 52.742-3.766 1.4-8.515 1.26-12.234-.36z" /></svg>;
|
||||
|
||||
function getSessionsStyleOption(type) {
|
||||
return (type || "recent") + "-sessions-style";
|
||||
}
|
||||
|
||||
export function getSessionsStyle(type) {
|
||||
var v = handler.xcall("get_local_option",getSessionsStyleOption(type));
|
||||
if (!v) v = type == "ab" ? "list" : "tile";
|
||||
return v;
|
||||
}
|
||||
|
||||
var searchPatterns = {};
|
||||
|
||||
export class SearchBar extends Element {
|
||||
|
||||
this(props) {
|
||||
this.type = (props || {}).type || "";
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let value = searchPatterns[this.type] || "";
|
||||
setTimeout(() => { this.$("input").value = value; }, 1);
|
||||
return (<div class="search-id" >
|
||||
<span class="search-icon">{search_icon}</span>
|
||||
<input type="text" novalue={translate("Search ID")} />
|
||||
{value && <span class="clear-input">{clear_icon}</span>}
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at span.clear-input"](_) {
|
||||
this.$("input").value = '';
|
||||
this.onChange('');
|
||||
}
|
||||
|
||||
["on change at input"](_, el) {
|
||||
this.onChange(el.value.trim());
|
||||
}
|
||||
|
||||
onChange(v) {
|
||||
searchPatterns[this.type] = v;
|
||||
app.multipleSessions.update();
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionStyle extends Element {
|
||||
type = "";
|
||||
|
||||
this(props) {
|
||||
this.type = (props || {}).type || "";
|
||||
}
|
||||
|
||||
render() {
|
||||
let sessionsStyle = getSessionsStyle(this.type);
|
||||
return (<div class="sessions-tab" style="margin-left: 0.5em;">
|
||||
<span class={sessionsStyle == "tile" ? "active" : "inactive"}>{svg_tile}</span>
|
||||
<span class={sessionsStyle != "tile" ? "active" : "inactive"}>{svg_list}</span>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at span.inactive"](_) {
|
||||
let option = getSessionsStyleOption(this.type);
|
||||
let sessionsStyle = getSessionsStyle(this.type);
|
||||
handler.xcall("set_option", option, sessionsStyle == "tile" ? "list" : "tile");
|
||||
app.componentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionList extends Element {
|
||||
sessions = [];
|
||||
type = "";
|
||||
style;
|
||||
|
||||
|
||||
this(props) {
|
||||
this.sessions = props.sessions;
|
||||
this.type = props.type;
|
||||
this.style = getSessionsStyle(props.type);
|
||||
}
|
||||
|
||||
getSessions() {
|
||||
let p = searchPatterns[this.type];
|
||||
if (!p) return this.sessions;
|
||||
let tmp = [];
|
||||
this.sessions.map( (s) => {
|
||||
let name = s[4] || s.alias || s[0] || s.id || "";
|
||||
if (name.indexOf(p) >= 0) tmp.push(s);
|
||||
});
|
||||
return tmp;
|
||||
}
|
||||
|
||||
render() {
|
||||
let sessions = this.getSessions();
|
||||
if (sessions.length == 0) return <div style="margin: *; font-size: 1.6em;">{translate("Empty")}</div>;
|
||||
sessions = sessions.map((x) => this.getSession(x));
|
||||
return <div class="recent-sessions-content" key={sessions.length} >
|
||||
<popup>
|
||||
<menu class="context" id="remote-context">
|
||||
<li id="connect">{translate('Connect')}</li>
|
||||
<li id="transfer">{translate('Transfer File')}</li>
|
||||
<li id="tunnel">{translate('TCP Tunneling')}</li>
|
||||
<li id="rdp">RDP</li>
|
||||
<div class="separator" />
|
||||
<li id="rename">{translate('Rename')}</li>
|
||||
{this.type != "fav" && <li id="remove">{translate('Remove')}</li>}
|
||||
{is_win && <li di="shortcut">{translate('Create Desktop Shortcut')}</li>}
|
||||
<li id="forget-password">{translate('Unremember Password')}</li>
|
||||
{(!this.type || this.type == "fav") && <li id="add-fav">{translate('Add to Favorites')}</li>}
|
||||
{(!this.type || this.type == "fav") && <li id="remove-fav">{translate('Remove from Favorites')}</li>}
|
||||
</menu>
|
||||
</popup>
|
||||
{sessions}
|
||||
</div >;
|
||||
}
|
||||
|
||||
getSession(s) {
|
||||
let id = s[0] || s.id || "";
|
||||
let username = s[1] || s.username || "";
|
||||
let hostname = s[2] || s.hostname || "";
|
||||
let platform = s[3] || s.platform || "";
|
||||
let alias = s[4] || s.alias || "";
|
||||
if (this.style == "list") {
|
||||
return (<div class="remote-session-link remote-session-list" id={id} platform={platform} title={alias ? "ID: " + id : ""} >
|
||||
<div class="platform" style={"background:" + string2RGB(id + platform, 0.5)}>
|
||||
{platform && platformSvg(platform, "white")}
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>
|
||||
<div id="alias" class="ellipsis">{alias ? alias : formatId(id)}</div>
|
||||
<div class="username ellipsis">{username}@{hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{svg_menu}
|
||||
</div>
|
||||
</div >);
|
||||
}
|
||||
return (<div class="remote-session-link remote-session" id={id} platform={platform} title={alias ? "ID: " + id : ""} style={"background:" + string2RGB(id + platform, 0.5)} >
|
||||
<div class="platform">
|
||||
{platform && platformSvg(platform, "white")}
|
||||
<div class="username ellipsis">{username}@{hostname}</div>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div id="alias" class="ellipsis">{alias ? alias : formatId(id)}</div>
|
||||
{svg_menu}
|
||||
</div >
|
||||
</div >);
|
||||
}
|
||||
|
||||
["on doubleclick at div.remote-session-link"](evt, me) {
|
||||
createNewConnect(me.id, "connect");
|
||||
}
|
||||
|
||||
["on click at #menu"](_, me) {
|
||||
let id = me.parentElement.parentElement.id;
|
||||
let platform = me.parentElement.parentElement.getAttribute("platform");
|
||||
this.$("#rdp").style.setProperty(
|
||||
"display", (platform == "Windows" && is_win) ? "block" : "none",
|
||||
);
|
||||
this.$("#forget-password").style.setProperty(
|
||||
"display", handler.xcall("peer_has_password", id) ? "block" : "none",
|
||||
);
|
||||
if (!this.type || this.type == "fav") {
|
||||
let in_fav = handler.xcall("get_fav").indexOf(id) >= 0;
|
||||
let el = this.$("add-fav");
|
||||
if (el) el.style.setProperty(
|
||||
"display", in_fav ? "none" : "block",
|
||||
);
|
||||
el = this.$("remove-fav");
|
||||
if (el) el.style.setProperty(
|
||||
"display", in_fav ? "block" : "none",
|
||||
);
|
||||
}
|
||||
// https://sciter.com/forums/topic/replacecustomize-context-menu/
|
||||
let menu = this.$("menu#remote-context");
|
||||
menu.setAttribute("remote-id",id);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
["on click at menu#remote-context li"](evt, me) {
|
||||
let action = me.id;
|
||||
let id = me.parentElement.getAttribute("remote-id");
|
||||
if (action == "connect") {
|
||||
createNewConnect(id, "connect");
|
||||
} else if (action == "transfer") {
|
||||
createNewConnect(id, "file-transfer");
|
||||
} else if (action == "remove") {
|
||||
if (!this.type) {
|
||||
handler.xcall("remove_peer", id);
|
||||
app.componentUpdate();
|
||||
}
|
||||
} else if (action == "forget-password") {
|
||||
handler.forget_password(id);
|
||||
} else if (action == "shortcut") {
|
||||
handler.xcall("create_shortcut", id);
|
||||
} else if (action == "rdp") {
|
||||
createNewConnect(id, "rdp");
|
||||
} else if (action == "add-fav") {
|
||||
var favs = handler.get_fav();
|
||||
if (favs.indexOf(id) < 0) {
|
||||
favs = [id].concat(favs);
|
||||
handler.store_fav(favs);
|
||||
}
|
||||
app.multipleSessions.update();
|
||||
app.update();
|
||||
} else if (action == "remove-fav") {
|
||||
var favs = handler.get_fav();
|
||||
var i = favs.indexOf(id);
|
||||
favs.splice(i, 1);
|
||||
handler.store_fav(favs);
|
||||
app.multipleSessions.update();
|
||||
} else if (action == "tunnel") {
|
||||
createNewConnect(id, "port-forward");
|
||||
} else if (action == "rename") {
|
||||
let old_name = handler.xcall("get_peer_option", id, "alias");
|
||||
msgbox("custom-rename", "Rename", "<div class='form'> \
|
||||
<div><input name='name' class='outline-focus' style='width: *; height: 23px', value='" + old_name + "' /></div> \
|
||||
</div> \
|
||||
",
|
||||
function (res = null) {
|
||||
if (!res) return;
|
||||
let name = (res.name || "").trim();
|
||||
if (name != old_name) {
|
||||
handler.xcall("set_peer_option", id, "alias", name);
|
||||
}
|
||||
app.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionsType() {
|
||||
return handler.xcall("get_local_option", "show-sessions-type");
|
||||
}
|
||||
|
||||
class Favorites extends Element {
|
||||
render() {
|
||||
var sessions = handler.xcall("get_fav").map(function(f) {
|
||||
return handler.xcall("get_peer", f);
|
||||
});
|
||||
return <SessionList sessions={sessions} type="fav" />;
|
||||
}
|
||||
}
|
||||
|
||||
export class MultipleSessions extends Element {
|
||||
render() {
|
||||
var type = getSessionsType();
|
||||
return <div style="size: *">
|
||||
<div class="sessions-bar">
|
||||
<div style="width:*" class="sessions-tab" id="sessions-type">
|
||||
<span class={!type ? 'active' : 'inactive'}>{translate('Recent Sessions')}</span>
|
||||
<span id="fav" class={type == "fav" ? 'active' : 'inactive'}>{translate('Favorites')}</span>
|
||||
</div>
|
||||
{!this.hidden && <SearchBar type={type} />}
|
||||
{!this.hidden && <SessionStyle type={type} />}
|
||||
</div>
|
||||
{!this.hidden &&
|
||||
((type == "fav" && <Favorites />) ||
|
||||
<SessionList sessions={handler.xcall("get_recent_sessions")} />)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
["on click at div#sessions-type span.inactive"] (_, el) {
|
||||
handler.xcall("set_option", 'show-sessions-type', el.id || "");
|
||||
this.componentUpdate();
|
||||
}
|
||||
|
||||
onSize() {
|
||||
let w = this.$(".sessions-bar").state.box("width") - 220;
|
||||
this.$("#sessions-type span").style.setProperty(
|
||||
"max-width", (w / 2) + "px",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
document.onsizechange = () => { if (app && app.multipleSessions) app.multipleSessions.onSize(); }
|
||||
@@ -4,11 +4,11 @@ body {
|
||||
|
||||
div.content {
|
||||
flow: horizontal;
|
||||
size: *;
|
||||
size: "*";
|
||||
}
|
||||
|
||||
div.left-panel {
|
||||
size: *;
|
||||
size: "*";
|
||||
padding: 1em;
|
||||
border-spacing: 1em;
|
||||
overflow-x: scroll-indicator;
|
||||
@@ -38,7 +38,7 @@ div.chaticon:active {
|
||||
div.right-panel {
|
||||
background: white;
|
||||
border-left: color(border) 1px solid;
|
||||
size: *;
|
||||
size: "*";
|
||||
}
|
||||
|
||||
div.icon-and-id {
|
||||
@@ -71,7 +71,7 @@ div.permissions > div {
|
||||
}
|
||||
|
||||
div.permissions icon {
|
||||
margin: *;
|
||||
margin: "*";
|
||||
size: 32px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
@@ -99,7 +99,7 @@ icon.audio {
|
||||
}
|
||||
|
||||
div.buttons {
|
||||
width: *;
|
||||
width: "*";
|
||||
border-spacing: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -120,14 +120,14 @@ button#disconnect:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
@media not_osx {
|
||||
header .window-toolbar {
|
||||
left: 40px;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media platform == "OSX" {
|
||||
@media is_osx {
|
||||
header .tabs-wrapper {
|
||||
margin-left: 80px;
|
||||
margin-top: 8px;
|
||||
@@ -135,13 +135,13 @@ header .tabs-wrapper {
|
||||
}
|
||||
|
||||
div.tabs-wrapper {
|
||||
size: *;
|
||||
size: "*";
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
size: *;
|
||||
size: "*";
|
||||
flow: horizontal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -156,7 +156,7 @@ div.border-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: *;
|
||||
width: "*";
|
||||
height: 1px;
|
||||
background: color(border) 1px solid;
|
||||
}
|
||||
@@ -213,7 +213,7 @@ div.tab-arrows {
|
||||
|
||||
div.tab-arrows span {
|
||||
display: inline-block;
|
||||
height: *;
|
||||
height: "*";
|
||||
margin: 0;
|
||||
padding: 6px 2px;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
@import url(cm.css);
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "cm.tis";
|
||||
</script>
|
||||
</style><link rel="stylesheet" href="common.css">
|
||||
</style><link rel="stylesheet" href="cm.css">
|
||||
<script type="module" src="cm.js"></script>
|
||||
</head>
|
||||
<header>
|
||||
<div.window-icon role="window-icon"><icon /></div>
|
||||
<div class="window-icon" role="window-icon"><icon /></div>
|
||||
<caption role="window-caption" />
|
||||
<div.border-bottom />
|
||||
<div.window-toolbar />
|
||||
<div.window-buttons />
|
||||
<div class="border-bottom" />
|
||||
<div class="window-toolbar" />
|
||||
<div class="window-buttons" />
|
||||
</header>
|
||||
<body #handler>
|
||||
</body>
|
||||
|
||||
383
src/ui/cm.js
Normal file
383
src/ui/cm.js
Normal file
@@ -0,0 +1,383 @@
|
||||
import { handler,view,is_osx,string2RGB,adjustBorder,svg_chat,translate,ChatBox,getNowStr,setWindowButontsAndIcon,is_linux } from "./common.js";
|
||||
import {$} from "@sciter";
|
||||
// TODO in sciterjs window-frame
|
||||
// view.windowFrame = is_osx ? #extended : #solid;
|
||||
|
||||
var body;
|
||||
var connections = [];
|
||||
var show_chat = false;
|
||||
|
||||
class Body extends Element {
|
||||
cur = 0;
|
||||
|
||||
this() {
|
||||
body = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (connections.length == 0) return <div />;
|
||||
let c = connections[this.cur];
|
||||
this.connection = c;
|
||||
this.cid = c.id;
|
||||
let auth = c.authorized;
|
||||
let callback = (msg)=> {
|
||||
this.sendMsg(msg);
|
||||
};
|
||||
setTimeout(adaptSize, 1);
|
||||
let right_style = show_chat ? "" : "display: none";
|
||||
return (<div class="content">
|
||||
<div class="left-panel">
|
||||
<div class="icon-and-id">
|
||||
<div class="icon" style={"background: " + string2RGB(c.name, 1)}>
|
||||
{c.name[0].toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div class="id" style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
|
||||
<div class="id">({c.peer_id})</div>
|
||||
<div style="margin-top: 1.2em">{translate('Connected')} {" "} <span id="time">{getElaspsed(c.time)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div>{translate('Permissions')}</div>}
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div class="permissions">
|
||||
<div class={!c.keyboard ? "disabled" : ""} title={translate('Allow using keyboard and mouse')}><icon class="keyboard" /></div>
|
||||
<div class={!c.clipboard ? "disabled" : ""} title={translate('Allow using clipboard')}><icon class="clipboard" /></div>
|
||||
<div class={!c.audio ? "disabled" : ""} title={translate('Allow hearing sound')}><icon class="audio" /></div>
|
||||
</div>}
|
||||
{c.port_forward ? <div>Port Forwarding: {c.port_forward}</div> : ""}
|
||||
<div style="size:*"/>
|
||||
<div class="buttons">
|
||||
{auth ? "" : <button class="button" tabindex="-1" id="accept">{translate('Accept')}</button>}
|
||||
{auth ? "" : <button class="button" tabindex="-1" class="outline" id="dismiss">{translate('Dismiss')}</button>}
|
||||
{auth ? <button class="button" tabindex="-1" id="disconnect">{translate('Disconnect')}</button> : ""}
|
||||
</div>
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div class="chaticon">{svg_chat}</div>}
|
||||
</div>
|
||||
<div class="right-panel" style={right_style}>
|
||||
{c.is_file_transfer || c.port_forward ? "" : <ChatBox msgs={c.msgs} callback={callback} />}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
sendMsg(text) {
|
||||
if (!text) return;
|
||||
let { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.msgs.push({ name: "me", text: text, time: getNowStr()});
|
||||
handler.xcall("send_msg",cid, text);
|
||||
body.componentUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
["on click at icon.keyboard"](e) {
|
||||
let { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.keyboard = !connection.keyboard;
|
||||
body.componentUpdate();
|
||||
handler.xcall("switch_permission",cid, "keyboard", connection.keyboard);
|
||||
});
|
||||
}
|
||||
|
||||
["on click at icon.clipboard"]() {
|
||||
let { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.clipboard = !connection.clipboard;
|
||||
body.componentUpdate();
|
||||
handler.xcall("switch_permission",cid, "clipboard", connection.clipboard);
|
||||
});
|
||||
}
|
||||
|
||||
["on click at icon.audio"]() {
|
||||
let { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.audio = !connection.audio;
|
||||
body.componentUpdate();
|
||||
handler.xcall("switch_permission",cid, "audio", connection.audio);
|
||||
});
|
||||
}
|
||||
|
||||
["on click at button#accept"]() {
|
||||
let { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.authorized = true;
|
||||
body.componentUpdate();
|
||||
handler.xcall("authorize",cid);
|
||||
setTimeout(()=>view.state = Window.WINDOW_MINIMIZED,30);
|
||||
});
|
||||
}
|
||||
|
||||
["on click at button#dismiss"]() {
|
||||
let cid = this.cid;
|
||||
checkClickTime(function() {
|
||||
handler.close(cid); // TEST
|
||||
});
|
||||
}
|
||||
|
||||
["on click at button#disconnect"]() {
|
||||
let cid = this.cid;
|
||||
checkClickTime(function() {
|
||||
handler.close(cid); // TEST
|
||||
});
|
||||
}
|
||||
["on click at div.chaticon"]() {
|
||||
checkClickTime(function() {
|
||||
show_chat = !show_chat;
|
||||
adaptSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$("body").content(<Body />);
|
||||
|
||||
var header;
|
||||
|
||||
class Header extends Element {
|
||||
this() {
|
||||
header = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
let me = this;
|
||||
let conn = connections[body.cur];
|
||||
if (conn && conn.unreaded > 0) {
|
||||
let el = this.select("#unreaded" + conn.id); // TODO select
|
||||
if (el) el.style.setProperty("display","inline-block");
|
||||
setTimeout(function() {
|
||||
conn.unreaded = 0;
|
||||
let el = this.select("#unreaded" + conn.id); // TODO
|
||||
if (el) el.style.setProperty("display","none");
|
||||
},300);
|
||||
}
|
||||
let tabs = connections.map((c, i)=> this.renderTab(c, i));
|
||||
return (<div class="tabs-wrapper"><div class="tabs">
|
||||
{tabs}
|
||||
</div>
|
||||
<div class="tab-arrows">
|
||||
<span class="left-arrow"><</span>
|
||||
<span class="right-arrow">></span>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderTab(c, i) {
|
||||
let cur = body.cur;
|
||||
return (<div class={i == cur ? "active-tab tab" : "tab"}>
|
||||
{c.name}
|
||||
{c.unreaded > 0 ? <span class="unreaded" id={"unreaded" + c.id}>{c.unreaded}</span> : ""}
|
||||
</div>);
|
||||
}
|
||||
|
||||
update_cur(idx) {
|
||||
checkClickTime(function(){
|
||||
body.cur = idx;
|
||||
update();
|
||||
setTimeout(adjustHeader,1);
|
||||
});
|
||||
}
|
||||
|
||||
["on click at div.tab"] (_, me) {
|
||||
let idx = me.index;
|
||||
if (idx == body.cur) return;
|
||||
this.update_cur(idx);
|
||||
}
|
||||
|
||||
["on click at span.left-arrow"]() {
|
||||
let cur = body.cur;
|
||||
if (cur == 0) return;
|
||||
this.update_cur(cur - 1);
|
||||
}
|
||||
|
||||
["on click at span.right-arrow"]() {
|
||||
let cur = body.cur;
|
||||
if (cur == connections.length - 1) return;
|
||||
this.update_cur(cur + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_osx) {
|
||||
$("header").content(<Header />);
|
||||
$("header").attributes["role"] = "window-caption"; // TODO
|
||||
} else {
|
||||
$("div.window-toolbar").content(<Header />);
|
||||
setWindowButontsAndIcon(true);
|
||||
}
|
||||
|
||||
function checkClickTime(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function adaptSize() {
|
||||
$("div.right-panel").style.setProperty("display",show_chat ? "block" : "none");
|
||||
let el = $("div.chaticon");
|
||||
if (el) el.classList.toggle("active", show_chat);
|
||||
let [x, y, w, h] = view.state.box("rectw", "border", "screen");
|
||||
if (show_chat && w < 600) {
|
||||
view.move(x - (600 - w), y, 600, h);
|
||||
} else if (!show_chat && w > 450) {
|
||||
view.move(x + (w - 300), y, 300, h);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
header.componentUpdate();
|
||||
body.componentUpdate();
|
||||
}
|
||||
|
||||
function bring_to_top(idx=-1) {
|
||||
if (view.state == Window.WINDOW_HIDDEN || view.state == Window.WINDOW_MINIMIZED) {
|
||||
if (is_linux) {
|
||||
view.focus = $("body");
|
||||
} else {
|
||||
view.state = Window.WINDOW_SHOWN;
|
||||
}
|
||||
if (idx >= 0) body.cur = idx;
|
||||
} else {
|
||||
view.isTopmost = true; // TEST
|
||||
view.isTopmost = false; // TEST
|
||||
}
|
||||
}
|
||||
|
||||
handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio) {
|
||||
let conn;
|
||||
connections.map(function(c) {
|
||||
if (c.id == id) conn = c;
|
||||
});
|
||||
if (conn) {
|
||||
conn.authorized = authorized;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (!name) name = "NA";
|
||||
connections.push({
|
||||
id: id, is_file_transfer: is_file_transfer, peer_id: peer_id,
|
||||
port_forward: port_forward,
|
||||
name: name, authorized: authorized, time: new Date(),
|
||||
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
|
||||
audio: audio,
|
||||
});
|
||||
body.cur = connections.length - 1;
|
||||
bring_to_top();
|
||||
update();
|
||||
setTimeout(adjustHeader,1);
|
||||
if (authorized) {
|
||||
setTimeout(()=>view.state = Window.WINDOW_MINIMIZED,3000);
|
||||
}
|
||||
}
|
||||
|
||||
handler.removeConnection = function(id) {
|
||||
let i = -1;
|
||||
connections.map(function(c, idx) {
|
||||
if (c.id == id) i = idx;
|
||||
});
|
||||
connections.splice(i, 1);
|
||||
if (connections.length == 0) {
|
||||
handler.xcall("exit");
|
||||
} else {
|
||||
if (body.cur >= i && body.cur > 0) body.cur -= 1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
handler.newMessage = function(id, text) {
|
||||
let idx = -1;
|
||||
connections.map(function(c, i) {
|
||||
if (c.id == id) idx = i;
|
||||
});
|
||||
let conn = connections[idx];
|
||||
if (!conn) return;
|
||||
conn.msgs.push({name: conn.name, text: text, time: getNowStr()});
|
||||
bring_to_top(idx);
|
||||
if (idx == body.cur) show_chat = true;
|
||||
conn.unreaded += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
handler.awake = function() {
|
||||
view.state = Window.WINDOW_SHOWN;
|
||||
view.focus = $("body");
|
||||
}
|
||||
|
||||
// TEST
|
||||
// view << event statechange {
|
||||
// adjustBorder();
|
||||
// }
|
||||
view.on("statechange",()=>{
|
||||
adjustBorder();
|
||||
})
|
||||
|
||||
document.on("ready",()=>{
|
||||
adjustBorder();
|
||||
let [sw, sh] = view.screenBox("workarea", "dimension");
|
||||
let w = 300;
|
||||
let h = 400;
|
||||
view.move(sw - w, 0, w, h);
|
||||
})
|
||||
|
||||
document.on("unloadequest",(evt)=>{
|
||||
view.state = Window.WINDOW_HIDDEN;
|
||||
console.log("cm unloadequest")
|
||||
evt.preventDefault(); // prevent unloading TEST
|
||||
})
|
||||
|
||||
function getElaspsed(time) {
|
||||
// let now = new Date();
|
||||
// let seconds = Date.diff(time, now, #seconds);
|
||||
// let hours = seconds / 3600;
|
||||
// let days = hours / 24;
|
||||
// hours = hours % 24;
|
||||
// let minutes = seconds % 3600 / 60;
|
||||
// seconds = seconds % 60;
|
||||
// let out = String.printf("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
// if (days > 0) {
|
||||
// out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out);
|
||||
// }
|
||||
let out = "TIME TODO" + new Date(); // TODO
|
||||
return out;
|
||||
}
|
||||
|
||||
// updateTime
|
||||
setInterval(function() {
|
||||
let el = $("#time");
|
||||
if (el) {
|
||||
let c = connections[body.cur];
|
||||
if (c) {
|
||||
el.text = getElaspsed(c.time);
|
||||
}
|
||||
}
|
||||
},1000);
|
||||
|
||||
|
||||
function adjustHeader() {
|
||||
let hw = $("header").state.box("width");
|
||||
let tabswrapper = $("div.tabs-wrapper");
|
||||
let tabs = $("div.tabs");
|
||||
let arrows = $("div.tab-arrows");
|
||||
if (!arrows) return;
|
||||
let n = connections.length;
|
||||
let wtab = 80;
|
||||
let max = hw - 98;
|
||||
let need_width = n * wtab + 2; // include border of active tab
|
||||
if (need_width < max) {
|
||||
arrows.style.setProperty("display","none");
|
||||
tabs.style.setProperty("width",need_width);
|
||||
tabs.style.setProperty("margin-left",0);
|
||||
tabswrapper.style.setProperty("width",need_width);
|
||||
} else {
|
||||
let margin = (body.cur + 1) * wtab - max + 30;
|
||||
if (margin < 0) margin = 0;
|
||||
arrows.style.setProperty("display","block");
|
||||
tabs.style.setProperty("width",(max - 20 + margin) + 'px');
|
||||
tabs.style.setProperty("margin-left",-margin + 'px');
|
||||
tabswrapper.style.setProperty("width",(max + 10) + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
document.onsizechange = ()=>{
|
||||
console.log("cm onsizechange");
|
||||
adjustHeader();
|
||||
}
|
||||
|
||||
// handler.addConnection(0, false, 0, "", "test1", true, false, false, false);
|
||||
// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false);
|
||||
// handler.addConnection(2, false, 0, "", "test3", true, false, false, false);
|
||||
// handler.newMessage(0, 'h');
|
||||
34
src/ui/cm.rs
34
src/ui/cm.rs
@@ -91,13 +91,13 @@ impl ConnectionManager {
|
||||
clipboard,
|
||||
audio
|
||||
),
|
||||
);
|
||||
); // TODO
|
||||
self.write().unwrap().senders.insert(id, tx);
|
||||
}
|
||||
|
||||
fn remove_connection(&self, id: i32) {
|
||||
self.write().unwrap().senders.remove(&id);
|
||||
self.call("removeConnection", &make_args!(id));
|
||||
self.call("removeConnection", &make_args!(id)); // TODO
|
||||
}
|
||||
|
||||
async fn handle_data(
|
||||
@@ -300,6 +300,10 @@ impl ConnectionManager {
|
||||
fn exit(&self) {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn t(&self, name: String) -> String {
|
||||
crate::client::translate(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl sciter::EventHandler for ConnectionManager {
|
||||
@@ -308,6 +312,7 @@ impl sciter::EventHandler for ConnectionManager {
|
||||
}
|
||||
|
||||
sciter::dispatch_script_call! {
|
||||
fn t(String);
|
||||
fn get_icon();
|
||||
fn close(i32);
|
||||
fn authorize(i32);
|
||||
@@ -405,12 +410,14 @@ async fn start_pa() {
|
||||
break;
|
||||
}
|
||||
let spec = pulse::sample::Spec {
|
||||
format: pulse::sample::Format::F32be,
|
||||
format: pulse::sample::Format::F32le,
|
||||
channels: 2,
|
||||
rate: crate::platform::linux::PA_SAMPLE_RATE,
|
||||
};
|
||||
log::info!("pa monitor: {:?}", device);
|
||||
if let Ok(s) = psimple::Simple::new(
|
||||
// systemctl --user status pulseaudio.service
|
||||
let mut buf: Vec<u8> = vec![0; 480 * 4];
|
||||
match psimple::Simple::new(
|
||||
None, // Use the default server
|
||||
APP_NAME, // Our application’s name
|
||||
pulse::stream::Direction::Record, // We want a record stream
|
||||
@@ -420,22 +427,19 @@ async fn start_pa() {
|
||||
None, // Use default channel map
|
||||
None, // Use default buffering attributes
|
||||
) {
|
||||
loop {
|
||||
Ok(s) => loop {
|
||||
if let Some(Err(_)) = stream.next_timeout2(1).await {
|
||||
break;
|
||||
}
|
||||
let mut out: Vec<u8> = Vec::with_capacity(480 * 4);
|
||||
unsafe {
|
||||
out.set_len(out.capacity());
|
||||
}
|
||||
if let Ok(_) = s.read(&mut out) {
|
||||
if out.iter().filter(|x| **x != 0).next().is_some() {
|
||||
allow_err!(stream.send(&Data::RawMessage(out)).await);
|
||||
}
|
||||
if let Ok(_) = s.read(&mut buf) {
|
||||
allow_err!(
|
||||
stream.send(&Data::RawMessage(buf.clone())).await
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Could not create simple pulse: {}", err);
|
||||
}
|
||||
} else {
|
||||
log::error!("Could not create simple pulse");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
398
src/ui/cm.tis
398
src/ui/cm.tis
@@ -1,398 +0,0 @@
|
||||
view.windowFrame = is_osx ? #extended : #solid;
|
||||
|
||||
var body;
|
||||
var connections = [];
|
||||
var show_chat = false;
|
||||
|
||||
class Body: Reactor.Component
|
||||
{
|
||||
this var cur = 0;
|
||||
|
||||
function this() {
|
||||
body = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (connections.length == 0) return <div />;
|
||||
var c = connections[this.cur];
|
||||
this.connection = c;
|
||||
this.cid = c.id;
|
||||
var auth = c.authorized;
|
||||
var me = this;
|
||||
var callback = function(msg) {
|
||||
me.sendMsg(msg);
|
||||
};
|
||||
self.timer(1ms, adaptSize);
|
||||
var right_style = show_chat ? "" : "display: none";
|
||||
return <div .content>
|
||||
<div .left-panel>
|
||||
<div .icon-and-id>
|
||||
<div .icon style={"background: " + string2RGB(c.name, 1)}>
|
||||
{c.name[0].toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
|
||||
<div .id>({c.peer_id})</div>
|
||||
<div style="margin-top: 1.2em">Connected <span #time>{getElaspsed(c.time)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div>Permissions</div>}
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div .permissions>
|
||||
<div class={!c.keyboard ? "disabled" : ""} title="Allow using keyboard and mouse"><icon .keyboard /></div>
|
||||
<div class={!c.clipboard ? "disabled" : ""} title="Allow using clipboard"><icon .clipboard /></div>
|
||||
<div class={!c.audio ? "disabled" : ""} title="Allow hearing sound"><icon .audio /></div>
|
||||
</div>}
|
||||
{c.port_forward ? <div>Port Forwarding: {c.port_forward}</div> : ""}
|
||||
<div style="size:*"/>
|
||||
<div .buttons>
|
||||
{auth ? "" : <button .button tabindex="-1" #accept>Accept</button>}
|
||||
{auth ? "" : <button .button tabindex="-1" .outline #dismiss>Dismiss</button>}
|
||||
{auth ? <button .button tabindex="-1" #disconnect>Disconnect</button> : ""}
|
||||
</div>
|
||||
{c.is_file_transfer || c.port_forward ? "" : <div .chaticon>{svg_chat}</div>}
|
||||
</div>
|
||||
<div .right-panel style={right_style}>
|
||||
{c.is_file_transfer || c.port_forward ? "" : <ChatBox msgs={c.msgs} callback={callback} />}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function sendMsg(text) {
|
||||
if (!text) return;
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.msgs.push({ name: "me", text: text, time: getNowStr()});
|
||||
handler.send_msg(cid, text);
|
||||
body.update();
|
||||
});
|
||||
}
|
||||
|
||||
event click $(icon.keyboard) (e) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.keyboard = !connection.keyboard;
|
||||
body.update();
|
||||
handler.switch_permission(cid, "keyboard", connection.keyboard);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(icon.clipboard) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.clipboard = !connection.clipboard;
|
||||
body.update();
|
||||
handler.switch_permission(cid, "clipboard", connection.clipboard);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(icon.audio) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.audio = !connection.audio;
|
||||
body.update();
|
||||
handler.switch_permission(cid, "audio", connection.audio);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(button#accept) {
|
||||
var { cid, connection } = this;
|
||||
checkClickTime(function() {
|
||||
connection.authorized = true;
|
||||
body.update();
|
||||
handler.authorize(cid);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(button#dismiss) {
|
||||
var cid = this.cid;
|
||||
checkClickTime(function() {
|
||||
handler.close(cid);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(button#disconnect) {
|
||||
var cid = this.cid;
|
||||
checkClickTime(function() {
|
||||
handler.close(cid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<Body />);
|
||||
|
||||
var header;
|
||||
|
||||
class Header: Reactor.Component
|
||||
{
|
||||
function this() {
|
||||
header = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
var conn = connections[body.cur];
|
||||
if (conn && conn.unreaded > 0) {;
|
||||
var el = me.select("#unreaded" + conn.id);
|
||||
if (el) el.style.set {
|
||||
display: "inline-block",
|
||||
};
|
||||
self.timer(300ms, function() {
|
||||
conn.unreaded = 0;
|
||||
var el = me.select("#unreaded" + conn.id);
|
||||
if (el) el.style.set {
|
||||
display: "none",
|
||||
};
|
||||
});
|
||||
}
|
||||
var tabs = connections.map(function(c, i) { return me.renderTab(c, i) });
|
||||
return <div .tabs-wrapper><div .tabs>
|
||||
{tabs}
|
||||
</div>
|
||||
<div .tab-arrows>
|
||||
<span .left-arrow><</span>
|
||||
<span .right-arrow>></span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderTab(c, i) {
|
||||
var cur = body.cur;
|
||||
return <div class={i == cur ? "active-tab tab" : "tab"}>
|
||||
{c.name}
|
||||
{c.unreaded > 0 ? <span .unreaded id={"unreaded" + c.id}>{c.unreaded}</span> : ""}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function update_cur(idx) {
|
||||
checkClickTime(function() {
|
||||
body.cur = idx;
|
||||
update();
|
||||
self.timer(1ms, adjustHeader);
|
||||
});
|
||||
}
|
||||
|
||||
event click $(div.tab) (_, me) {
|
||||
var idx = me.index;
|
||||
if (idx == body.cur) return;
|
||||
this.update_cur(idx);
|
||||
}
|
||||
|
||||
event click $(span.left-arrow) {
|
||||
var cur = body.cur;
|
||||
if (cur == 0) return;
|
||||
this.update_cur(cur - 1);
|
||||
}
|
||||
|
||||
event click $(span.right-arrow) {
|
||||
var cur = body.cur;
|
||||
if (cur == connections.length - 1) return;
|
||||
this.update_cur(cur + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_osx) {
|
||||
$(header).content(<Header />);
|
||||
$(header).attributes["role"] = "window-caption";
|
||||
} else {
|
||||
$(div.window-toolbar).content(<Header />);
|
||||
setWindowButontsAndIcon(true);
|
||||
}
|
||||
|
||||
event click $(div.chaticon) {
|
||||
checkClickTime(function() {
|
||||
show_chat = !show_chat;
|
||||
adaptSize();
|
||||
});
|
||||
}
|
||||
|
||||
function checkClickTime(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function adaptSize() {
|
||||
$(div.right-panel).style.set {
|
||||
display: show_chat ? "block" : "none",
|
||||
};
|
||||
var el = $(div.chaticon);
|
||||
if (el) el.attributes.toggleClass("active", show_chat);
|
||||
var (x, y, w, h) = view.box(#rectw, #border, #screen);
|
||||
if (show_chat && w < 600) {
|
||||
view.move(x - (600 - w), y, 600, h);
|
||||
} else if (!show_chat && w > 450) {
|
||||
view.move(x + (w - 300), y, 300, h);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
header.update();
|
||||
body.update();
|
||||
}
|
||||
|
||||
function bring_to_top(idx=-1) {
|
||||
if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) {
|
||||
if (is_linux) {
|
||||
view.focus = self;
|
||||
} else {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
}
|
||||
if (idx >= 0) body.cur = idx;
|
||||
} else {
|
||||
view.windowTopmost = true;
|
||||
view.windowTopmost = false;
|
||||
}
|
||||
}
|
||||
|
||||
handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio) {
|
||||
var conn;
|
||||
connections.map(function(c) {
|
||||
if (c.id == id) conn = c;
|
||||
});
|
||||
if (conn) {
|
||||
conn.authorized = authorized;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (!name) name = "NA";
|
||||
connections.push({
|
||||
id: id, is_file_transfer: is_file_transfer, peer_id: peer_id,
|
||||
port_forward: port_forward,
|
||||
name: name, authorized: authorized, time: new Date(),
|
||||
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
|
||||
audio: audio,
|
||||
});
|
||||
body.cur = connections.length - 1;
|
||||
bring_to_top();
|
||||
update();
|
||||
self.timer(1ms, adjustHeader);
|
||||
if (authorized) {
|
||||
self.timer(3s, function() {
|
||||
view.windowState = View.WINDOW_MINIMIZED;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handler.removeConnection = function(id) {
|
||||
var i = -1;
|
||||
connections.map(function(c, idx) {
|
||||
if (c.id == id) i = idx;
|
||||
});
|
||||
connections.splice(i, 1);
|
||||
if (connections.length == 0) {
|
||||
handler.exit();
|
||||
} else {
|
||||
if (body.cur >= i && body.cur > 0) body.cur -= 1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
handler.newMessage = function(id, text) {
|
||||
var idx = -1;
|
||||
connections.map(function(c, i) {
|
||||
if (c.id == id) idx = i;
|
||||
});
|
||||
var conn = connections[idx];
|
||||
if (!conn) return;
|
||||
conn.msgs.push({name: conn.name, text: text, time: getNowStr()});
|
||||
bring_to_top(idx);
|
||||
if (idx == body.cur) show_chat = true;
|
||||
conn.unreaded += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
handler.awake = function() {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
view.focus = self;
|
||||
}
|
||||
|
||||
view << event statechange {
|
||||
adjustBorder();
|
||||
}
|
||||
|
||||
function self.ready() {
|
||||
adjustBorder();
|
||||
var (sw, sh) = view.screenBox(#workarea, #dimension);
|
||||
var w = 300;
|
||||
var h = 400;
|
||||
view.move(sw - w, 0, w, h);
|
||||
}
|
||||
|
||||
function getElaspsed(time) {
|
||||
var now = new Date();
|
||||
var seconds = Date.diff(time, now, #seconds);
|
||||
var hours = seconds / 3600;
|
||||
var days = hours / 24;
|
||||
hours = hours % 24;
|
||||
var minutes = seconds % 3600 / 60;
|
||||
seconds = seconds % 60;
|
||||
var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
if (days > 0) {
|
||||
out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
self.timer(1s, function() {
|
||||
var el = $(#time);
|
||||
if (el) {
|
||||
var c = connections[body.cur];
|
||||
if (c) {
|
||||
el.text = getElaspsed(c.time);
|
||||
}
|
||||
}
|
||||
updateTime();
|
||||
});
|
||||
}
|
||||
|
||||
updateTime();
|
||||
|
||||
function self.closing() {
|
||||
view.windowState = View.WINDOW_HIDDEN;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function adjustHeader() {
|
||||
var hw = $(header).box(#width);
|
||||
var tabswrapper = $(div.tabs-wrapper);
|
||||
var tabs = $(div.tabs);
|
||||
var arrows = $(div.tab-arrows);
|
||||
if (!arrows) return;
|
||||
var n = connections.length;
|
||||
var wtab = 80;
|
||||
var max = hw - 98;
|
||||
var need_width = n * wtab + 2; // include border of active tab
|
||||
if (need_width < max) {
|
||||
arrows.style.set {
|
||||
display: "none",
|
||||
};
|
||||
tabs.style.set {
|
||||
width: need_width,
|
||||
margin-left: 0,
|
||||
};
|
||||
tabswrapper.style.set {
|
||||
width: need_width,
|
||||
};
|
||||
} else {
|
||||
var margin = (body.cur + 1) * wtab - max + 30;
|
||||
if (margin < 0) margin = 0;
|
||||
arrows.style.set {
|
||||
display: "block",
|
||||
};
|
||||
tabs.style.set {
|
||||
width: (max - 20 + margin) + 'px',
|
||||
margin-left: -margin + 'px'
|
||||
};
|
||||
tabswrapper.style.set {
|
||||
width: (max + 10) + 'px',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
view.on("size", adjustHeader);
|
||||
|
||||
// handler.addConnection(0, false, 0, "", "test1", true, false, false, false);
|
||||
// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false);
|
||||
// handler.addConnection(2, false, 0, "", "test3", true, false, false, false);
|
||||
// handler.newMessage(0, 'h');
|
||||
@@ -4,6 +4,7 @@ html {
|
||||
var(gray-bg): #eee;
|
||||
var(bg): white;
|
||||
var(border): #ccc;
|
||||
var(hover-border): #999;
|
||||
var(text): #222;
|
||||
var(placeholder): #aaa;
|
||||
var(lighter-text): #888;
|
||||
@@ -52,6 +53,10 @@ button.button:active, button.active {
|
||||
border-color: color(accent);
|
||||
}
|
||||
|
||||
button.button:hover, button.outline:hover {
|
||||
border-color: color(hover-border);
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], input[type=number] {
|
||||
width: *;
|
||||
font-size: 1.5em;
|
||||
@@ -156,9 +161,7 @@ header caption {
|
||||
top: 0;
|
||||
padding: 0 10px;
|
||||
width: 22px;
|
||||
height: *;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
color: black;
|
||||
border: none;
|
||||
background: none;
|
||||
@@ -321,5 +324,5 @@ menu li.selected span {
|
||||
|
||||
menu li.line-through {
|
||||
text-decoration-line: line-through;
|
||||
color: grey;
|
||||
color: red;
|
||||
}
|
||||
|
||||
379
src/ui/common.js
Normal file
379
src/ui/common.js
Normal file
@@ -0,0 +1,379 @@
|
||||
|
||||
export const view = Window.this;
|
||||
export const handler = document.$("#handler") || view;
|
||||
|
||||
try { view.windowIcon = document.url(handler.xcall("get_icon")); } catch (e) { }
|
||||
|
||||
export const OS = view.mediaVar("platform");
|
||||
export const is_osx = OS == "OSX";
|
||||
export const is_win = OS == "Windows";
|
||||
export const is_linux = OS == "Linux";
|
||||
|
||||
view.mediaVar("is_osx", is_osx);
|
||||
view.mediaVar("not_osx", !is_osx);
|
||||
handler.is_port_forward = false;
|
||||
handler.is_file_transfer = false;
|
||||
export var is_xfce = false;
|
||||
try { is_xfce = handler.xcall("is_xfce"); } catch (e) { }
|
||||
|
||||
|
||||
export function translate(name) {
|
||||
try {
|
||||
return handler.xcall("t", name);
|
||||
} catch (_) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
export function hashCode(str) {
|
||||
let hash = 160 << 16 + 114 << 8 + 91;
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return hash % 16777216;
|
||||
}
|
||||
|
||||
export function intToRGB(i, a = 1) {
|
||||
return `rgba(${((i >> 16) & 0xFF)}, ${((i >> 8) & 0x7F)},${(i & 0xFF)},${a})`;
|
||||
}
|
||||
|
||||
export function string2RGB(s, a = 1) {
|
||||
return intToRGB(hashCode(s), a);
|
||||
}
|
||||
|
||||
export function getTime() {
|
||||
return new Date().valueOf();
|
||||
}
|
||||
|
||||
export function platformSvg(platform, color) {
|
||||
platform = (platform || "").toLowerCase();
|
||||
if (platform == "linux") {
|
||||
return (<svg viewBox="0 0 256 256">
|
||||
<g transform="translate(0 256) scale(.1 -.1)" fill={color}>
|
||||
<path d="m1215 2537c-140-37-242-135-286-278-23-75-23-131 1-383l18-200-54-60c-203-224-383-615-384-831v-51l-66-43c-113-75-194-199-194-300 0-110 99-234 244-305 103-50 185-69 296-69 100 0 156 14 211 54 26 18 35 19 78 10 86-18 233-24 335-12 85 10 222 38 269 56 9 4 19-7 29-35 20-50 52-64 136-57 98 8 180 52 282 156 124 125 180 244 180 380 0 80-28 142-79 179l-36 26 4 119c5 175-22 292-105 460-74 149-142 246-286 409-43 49-78 92-78 97 0 4-7 52-15 107-8 54-19 140-24 189-13 121-41 192-103 260-95 104-248 154-373 122zm172-112c62-19 134-80 163-140 15-31 28-92 41-193 27-214 38-276 57-304 9-14 59-74 111-134 92-106 191-246 236-334 69-137 115-339 101-451l-7-55-71 10c-100 13-234-5-265-36-54-55-85-207-82-412l1-141-51-17c-104-34-245-51-380-45-69 3-142 10-162 16-32 10-37 17-53 68-23 72-87 201-136 273-80 117-158 188-237 215-37 13-37 13-34 61 13 211 182 555 373 759 57 62 58 63 58 121 0 33-9 149-19 259-21 224-18 266 26 347 67 122 193 174 330 133zm687-1720c32-9 71-25 87-36 60-42 59-151-4-274-59-119-221-250-317-257-34-3-35-2-48 47-18 65-20 329-3 413 16 83 29 110 55 115 51 10 177 6 230-8zm-1418-80c79-46 187-195 247-340 41-99 43-121 12-141-39-25-148-30-238-10-142 32-264 112-307 202-20 41-21 50-10 87 24 83 102 166 192 207 54 25 53 25 104-5z" />
|
||||
<path d="m1395 1945c-92-16-220-52-256-70-28-15-29-18-29-89 0-247 165-397 345-312 60 28 77 46 106 111 54 123 0 378-80 374-9 0-47-7-86-14zm74-156c15-69 14-112-5-159s-55-70-111-70c-48 0-78 20-102 68-15 29-41 131-41 159 0 9 230 63 242 57 3-2 11-27 17-55z" />
|
||||
</g>
|
||||
</svg>);
|
||||
}
|
||||
if (platform == "mac os") {
|
||||
return (<svg viewBox="0 0 384 512">
|
||||
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" fill={color} />
|
||||
</svg>);
|
||||
}
|
||||
return (<svg viewBox="0 0 448 512">
|
||||
<path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" fill={color} />
|
||||
</svg>);
|
||||
}
|
||||
|
||||
export function centerize(w, h) {
|
||||
let [sx, sy, sw, sh] = view.screenBox("workarea", "rectw");
|
||||
if (w > sw) w = sw;
|
||||
if (h > sh) h = sh;
|
||||
const x = (sx + sw - w) / 2;
|
||||
const y = (sy + sh - h) / 2;
|
||||
view.move(x, y, w, h);
|
||||
}
|
||||
|
||||
// TODO CSS
|
||||
export function setWindowButontsAndIcon(only_min = false) {
|
||||
if (only_min) {
|
||||
document.$("div.window-buttons").content(
|
||||
<div>
|
||||
<button class="window minimize" id="button-window" tabindex="-1" role="window-minimize" ><div /></button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
document.$("div.window-buttons").content(<div>
|
||||
<button class="window minimize" tabindex="-1" role="window-minimize" ><div /></button>
|
||||
<button class="window maximize" tabindex="-1" role="window-maximize" ><div /></button>
|
||||
<button class="window close" tabindex="-1" role="window-close" ><div /></button>
|
||||
</div>);
|
||||
}
|
||||
document.$("div.window-icon>icon").style.setProperty(
|
||||
"background-image", `url(${handler.xcall("get_icon")})`,
|
||||
);
|
||||
}
|
||||
|
||||
export function adjustBorder() {
|
||||
if (is_osx) {
|
||||
let headerStyle = document.$("header").style;
|
||||
if (view.state == Window.WINDOW_FULL_SCREEN) {
|
||||
headerStyle.setProperty("display", "none",);
|
||||
} else {
|
||||
headerStyle.setProperty("display", "block",);
|
||||
headerStyle.setProperty("padding", "0",);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (view.state == Window.WINDOW_MAXIMIZED) {
|
||||
document.style.setProperty("border", "window-frame-width solid transparent");
|
||||
} else if (view.state == Window.WINDOW_FULL_SCREEN) {
|
||||
document.style.setProperty("border", "none");
|
||||
} else {
|
||||
document.style.setProperty("border", "black solid 1px");
|
||||
}
|
||||
let el = document.$("button#maximize");
|
||||
if (el) el.classList.toggle("restore", view.state == Window.WINDOW_MAXIMIZED);
|
||||
el = document.$("span#fullscreen");
|
||||
if (el) el.classList.toggle("active", view.state == Window.WINDOW_FULL_SCREEN);
|
||||
}
|
||||
|
||||
export const svg_checkmark = (<svg viewBox="0 0 492 492"><path d="M484 105l-16-17a27 27 0 00-38 0L204 315 62 173c-5-5-12-7-19-7s-14 2-19 7L8 189a27 27 0 000 38l160 160v1l16 16c5 5 12 8 19 8 8 0 14-3 20-8l16-16v-1l245-244a27 27 0 000-38z" /></svg>);
|
||||
export const svg_edit = (<svg id="edit" viewBox="0 0 384 384">
|
||||
<path d="M0 304v80h80l236-236-80-80zM378 56L328 6c-8-8-22-8-30 0l-39 39 80 80 39-39c8-8 8-22 0-30z" />
|
||||
</svg>);
|
||||
export const svg_eye = (<svg viewBox="0 0 469.33 469.33">
|
||||
<path d="m234.67 170.67c-35.307 0-64 28.693-64 64s28.693 64 64 64 64-28.693 64-64-28.694-64-64-64z" />
|
||||
<path d="m234.67 74.667c-106.67 0-197.76 66.346-234.67 160 36.907 93.653 128 160 234.67 160 106.77 0 197.76-66.347 234.67-160-36.907-93.654-127.89-160-234.67-160zm0 266.67c-58.88 0-106.67-47.787-106.67-106.67s47.787-106.67 106.67-106.67 106.67 47.787 106.67 106.67-47.787 106.67-106.67 106.67z" />
|
||||
</svg>);
|
||||
export const svg_send = (<svg viewBox="0 0 448 448">
|
||||
<polygon points="0.213 32 0 181.33 320 224 0 266.67 0.213 416 448 224" />
|
||||
</svg>);
|
||||
export const svg_chat = (<svg viewBox="0 0 511.07 511.07">
|
||||
<path d="m74.39 480.54h-36.213l25.607-25.607c13.807-13.807 22.429-31.765 24.747-51.246-36.029-23.644-62.375-54.751-76.478-90.425-14.093-35.647-15.864-74.888-5.121-113.48 12.89-46.309 43.123-88.518 85.128-118.85 45.646-32.963 102.47-50.387 164.33-50.387 77.927 0 143.61 22.389 189.95 64.745 41.744 38.159 64.734 89.63 64.734 144.93 0 26.868-5.471 53.011-16.26 77.703-11.165 25.551-27.514 48.302-48.593 67.619-46.399 42.523-112.04 65-189.83 65-28.877 0-59.01-3.855-85.913-10.929-25.465 26.123-59.972 40.929-96.086 40.929zm182-420c-124.04 0-200.15 73.973-220.56 147.28-19.284 69.28 9.143 134.74 76.043 175.12l7.475 4.511-0.23 8.727c-0.456 17.274-4.574 33.912-11.945 48.952 17.949-6.073 34.236-17.083 46.99-32.151l6.342-7.493 9.405 2.813c26.393 7.894 57.104 12.241 86.477 12.241 154.37 0 224.68-93.473 224.68-180.32 0-46.776-19.524-90.384-54.976-122.79-40.713-37.216-99.397-56.888-169.71-56.888z" />
|
||||
</svg>);
|
||||
|
||||
export function scrollToBottom(el) {
|
||||
// TEST .box()
|
||||
let y = el.state.box("height", "content") - el.state.box("height", "client");
|
||||
el.scrollTo(0, y);
|
||||
}
|
||||
|
||||
export function getNowStr() {
|
||||
let now = new Date();
|
||||
return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second);
|
||||
}
|
||||
|
||||
/******************** start of chatbox ****************************************/
|
||||
export class ChatBox extends Element {
|
||||
msgs = [];
|
||||
callback;
|
||||
|
||||
this(props) {
|
||||
if (props) {
|
||||
this.msgs = props.msgs || [];
|
||||
this.callback = props.callback;
|
||||
}
|
||||
}
|
||||
|
||||
renderMsg(msg) {
|
||||
let cls = msg.name == "me" ? "right-side msg" : "left-side msg";
|
||||
return (
|
||||
<div class={cls}>
|
||||
{msg.name == "me" ?
|
||||
<div class="time">{msg.time + " "} me</div> :
|
||||
<div class="name">{msg.name} {" " + msg.time}</div>
|
||||
}
|
||||
<div class="text">{msg.text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let msgs = this.msgs.map((msg) => this.renderMsg(msg));
|
||||
setTimeout(() => {
|
||||
scrollToBottom(this.msgs);
|
||||
}, 1);
|
||||
// TODO @{this.msgs} in TIS:<htmlarea spellcheck="false" readonly .msgs @{this.msgs} >
|
||||
return (<div class="msgbox">
|
||||
<htmlarea spellcheck="false" readonly class="msg" >
|
||||
{msgs}
|
||||
</htmlarea>
|
||||
<div class="send">
|
||||
<input type="text" class="outline-focus" />
|
||||
<span>{svg_send}</span>
|
||||
</div>
|
||||
</div >);
|
||||
}
|
||||
|
||||
send() {
|
||||
let el = this.$("input");
|
||||
let value = (el.value || "").trim();
|
||||
el.value = "";
|
||||
if (!value) return;
|
||||
if (this.callback) this.callback(value);
|
||||
}
|
||||
|
||||
["on keydown at input"](evt) {
|
||||
// TODO is shortcutKey useless?
|
||||
if (!evt.shortcutKey) {
|
||||
// TODO TEST Windows/Mac
|
||||
if (evt.code == "KeyRETURN") {
|
||||
this.send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
["on click at div.send span"](evt) {
|
||||
this.send();
|
||||
view.focus = this.$("input");
|
||||
}
|
||||
}
|
||||
/******************** end of chatbox ****************************************/
|
||||
|
||||
/******************** start of msgbox ****************************************/
|
||||
var remember_password = false;
|
||||
var msgbox_params;
|
||||
function getMsgboxParams() {
|
||||
return msgbox_params;
|
||||
}
|
||||
|
||||
// tmp workaround https://sciter.com/forums/topic/menu-not-be-hidden-when-open-dialog-on-linux/
|
||||
export function msgbox(type, title, text, callback = null, height = 180, width = 500, retry = 0, contentStyle = "") {
|
||||
if (is_linux) { // fix menu not hidden issue
|
||||
setTimeout(() => msgbox_(type, title, text, callback, height, width, retry, contentStyle), 1);
|
||||
} else {
|
||||
msgbox_(type, title, text, callback, height, width, retry, contentStyle);
|
||||
}
|
||||
}
|
||||
|
||||
function msgbox_(type, title, text, callback, height, width, retry, contentStyle) {
|
||||
let has_msgbox = msgbox_params != null;
|
||||
if (!has_msgbox && !type) return;
|
||||
let remember = false;
|
||||
try {
|
||||
remember = handler.xcall("get_remember");
|
||||
} catch (e) { }
|
||||
msgbox_params = {
|
||||
remember: remember, type: type, text: text, title: title,
|
||||
getParams: getMsgboxParams,
|
||||
callback: callback, translate: translate,
|
||||
retry: retry, contentStyle: contentStyle,
|
||||
};
|
||||
if (has_msgbox) return;
|
||||
let dialog = {
|
||||
client: true,
|
||||
parameters: msgbox_params,
|
||||
width: width + (is_xfce ? 50 : 0),
|
||||
height: height + (is_xfce ? 50 : 0),
|
||||
};
|
||||
let html = handler.xcall("get_msgbox");
|
||||
if (html) dialog.html = html;
|
||||
else dialog.url = document.url("msgbox.html");
|
||||
let res = view.modal(dialog);
|
||||
msgbox_params = null;
|
||||
console.log(`msgbox return, type: ${type}, res: ${res}`);
|
||||
if (type.indexOf("custom") >= 0) {
|
||||
//
|
||||
} else if (!res) {
|
||||
if (!handler.is_port_forward) view.close();
|
||||
} else if (res == "!alive") {
|
||||
// do nothing
|
||||
} else if (res.type == "input-password") {
|
||||
if (!handler.is_port_forward) msgbox("connecting", "Connecting...", "Logging in...");
|
||||
handler.login(res.password, res.remember);
|
||||
if (!is_port_forward) msgbox("connecting", "Connecting...", "Logging in...");
|
||||
} else if (res.reconnect) {
|
||||
if (!handler.is_port_forward) connecting();
|
||||
handler.reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export function connecting() {
|
||||
handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait.");
|
||||
}
|
||||
|
||||
handler.msgbox = function (type, title, text, retry = 0) {
|
||||
setTimeout(() => msgbox(type, title, text, null, 180, 500, retry), 30);
|
||||
}
|
||||
|
||||
var reconnectTimeout = 1;
|
||||
handler.msgbox_retry = function (type, title, text, hasRetry) {
|
||||
handler.msgbox(type, title, text, hasRetry ? reconnectTimeout : 0);
|
||||
if (hasRetry) {
|
||||
reconnectTimeout *= 2;
|
||||
} else {
|
||||
reconnectTimeout = 1;
|
||||
}
|
||||
}
|
||||
/******************** end of msgbox ****************************************/
|
||||
// TODO Progress
|
||||
// function Progress()
|
||||
// {
|
||||
// var _val;
|
||||
// var pos = -0.25;
|
||||
|
||||
// function step() {
|
||||
// if( _val !== undefined ) { this.refresh(); return false; }
|
||||
// pos += 0.02;
|
||||
// if( pos > 1.25)
|
||||
// pos = -0.25;
|
||||
// this.refresh();
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// function paintNoValue(gfx)
|
||||
// {
|
||||
// var (w,h) = this.box(#dimension,#inner);
|
||||
// var x = pos * w;
|
||||
// w = w * 0.25;
|
||||
// gfx.fillColor( this.style#color )
|
||||
// .pushLayer(#inner-box)
|
||||
// .rectangle(x,0,w,h)
|
||||
// .popLayer();
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// this[#value] = property(v) {
|
||||
// get return _val;
|
||||
// set {
|
||||
// _val = undefined;
|
||||
// pos = -0.25;
|
||||
// this.paintContent = paintNoValue;
|
||||
// this.animate(step);
|
||||
// this.refresh();
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.value = "";
|
||||
// }
|
||||
|
||||
const svg_eye_cross = (<svg viewBox="0 -21 511.96 511">
|
||||
<path d="m506.68 261.88c7.043-16.984 7.043-36.461 0-53.461-41.621-100.4-140.03-165.27-250.71-165.27-46.484 0-90.797 11.453-129.64 32.191l-68.605-68.609c-8.3438-8.3398-21.824-8.3398-30.168 0-8.3398 8.3398-8.3398 21.824 0 30.164l271.49 271.49 86.484 86.488 68.676 68.672c4.1797 4.1797 9.6406 6.2695 15.102 6.2695 5.4609 0 10.922-2.0898 15.082-6.25 8.3438-8.3398 8.3438-21.824 0-30.164l-62.145-62.145c36.633-27.883 66.094-65.109 84.438-109.38zm-293.91-100.1c12.648-7.5742 27.391-11.969 43.199-11.969 47.062 0 85.332 38.273 85.332 85.336 0 15.805-4.3945 30.547-11.969 43.199z" />
|
||||
<path d="m255.97 320.48c-47.062 0-85.336-38.273-85.336-85.332 0-3.0938 0.59766-6.0195 0.91797-9.0039l-106.15-106.16c-25.344 24.707-46.059 54.465-60.117 88.43-7.043 16.98-7.043 36.457 0 53.461 41.598 100.39 140.01 165.27 250.69 165.27 34.496 0 67.797-6.3164 98.559-18.027l-89.559-89.559c-2.9844 0.32031-5.9062 0.91797-9 0.91797z" />
|
||||
</svg>);
|
||||
|
||||
export class PasswordComponent extends Element {
|
||||
visible = false;
|
||||
value = '';
|
||||
name = 'password';
|
||||
|
||||
constructor(props) {
|
||||
if (props && props.value) {
|
||||
this.value = props.value;
|
||||
}
|
||||
if (props && props.name) {
|
||||
this.name = props.name;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="password">
|
||||
<input class="outline-focus" name={this.name} value={this.value} type={this.visible ? "text" : "password"} />
|
||||
{this.visible ? svg_eye_cross : svg_eye}
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at svg"](svg) {
|
||||
let el = this.$("input");
|
||||
let value = el.value;
|
||||
// TODO selectionStart/selectionEnd run ok,but always return 0
|
||||
let start = el.xcall("selectionStart") || 0;
|
||||
let end = el.xcall("selectionEnd");
|
||||
this.componentUpdate({ visible: !this.visible });
|
||||
setTimeout(() => {
|
||||
let el = this.$("input");
|
||||
view.focus = el;
|
||||
el.value = value;
|
||||
el.xcall("setSelection", start, end);
|
||||
}, 30)
|
||||
}
|
||||
}
|
||||
|
||||
export function isReasonableSize(r) {
|
||||
let x = r[0];
|
||||
let y = r[1];
|
||||
return !(x < -3200 || x > 3200 || y < -3200 || y > 3200);
|
||||
}
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
include "sciter:reactor.tis";
|
||||
|
||||
var handler = $(#handler) || view;
|
||||
try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {}
|
||||
var OS = view.mediaVar("platform");
|
||||
var is_osx = OS == "OSX";
|
||||
var is_win = OS == "Windows";
|
||||
var is_linux = OS == "Linux";
|
||||
var is_file_transfer;
|
||||
var is_xfce = false;
|
||||
try { is_xfce = handler.is_xfce(); } catch(e) {}
|
||||
|
||||
function hashCode(str) {
|
||||
var hash = 160 << 16 + 114 << 8 + 91;
|
||||
for (var i = 0; i < str.length; i += 1) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return hash % 16777216;
|
||||
}
|
||||
|
||||
function intToRGB(i, a = 1) {
|
||||
return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0x7F)
|
||||
+ ',' + (i & 0xFF) + ',' + a + ')';
|
||||
}
|
||||
|
||||
function string2RGB(s, a = 1) {
|
||||
return intToRGB(hashCode(s), a);
|
||||
}
|
||||
|
||||
function getTime() {
|
||||
var now = new Date();
|
||||
return now.valueOf();
|
||||
}
|
||||
|
||||
function platformSvg(platform, color) {
|
||||
platform = (platform || "").toLowerCase();
|
||||
if (platform == "linux") {
|
||||
return <svg viewBox="0 0 256 256">
|
||||
<g transform="translate(0 256) scale(.1 -.1)" fill={color}>
|
||||
<path d="m1215 2537c-140-37-242-135-286-278-23-75-23-131 1-383l18-200-54-60c-203-224-383-615-384-831v-51l-66-43c-113-75-194-199-194-300 0-110 99-234 244-305 103-50 185-69 296-69 100 0 156 14 211 54 26 18 35 19 78 10 86-18 233-24 335-12 85 10 222 38 269 56 9 4 19-7 29-35 20-50 52-64 136-57 98 8 180 52 282 156 124 125 180 244 180 380 0 80-28 142-79 179l-36 26 4 119c5 175-22 292-105 460-74 149-142 246-286 409-43 49-78 92-78 97 0 4-7 52-15 107-8 54-19 140-24 189-13 121-41 192-103 260-95 104-248 154-373 122zm172-112c62-19 134-80 163-140 15-31 28-92 41-193 27-214 38-276 57-304 9-14 59-74 111-134 92-106 191-246 236-334 69-137 115-339 101-451l-7-55-71 10c-100 13-234-5-265-36-54-55-85-207-82-412l1-141-51-17c-104-34-245-51-380-45-69 3-142 10-162 16-32 10-37 17-53 68-23 72-87 201-136 273-80 117-158 188-237 215-37 13-37 13-34 61 13 211 182 555 373 759 57 62 58 63 58 121 0 33-9 149-19 259-21 224-18 266 26 347 67 122 193 174 330 133zm687-1720c32-9 71-25 87-36 60-42 59-151-4-274-59-119-221-250-317-257-34-3-35-2-48 47-18 65-20 329-3 413 16 83 29 110 55 115 51 10 177 6 230-8zm-1418-80c79-46 187-195 247-340 41-99 43-121 12-141-39-25-148-30-238-10-142 32-264 112-307 202-20 41-21 50-10 87 24 83 102 166 192 207 54 25 53 25 104-5z"/>
|
||||
<path d="m1395 1945c-92-16-220-52-256-70-28-15-29-18-29-89 0-247 165-397 345-312 60 28 77 46 106 111 54 123 0 378-80 374-9 0-47-7-86-14zm74-156c15-69 14-112-5-159s-55-70-111-70c-48 0-78 20-102 68-15 29-41 131-41 159 0 9 230 63 242 57 3-2 11-27 17-55z"/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
if (platform == "mac os") {
|
||||
return <svg viewBox="0 0 384 512">
|
||||
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" fill={color}/>
|
||||
</svg>;
|
||||
}
|
||||
return <svg viewBox="0 0 448 512">
|
||||
<path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" fill={color}/>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
function centerize(w, h) {
|
||||
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
|
||||
if (w > sw) w = sw;
|
||||
if (h > sh) h = sh;
|
||||
var x = (sx + sw - w) / 2;
|
||||
var y = (sy + sh - h) / 2;
|
||||
view.move(x, y, w, h);
|
||||
}
|
||||
|
||||
function setWindowButontsAndIcon(only_min=false) {
|
||||
if (only_min) {
|
||||
$(div.window-buttons).content(<div>
|
||||
<button.window tabindex="-1" role="window-minimize" #minimize><div /></button>
|
||||
</div>);
|
||||
} else {
|
||||
$(div.window-buttons).content(<div>
|
||||
<button.window tabindex="-1" role="window-minimize" #minimize><div /></button>
|
||||
<button.window tabindex="-1" role="window-maximize" #maximize><div /></button>
|
||||
<button.window tabindex="-1" role="window-close" #close><div /></button>
|
||||
</div>);
|
||||
}
|
||||
$(div.window-icon>icon).style.set {
|
||||
"background-image": "url('" + handler.get_icon() + "')",
|
||||
};
|
||||
}
|
||||
|
||||
function adjustBorder() {
|
||||
if (is_osx) {
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
$(header).style.set {
|
||||
display: "none",
|
||||
};
|
||||
} else {
|
||||
$(header).style.set {
|
||||
display: "block",
|
||||
padding: "0",
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (view.windowState == view.WINDOW_MAXIMIZED) {
|
||||
self.style.set {
|
||||
border: "window-frame-width solid transparent",
|
||||
};
|
||||
} else if (view.windowState == view.WINDOW_FULL_SCREEN) {
|
||||
self.style.set {
|
||||
border: "none",
|
||||
};
|
||||
} else {
|
||||
self.style.set {
|
||||
border: "black solid 1px",
|
||||
};
|
||||
}
|
||||
var el = $(button#maximize);
|
||||
if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED);
|
||||
el = $(span#fullscreen);
|
||||
if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN);
|
||||
}
|
||||
|
||||
var svg_checkmark = <svg viewBox="0 0 492 492"><path d="M484 105l-16-17a27 27 0 00-38 0L204 315 62 173c-5-5-12-7-19-7s-14 2-19 7L8 189a27 27 0 000 38l160 160v1l16 16c5 5 12 8 19 8 8 0 14-3 20-8l16-16v-1l245-244a27 27 0 000-38z"/></svg>;
|
||||
var svg_edit = <svg #edit viewBox="0 0 384 384">
|
||||
<path d="M0 304v80h80l236-236-80-80zM378 56L328 6c-8-8-22-8-30 0l-39 39 80 80 39-39c8-8 8-22 0-30z"/>
|
||||
</svg>;
|
||||
var svg_eye = <svg viewBox="0 0 469.33 469.33">
|
||||
<path d="m234.67 170.67c-35.307 0-64 28.693-64 64s28.693 64 64 64 64-28.693 64-64-28.694-64-64-64z"/>
|
||||
<path d="m234.67 74.667c-106.67 0-197.76 66.346-234.67 160 36.907 93.653 128 160 234.67 160 106.77 0 197.76-66.347 234.67-160-36.907-93.654-127.89-160-234.67-160zm0 266.67c-58.88 0-106.67-47.787-106.67-106.67s47.787-106.67 106.67-106.67 106.67 47.787 106.67 106.67-47.787 106.67-106.67 106.67z"/>
|
||||
</svg>;
|
||||
var svg_send = <svg viewBox="0 0 448 448">
|
||||
<polygon points="0.213 32 0 181.33 320 224 0 266.67 0.213 416 448 224"/>
|
||||
</svg>;
|
||||
var svg_chat = <svg viewBox="0 0 511.07 511.07">
|
||||
<path d="m74.39 480.54h-36.213l25.607-25.607c13.807-13.807 22.429-31.765 24.747-51.246-36.029-23.644-62.375-54.751-76.478-90.425-14.093-35.647-15.864-74.888-5.121-113.48 12.89-46.309 43.123-88.518 85.128-118.85 45.646-32.963 102.47-50.387 164.33-50.387 77.927 0 143.61 22.389 189.95 64.745 41.744 38.159 64.734 89.63 64.734 144.93 0 26.868-5.471 53.011-16.26 77.703-11.165 25.551-27.514 48.302-48.593 67.619-46.399 42.523-112.04 65-189.83 65-28.877 0-59.01-3.855-85.913-10.929-25.465 26.123-59.972 40.929-96.086 40.929zm182-420c-124.04 0-200.15 73.973-220.56 147.28-19.284 69.28 9.143 134.74 76.043 175.12l7.475 4.511-0.23 8.727c-0.456 17.274-4.574 33.912-11.945 48.952 17.949-6.073 34.236-17.083 46.99-32.151l6.342-7.493 9.405 2.813c26.393 7.894 57.104 12.241 86.477 12.241 154.37 0 224.68-93.473 224.68-180.32 0-46.776-19.524-90.384-54.976-122.79-40.713-37.216-99.397-56.888-169.71-56.888z"/>
|
||||
</svg>;
|
||||
|
||||
function scrollToBottom(el) {
|
||||
var y = el.box(#height, #content) - el.box(#height, #client);
|
||||
el.scrollTo(0, y);
|
||||
}
|
||||
|
||||
function getNowStr() {
|
||||
var now = new Date();
|
||||
return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second);
|
||||
}
|
||||
|
||||
/******************** start of chatbox ****************************************/
|
||||
class ChatBox: Reactor.Component {
|
||||
this var msgs = [];
|
||||
this var callback;
|
||||
|
||||
function this(params) {
|
||||
if (params) {
|
||||
this.msgs = params.msgs || [];
|
||||
this.callback = params.callback;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMsg(msg) {
|
||||
var cls = msg.name == "me" ? "right-side msg" : "left-side msg";
|
||||
return <div class={cls}>
|
||||
{msg.name == "me" ?
|
||||
<div .name>{msg.time + " "} me</div> :
|
||||
<div .name>{msg.name} {" " + msg.time}</div>
|
||||
}
|
||||
<div .text>{msg.text}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); });
|
||||
self.timer(1ms, function() {
|
||||
scrollToBottom(me.msgs);
|
||||
});
|
||||
return <div .msgbox>
|
||||
<htmlarea spellcheck="false" readonly .msgs @{this.msgs} >
|
||||
{msgs}
|
||||
</htmlarea>
|
||||
<div .send>
|
||||
<input|text .outline-focus />
|
||||
<span>{svg_send}</span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function send() {
|
||||
var el = this.$(input);
|
||||
var value = (el.value || "").trim();
|
||||
el.value = "";
|
||||
if (!value) return;
|
||||
if (this.callback) this.callback(value);
|
||||
}
|
||||
|
||||
event keydown $(input) (evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
this.send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event click $(div.send span) {
|
||||
this.send();
|
||||
view.focus = $(input);
|
||||
}
|
||||
}
|
||||
/******************** end of chatbox ****************************************/
|
||||
|
||||
/******************** start of msgbox ****************************************/
|
||||
var remember_password = false;
|
||||
var msgbox_params;
|
||||
function getMsgboxParams() {
|
||||
return msgbox_params;
|
||||
}
|
||||
|
||||
function msgbox(type, title, text, callback, height, width, retry=0) {
|
||||
var has_msgbox = msgbox_params != null;
|
||||
if (!has_msgbox && !type) return;
|
||||
var remember = false;
|
||||
try {
|
||||
remember = handler.get_remember();
|
||||
} catch(e) {}
|
||||
msgbox_params = {
|
||||
remember: remember, type: type, text: text, title: title,
|
||||
getParams: getMsgboxParams,
|
||||
callback: callback, retry: retry,
|
||||
};
|
||||
if (has_msgbox) return;
|
||||
var dialog = {
|
||||
client: true,
|
||||
parameters: msgbox_params,
|
||||
width: width + (is_xfce ? 50 : 0),
|
||||
height: height + (is_xfce ? 50 : 0),
|
||||
};
|
||||
var html = handler.get_msgbox();
|
||||
if (html) dialog.html = html;
|
||||
else dialog.url = self.url("msgbox.html");
|
||||
var res = view.dialog(dialog);
|
||||
msgbox_params = null;
|
||||
stdout.printf("msgbox return, type: %s, res: %s\n", type, res);
|
||||
if (type.indexOf("custom") >= 0) {
|
||||
//
|
||||
} else if (!res) {
|
||||
if (!is_port_forward) view.close();
|
||||
} else if (res == "!alive") {
|
||||
// do nothing
|
||||
} else if (res.type == "input-password") {
|
||||
if (!is_port_forward) connecting();
|
||||
handler.login(res.password, res.remember);
|
||||
} else if (res.reconnect) {
|
||||
if (!is_port_forward) connecting();
|
||||
handler.reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function connecting() {
|
||||
handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait.");
|
||||
}
|
||||
|
||||
handler.msgbox = function(type, title, text, callback=null, height=180, width=500, retry=0) {
|
||||
// directly call view.Dialog from native may crash, add timer here, seem safe
|
||||
// too short time, msgbox won't get focus, per my test, 150 is almost minimun
|
||||
self.timer(150ms, function() { msgbox(type, title, text, callback, height, width, retry); });
|
||||
}
|
||||
|
||||
var reconnectTimeout = 1;
|
||||
handler.msgbox_retry = function(type, title, text, hasRetry, callback=null, height=180, width=500) {
|
||||
handler.msgbox(type, title, text, callback, height, width, hasRetry ? reconnectTimeout : 0);
|
||||
if (hasRetry) {
|
||||
reconnectTimeout *= 2;
|
||||
} else {
|
||||
reconnectTimeout = 1;
|
||||
}
|
||||
}
|
||||
/******************** end of msgbox ****************************************/
|
||||
|
||||
function Progress()
|
||||
{
|
||||
var _val;
|
||||
var pos = -0.25;
|
||||
|
||||
function step() {
|
||||
if( _val !== undefined ) { this.refresh(); return false; }
|
||||
pos += 0.02;
|
||||
if( pos > 1.25)
|
||||
pos = -0.25;
|
||||
this.refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
function paintNoValue(gfx)
|
||||
{
|
||||
var (w,h) = this.box(#dimension,#inner);
|
||||
var x = pos * w;
|
||||
w = w * 0.25;
|
||||
gfx.fillColor( this.style#color )
|
||||
.pushLayer(#inner-box)
|
||||
.rectangle(x,0,w,h)
|
||||
.popLayer();
|
||||
return true;
|
||||
}
|
||||
|
||||
this[#value] = property(v) {
|
||||
get return _val;
|
||||
set {
|
||||
_val = undefined;
|
||||
pos = -0.25;
|
||||
this.paintContent = paintNoValue;
|
||||
this.animate(step);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
this.value = "";
|
||||
}
|
||||
@@ -30,6 +30,7 @@ table > thead {
|
||||
}
|
||||
|
||||
table > tbody {
|
||||
behavior: select-multiple;
|
||||
overflow-y: scroll-indicator;
|
||||
size: *;
|
||||
background: white;
|
||||
@@ -85,6 +86,15 @@ table.has_current tr:current /* current row */
|
||||
background-color: color(accent);
|
||||
}
|
||||
|
||||
table.has_current tbody tr:checked
|
||||
{
|
||||
background-color: color(accent);
|
||||
}
|
||||
|
||||
table.has_current tbody tr:checked td {
|
||||
color: highlighttext;
|
||||
}
|
||||
|
||||
table td
|
||||
{
|
||||
padding: 4px;
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import { handler,svg_send,translate,msgbox } from "./common.js";
|
||||
import {$} from "@sciter";
|
||||
|
||||
var remote_home_dir;
|
||||
|
||||
var svg_add_folder = <svg viewBox="0 0 443.29 443.29">
|
||||
const svg_add_folder = (<svg viewBox="0 0 443.29 443.29">
|
||||
<path d="m277.06 332.47h27.706v-55.412h55.412v-27.706h-55.412v-55.412h-27.706v55.412h-55.412v27.706h55.412z"/>
|
||||
<path d="m415.59 83.118h-202.06l-51.353-51.353c-2.597-2.597-6.115-4.058-9.794-4.058h-124.68c-15.274-1e-3 -27.706 12.431-27.706 27.705v332.47c0 15.273 12.432 27.706 27.706 27.706h387.88c15.273 0 27.706-12.432 27.706-27.706v-277.06c0-15.274-12.432-27.706-27.706-27.706zm0 304.76h-387.88v-332.47h118.94l51.354 51.353c2.597 2.597 6.115 4.058 9.794 4.058h207.79z"/>
|
||||
</svg>;
|
||||
var svg_trash = <svg viewBox="0 0 473.41 473.41">
|
||||
</svg>);
|
||||
const svg_trash = (<svg viewBox="0 0 473.41 473.41">
|
||||
<path d="m443.82 88.765h-88.765v-73.971c0-8.177-6.617-14.794-14.794-14.794h-207.12c-8.177 0-14.794 6.617-14.794 14.794v73.971h-88.764v29.588h14.39l57.116 342.69c1.185 7.137 7.354 12.367 14.592 12.367h241.64c7.238 0 13.407-5.23 14.592-12.367l57.116-342.69h14.794c-1e-3 0-1e-3 -29.588-1e-3 -29.588zm-295.88-59.177h177.53v59.176h-177.53zm196.85 414.24h-216.58l-54.241-325.47h325.06z"/>
|
||||
<path transform="matrix(.064 -.998 .998 .064 -.546 19.418)" d="m-360.4 301.29h207.54v29.592h-207.54z"/>
|
||||
<path transform="matrix(.998 -.064 .064 .998 -.628 .399)" d="m141.64 202.35h29.592v207.54h-29.592z"/>
|
||||
</svg>;
|
||||
var svg_arrow = <svg viewBox="0 0 482.24 482.24">
|
||||
</svg>);
|
||||
export const svg_arrow = (<svg viewBox="0 0 482.24 482.24">
|
||||
<path d="m206.81 447.79-206.81-206.67 206.74-206.67 24.353 24.284-165.17 165.17h416.31v34.445h-416.31l165.24 165.24z"/>
|
||||
</svg>;
|
||||
var svg_home = <svg viewBox="0 0 476.91 476.91">
|
||||
</svg>);
|
||||
const svg_home = (<svg viewBox="0 0 476.91 476.91">
|
||||
<path d="m461.78 209.41-212.21-204.89c-6.182-6.026-16.042-6.026-22.224 0l-212.2 204.88c-3.124 3.015-4.888 7.17-4.888 11.512 0 8.837 7.164 16 16 16h28.2v224c0 8.837 7.163 16 16 16h112c8.837 0 16-7.163 16-16v-128h80v128c0 8.837 7.163 16 16 16h112c8.837 0 16-7.163 16-16v-224h28.2c4.338 0 8.489-1.761 11.504-4.88 6.141-6.354 5.969-16.483-0.384-22.624zm-39.32 11.504c-8.837 0-16 7.163-16 16v224h-112v-128c0-8.837-7.163-16-16-16h-80c-8.837 0-16 7.163-16 16v128h-112v-224c0-8.837-7.163-16-16-16h-28.2l212.2-204.88 212.28 204.88h-28.28z"/>
|
||||
</svg>;
|
||||
var svg_refresh = <svg viewBox="0 0 551.13 551.13">
|
||||
</svg>);
|
||||
const svg_refresh = (<svg viewBox="0 0 551.13 551.13">
|
||||
<path d="m482.24 310.01c0 113.97-92.707 206.67-206.67 206.67s-206.67-92.708-206.67-206.67c0-102.21 74.639-187.09 172.23-203.56v65.78l86.114-86.114-86.114-86.115v71.641c-116.65 16.802-206.67 117.14-206.67 238.37 0 132.96 108.16 241.12 241.12 241.12s241.12-108.16 241.12-241.12z"/>
|
||||
</svg>;
|
||||
var svg_cancel = <svg .cancel viewBox="0 0 612 612"><polygon points="612 36.004 576.52 0.603 306 270.61 35.478 0.603 0 36.004 270.52 306.01 0 576 35.478 611.4 306 341.41 576.52 611.4 612 576 341.46 306.01"/></svg>;
|
||||
var svg_computer = <svg .computer viewBox="0 0 480 480">
|
||||
</svg>);
|
||||
export const svg_cancel = (<svg class="cancel" viewBox="0 0 612 612"><polygon points="612 36.004 576.52 0.603 306 270.61 35.478 0.603 0 36.004 270.52 306.01 0 576 35.478 611.4 306 341.41 576.52 611.4 612 576 341.46 306.01"/></svg>);
|
||||
const svg_computer = (<svg class="computer" viewBox="0 0 480 480">
|
||||
<g>
|
||||
<path fill="#2C8CFF" d="m276 395v11.148c0 2.327-1.978 4.15-4.299 3.985-21.145-1.506-42.392-1.509-63.401-0.011-2.322 0.166-4.3-1.657-4.3-3.985v-11.137c0-2.209 1.791-4 4-4h64c2.209 0 4 1.791 4 4zm204-340v288c0 17.65-14.35 32-32 32h-416c-17.65 0-32-14.35-32-32v-288c0-17.65 14.35-32 32-32h416c17.65 0 32 14.35 32 32zm-125.62 386.36c-70.231-21.843-158.71-21.784-228.76 0-4.22 1.31-6.57 5.8-5.26 10.02 1.278 4.085 5.639 6.591 10.02 5.26 66.093-20.58 151.37-21.125 219.24 0 4.22 1.31 8.71-1.04 10.02-5.26s-1.04-8.71-5.26-10.02z"/>
|
||||
</g>
|
||||
</svg>;
|
||||
</svg>);
|
||||
|
||||
// TODO
|
||||
function getSize(type, size) {
|
||||
if (!size) {
|
||||
if (type <= 3) return "";
|
||||
@@ -47,15 +51,15 @@ function getSize(type, size) {
|
||||
}
|
||||
|
||||
function getParentPath(is_remote, path) {
|
||||
var sep = handler.get_path_sep(is_remote);
|
||||
var res = path.lastIndexOf(sep);
|
||||
let sep = handler.xcall("get_path_sep",is_remote);
|
||||
let res = path.lastIndexOf(sep);
|
||||
if (res <= 0) return "/";
|
||||
return path.substr(0, res);
|
||||
}
|
||||
|
||||
function getFileName(is_remote, path) {
|
||||
var sep = handler.get_path_sep(is_remote);
|
||||
var res = path.lastIndexOf(sep);
|
||||
let sep = handler.xcall("get_path_sep",is_remote);
|
||||
let res = path.lastIndexOf(sep);
|
||||
return path.substr(res + 1);
|
||||
}
|
||||
|
||||
@@ -63,76 +67,75 @@ function getExt(name) {
|
||||
if (name.indexOf(".") == 0) {
|
||||
return "";
|
||||
}
|
||||
var i = name.lastIndexOf(".");
|
||||
let i = name.lastIndexOf(".");
|
||||
if (i > 0) return name.substr(i + 1);
|
||||
return "";
|
||||
}
|
||||
|
||||
var jobIdCounter = 1;
|
||||
|
||||
class JobTable: Reactor.Component {
|
||||
this var jobs = [];
|
||||
this var job_map = {};
|
||||
class JobTable extends Element {
|
||||
jobs = [];
|
||||
job_map = {};
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); });
|
||||
return <section><table .has_current .job-table>
|
||||
render() {
|
||||
let rows = this.jobs.map((job, i)=>this.renderRow(job, i));
|
||||
return (<section><table class="has_current job-table">
|
||||
<tbody key={rows.length}>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table></section>;
|
||||
</table></section>);
|
||||
}
|
||||
|
||||
event click $(svg.cancel) (_, me) {
|
||||
var job = this.jobs[me.parent.parent.index];
|
||||
var id = job.id;
|
||||
handler.cancel_job(id);
|
||||
["on click at svg.cancel"](_, me) {
|
||||
let job = this.jobs[me.parentElement.parentElement.index];
|
||||
let id = job.id;
|
||||
handler.xcall("cancel_job",id);
|
||||
delete this.job_map[id];
|
||||
var i = -1;
|
||||
let i = -1;
|
||||
this.jobs.map(function(job, idx) {
|
||||
if (job.id == id) i = idx;
|
||||
});
|
||||
this.jobs.splice(i, 1);
|
||||
this.update();
|
||||
var is_remote = job.is_remote;
|
||||
this.componentUpdate();
|
||||
let is_remote = job.is_remote;
|
||||
if (job.type != "del-dir") is_remote = !is_remote;
|
||||
refreshDir(is_remote);
|
||||
}
|
||||
|
||||
function send(path, is_remote) {
|
||||
var to;
|
||||
var show_hidden;
|
||||
send(path, is_remote) {
|
||||
let to;
|
||||
let show_hidden;
|
||||
if (is_remote) {
|
||||
to = file_transfer.local_folder_view.fd.path;
|
||||
to = file_transfer.local_folder_view.fd.path; // NULL
|
||||
show_hidden = file_transfer.remote_folder_view.show_hidden;
|
||||
} else {
|
||||
to = file_transfer.remote_folder_view.fd.path;
|
||||
show_hidden = file_transfer.local_folder_view.show_hidden;
|
||||
}
|
||||
if (!to) return;
|
||||
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
|
||||
var id = jobIdCounter;
|
||||
to += handler.xcall("get_path_sep",!is_remote) + getFileName(is_remote, path);
|
||||
let id = jobIdCounter;
|
||||
jobIdCounter += 1;
|
||||
this.jobs.push({ type: "transfer",
|
||||
id: id, path: path, to: to,
|
||||
include_hidden: show_hidden,
|
||||
is_remote: is_remote });
|
||||
this.job_map[id] = this.jobs[this.jobs.length - 1];
|
||||
handler.send_files(id, path, to, show_hidden, is_remote);
|
||||
this.update();
|
||||
handler.xcall("send_files",id, path, to, show_hidden, is_remote);
|
||||
this.componentUpdate();
|
||||
}
|
||||
|
||||
function addDelDir(path, is_remote) {
|
||||
var id = jobIdCounter;
|
||||
addDelDir(path, is_remote) {
|
||||
let id = jobIdCounter;
|
||||
jobIdCounter += 1;
|
||||
this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote });
|
||||
this.job_map[id] = this.jobs[this.jobs.length - 1];
|
||||
handler.remove_dir_all(id, path, is_remote);
|
||||
this.update();
|
||||
handler.xcall("remove_dir_all",id, path, is_remote);
|
||||
this.componentUpdate();
|
||||
}
|
||||
|
||||
function getSvg(job) {
|
||||
getSvg(job) {
|
||||
if (job.type == "transfer") {
|
||||
return svg_send;
|
||||
} else if (job.type == "del-dir") {
|
||||
@@ -140,37 +143,38 @@ class JobTable: Reactor.Component {
|
||||
}
|
||||
}
|
||||
|
||||
function getStatus(job) {
|
||||
if (!job.entries) return "Waiting";
|
||||
var i = job.file_num + 1;
|
||||
var n = job.num_entries || job.entries.length;
|
||||
getStatus(job) {
|
||||
if (!job.entries) return translate("Waiting");
|
||||
let i = job.file_num + 1;
|
||||
let n = job.num_entries || job.entries.length;
|
||||
if (i > n) i = n;
|
||||
var res = i + ' / ' + n + " files";
|
||||
let res = i + ' / ' + n + " " + translate("files");
|
||||
if (job.total_size > 0) {
|
||||
var s = getSize(0, job.finished_size);
|
||||
let s = getSize(0, job.finished_size);
|
||||
if (s) s += " / ";
|
||||
res += ", " + s + getSize(0, job.total_size);
|
||||
}
|
||||
// below has problem if some file skipped
|
||||
var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger();
|
||||
let percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger();
|
||||
if (job.finished) percent = '100';
|
||||
if (percent) res += ", " + percent + "%";
|
||||
if (job.finished) res = "Finished " + res;
|
||||
if (job.finished) res = translate("Finished") + " " + res;
|
||||
if (job.speed) res += ", " + getSize(0, job.speed) + "/s";
|
||||
return res;
|
||||
}
|
||||
|
||||
function updateJob(job) {
|
||||
var el = this.select("div[id=s" + job.id + "]");
|
||||
updateJob(job) {
|
||||
let el = this.$("div#s" + job.id); // TODO TEST
|
||||
console.log("updateJob el",el);
|
||||
if (el) el.text = this.getStatus(job);
|
||||
}
|
||||
|
||||
function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
|
||||
var job = this.job_map[id];
|
||||
updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
|
||||
let job = this.job_map[id];
|
||||
if (!job) return;
|
||||
if (file_num < job.file_num) return;
|
||||
job.file_num = file_num;
|
||||
var n = job.num_entries || job.entries.length;
|
||||
let n = job.num_entries || job.entries.length;
|
||||
job.finished = job.file_num >= n - 1 || err == "cancel";
|
||||
job.finished_size = finished_size;
|
||||
job.speed = speed || 0;
|
||||
@@ -178,151 +182,158 @@ class JobTable: Reactor.Component {
|
||||
if (job.type == "del-dir") {
|
||||
if (job.finished) {
|
||||
if (!err) {
|
||||
handler.remove_dir(job.id, job.path, job.is_remote);
|
||||
handler.xcall("remove_dir",job.id, job.path, job.is_remote);
|
||||
refreshDir(job.is_remote);
|
||||
}
|
||||
} else if (!job.no_confirm) {
|
||||
handler.confirm_delete_files(id, job.file_num + 1);
|
||||
handler.xcall("confirm_delete_files",id, job.file_num + 1);
|
||||
}
|
||||
} else if (job.finished || file_num == -1) {
|
||||
refreshDir(!job.is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRow(job, i) {
|
||||
var svg = this.getSvg(job);
|
||||
return <tr class={job.is_remote ? "is_remote" : ""}><td>
|
||||
renderRow(job, i) {
|
||||
svg = this.getSvg(job);
|
||||
return (<tr class={job.is_remote ? "is_remote" : ""}><td>
|
||||
{svg}
|
||||
<div .text>
|
||||
<div .path>{job.path}</div>
|
||||
<div class="text">
|
||||
<div class="path">{job.path}</div>
|
||||
<div id={"s" + job.id}>{this.getStatus(job)}</div>
|
||||
</div>
|
||||
{svg_cancel}
|
||||
</td></tr>;
|
||||
</td></tr>);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderView : Reactor.Component {
|
||||
this var fd = {};
|
||||
this var history = [];
|
||||
this var show_hidden = false;
|
||||
class FolderView extends Element {
|
||||
fd = {};
|
||||
history = [];
|
||||
show_hidden = false;
|
||||
select_dir;
|
||||
|
||||
function sep() {
|
||||
return handler.get_path_sep(this.is_remote);
|
||||
sep() {
|
||||
return handler.xcall("get_path_sep",this.is_remote);
|
||||
}
|
||||
|
||||
function this(params) {
|
||||
this(params) {
|
||||
this.is_remote = params.is_remote;
|
||||
if (this.is_remote) {
|
||||
this.show_hidden = !!handler.get_option("remote_show_hidden");
|
||||
this.show_hidden = !!handler.xcall("get_option","remote_show_hidden");
|
||||
} else {
|
||||
this.show_hidden = !!handler.get_option("local_show_hidden");
|
||||
this.show_hidden = !!handler.xcall("get_option","local_show_hidden");
|
||||
}
|
||||
if (!this.is_remote) {
|
||||
var dir = handler.get_option("local_dir");
|
||||
let dir = handler.xcall("get_option","local_dir");
|
||||
if (dir) {
|
||||
this.fd = handler.read_dir(dir, this.show_hidden);
|
||||
this.fd = handler.xcall("read_dir",dir, this.show_hidden);
|
||||
if (this.fd) return;
|
||||
}
|
||||
this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden);
|
||||
this.fd = handler.xcall("read_dir",handler.xcall("get_home_dir"), this.show_hidden);
|
||||
}
|
||||
}
|
||||
|
||||
// sort predicate
|
||||
function foldersFirst(a, b) {
|
||||
foldersFirst(a, b) {
|
||||
if (a.type <= 3 && b.type > 3) return -1;
|
||||
if (a.type > 3 && b.type <= 3) return +1;
|
||||
if (a.name == b.name) return 0;
|
||||
return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase());
|
||||
return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase()); // TODO lexicalCompare
|
||||
}
|
||||
|
||||
function render()
|
||||
render()
|
||||
{
|
||||
return <section>
|
||||
return (<section>
|
||||
{this.renderTitle()}
|
||||
{this.renderNavBar()}
|
||||
{this.renderOpBar()}
|
||||
{this.renderTable()}
|
||||
</section>;
|
||||
</section>);
|
||||
}
|
||||
|
||||
function renderTitle() {
|
||||
return <div .title>
|
||||
renderTitle() {
|
||||
return (<div class="title">
|
||||
{svg_computer}
|
||||
<div .platform>{platformSvg(handler.get_platform(this.is_remote), "white")}</div>
|
||||
<div><span>{this.is_remote ? "Remote Computer" : "Local Computer"}</span></div>
|
||||
</div>
|
||||
<div class="platform">{platformSvg(handler.xcall("get_platform",this.is_remote), "white")}</div>
|
||||
<div><span>{translate(this.is_remote ? "Remote Computer" : "Local Computer")}</span></div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
function renderNavBar() {
|
||||
return <div .toolbar .navbar>
|
||||
<div .home .button>{svg_home}</div>
|
||||
<div .goback .button>{svg_arrow}</div>
|
||||
<div .goup .button>{svg_arrow}</div>
|
||||
renderNavBar() {
|
||||
return <div class="toolbar navbar">
|
||||
<div class="home button">{svg_home}</div>
|
||||
<div class="goback button">{svg_arrow}</div>
|
||||
<div class="goup button">{svg_arrow}</div>
|
||||
{this.renderSelect()}
|
||||
<div .refresh .button>{svg_refresh}</div>
|
||||
<div class="refresh button">{svg_refresh}</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderSelect() {
|
||||
return <select editable .select-dir @{this.select_dir}>
|
||||
// TODO
|
||||
componentDidMount(){
|
||||
this.select_dir = this.$("select.select-dir")
|
||||
}
|
||||
|
||||
renderSelect() {
|
||||
return (<select editable class="select-dir">
|
||||
<option>/</option>
|
||||
</select>;
|
||||
</select>);
|
||||
}
|
||||
|
||||
function renderOpBar() {
|
||||
renderOpBar() {
|
||||
if (this.is_remote) {
|
||||
return <div .toolbar .remote>
|
||||
<div .send .button>{svg_send}<span>Receive</span></div>
|
||||
<div .spacer></div>
|
||||
<div .add-folder .button>{svg_add_folder}</div>
|
||||
<div .trash .button>{svg_trash}</div>
|
||||
</div>;
|
||||
return (<div class="toolbar remote">
|
||||
<div class="send button">{svg_send}<span>{translate('Receive')}</span></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="add-folder button">{svg_add_folder}</div>
|
||||
<div class="trash button">{svg_trash}</div>
|
||||
</div>);
|
||||
}
|
||||
return <div .toolbar>
|
||||
<div .add-folder .button>{svg_add_folder}</div>
|
||||
<div .trash .button>{svg_trash}</div>
|
||||
<div .spacer></div>
|
||||
<div .send .button><span>Send</span>{svg_send}</div>
|
||||
</div>;
|
||||
return (<div class="toolbar">
|
||||
<div class="add-folder button">{svg_add_folder}</div>
|
||||
<div class="trash button">{svg_trash}</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="send button"><span>{translate('Send')}</span>{svg_send}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
function get_updated() {
|
||||
this.table.sortRows(false);
|
||||
get_updated() {
|
||||
this.table.sortRows(false); // TODO sortRows
|
||||
if (this.fd && this.fd.path) this.select_dir.value = this.fd.path;
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
var fd = this.fd;
|
||||
var entries = fd.entries || [];
|
||||
var table = this.table;
|
||||
renderTable() {
|
||||
let fd = this.fd;
|
||||
let entries = fd.entries || [];
|
||||
let table = this.table;
|
||||
if (!table || !table.sortBy) {
|
||||
entries.sort(this.foldersFirst);
|
||||
entries.sort(this.foldersFirst); // TODO sort function
|
||||
}
|
||||
var me = this;
|
||||
var path = fd.path;
|
||||
let path = fd.path;
|
||||
if (path != "/" && path) {
|
||||
entries = [{ name: "..", type: 1 }].concat(entries);
|
||||
}
|
||||
var rows = entries.map(function(e) { return me.renderRow(e); });
|
||||
var id = (this.is_remote ? "remote" : "local") + "-folder-view";
|
||||
return <table @{this.table} .folder-view .has_current id={id}>
|
||||
let rows = entries.map(e=>this.renderRow(e));
|
||||
let id = (this.is_remote ? "remote" : "local") + "-folder-view";
|
||||
//@{} return (<table @{this.table} .folder-view .has_current id={id}>
|
||||
|
||||
return (<table class="folder-view has_current" id={id}>
|
||||
<thead>
|
||||
<tr><th></th><th .sortable>Name</th><th .sortable>Modified</th><th .sortable>Size</th></tr>
|
||||
<tr><th></th><th class="sortable">{translate('Name')}</th><th class="sortable">{translate('Modified')}</th><th class="sortable">{translate('Size')}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<popup>
|
||||
<menu.context id={id}>
|
||||
<li #switch-hidden class={this.show_hidden ? "selected" : ""}><span>{svg_checkmark}</span>Show Hidden Files</li>
|
||||
<menu class="context" id={id}>
|
||||
<li id="switch-hidden" class={this.show_hidden ? "selected" : ""}><span>{svg_checkmark}</span>{translate('Show Hidden Files')}</li>
|
||||
</menu>
|
||||
</popup>
|
||||
</table>;
|
||||
</table>);
|
||||
}
|
||||
|
||||
function joinPath(name) {
|
||||
var path = this.fd.path;
|
||||
joinPath(name) {
|
||||
let path = this.fd.path;
|
||||
if (path == "/") {
|
||||
if (this.sep() == "/") return this.sep() + name;
|
||||
else return name;
|
||||
@@ -330,162 +341,188 @@ class FolderView : Reactor.Component {
|
||||
return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name;
|
||||
}
|
||||
|
||||
function attached() {
|
||||
var me = this;
|
||||
this.table.onRowDoubleClick = function (row) {
|
||||
var type = row[0].attributes["type"];
|
||||
attached() {
|
||||
this.table.onRowDoubleClick = (row)=>{
|
||||
let type = row[0].attributes["type"];
|
||||
if (type > 3) return;
|
||||
var name = row[1].text;
|
||||
var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name);
|
||||
me.goto(path, true);
|
||||
let name = row[1].text;
|
||||
let path = name == ".." ? getParentPath(this.is_remote, this.fd.path) : this.joinPath(name);
|
||||
this.goto(path, true);
|
||||
}
|
||||
this.get_updated();
|
||||
}
|
||||
|
||||
function goto(path, push) {
|
||||
goto(path, push) {
|
||||
if (!path) return;
|
||||
if (this.sep() == "\\" && path.length == 2) { // windows drive
|
||||
path += "\\";
|
||||
}
|
||||
if (push) this.pushHistory();
|
||||
if (this.is_remote) {
|
||||
handler.read_remote_dir(path, this.show_hidden);
|
||||
handler.xcall("read_remote_dir",path, this.show_hidden);
|
||||
} else {
|
||||
var fd = handler.read_dir(path, this.show_hidden);
|
||||
var fd = handler.xcall("read_dir",path, this.show_hidden);
|
||||
this.refresh({ fd: fd });
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(data) {
|
||||
refresh(data) {
|
||||
if (!data.fd || !data.fd.path) return;
|
||||
if (this.is_remote && !remote_home_dir) {
|
||||
remote_home_dir = data.fd.path;
|
||||
}
|
||||
this.update(data);
|
||||
var me = this;
|
||||
self.timer(1ms, function() { me.get_updated(); });
|
||||
this.componentUpdate(data);
|
||||
setTimeout(()=>this.get_updated(),1);
|
||||
}
|
||||
|
||||
function renderRow(entry) {
|
||||
var path;
|
||||
renderRow(entry) {
|
||||
let path;
|
||||
if (this.is_remote) {
|
||||
path = handler.get_icon_path(entry.type, getExt(entry.name));
|
||||
path = handler.xcall("get_icon_path",entry.type, getExt(entry.name));
|
||||
} else {
|
||||
path = this.joinPath(entry.name);
|
||||
}
|
||||
var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0;
|
||||
return <tr>
|
||||
let tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0; // TODO toFloat()
|
||||
return (<tr role="option">
|
||||
<td type={entry.type} filename={path}></td>
|
||||
<td>{entry.name}</td>
|
||||
<td value={entry.time || 0}>{tm || ""}</td>
|
||||
<td value={entry.size || 0}>{getSize(entry.type, entry.size)}</td>
|
||||
</tr>;
|
||||
</tr>);
|
||||
}
|
||||
|
||||
event click $(#switch-hidden) {
|
||||
["on click at #switch-hidden"]() {
|
||||
this.show_hidden = !this.show_hidden;
|
||||
this.refreshDir();
|
||||
}
|
||||
|
||||
event click $(.goup) () {
|
||||
var path = this.fd.path;
|
||||
["on click at .goup"]() {
|
||||
let path = this.fd.path;
|
||||
if (!path || path == "/") return;
|
||||
path = getParentPath(this.is_remote, path);
|
||||
this.goto(path, true);
|
||||
}
|
||||
|
||||
event click $(.goback) () {
|
||||
var path = this.history.pop();
|
||||
["on click at .goback"] () {
|
||||
let path = this.history.pop();
|
||||
if (!path) return;
|
||||
this.goto(path, false);
|
||||
}
|
||||
|
||||
event click $(.trash) () {
|
||||
var row = this.getCurrentRow();
|
||||
if (!row) return;
|
||||
var path = row[0];
|
||||
var type = row[1];
|
||||
var new_history = [];
|
||||
for (var i = 0; i < this.history.length; ++i) {
|
||||
var h = this.history[i];
|
||||
if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h);
|
||||
["on click at .trash"]() {
|
||||
let rows = this.getCurrentRows();
|
||||
if (!rows || rows.length == 0) return;
|
||||
|
||||
let delete_dirs = new Array();
|
||||
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
let row = rows[i];
|
||||
|
||||
let path = row[0];
|
||||
let type = row[1];
|
||||
|
||||
let new_history = [];
|
||||
for (let j = 0; j < this.history.length; ++j) {
|
||||
let h = this.history[j];
|
||||
if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h);
|
||||
}
|
||||
this.history = new_history;
|
||||
if (type == 1) {
|
||||
delete_dirs.push(path);
|
||||
} else {
|
||||
confirmDelete(path, this.is_remote);
|
||||
}
|
||||
}
|
||||
this.history = new_history;
|
||||
if (type == 1) {
|
||||
file_transfer.job_table.addDelDir(path, this.is_remote);
|
||||
} else {
|
||||
confirmDelete(path, this.is_remote);
|
||||
for (let i = 0; i < delete_dirs.length; ++i) {
|
||||
file_transfer.job_table.addDelDir(delete_dirs[i], this.is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
event click $(.add-folder) () {
|
||||
var me = this;
|
||||
handler.msgbox("custom", "Create Folder", "<div .form> \
|
||||
<div>Please enter the folder name:</div> \
|
||||
["on click at .add-folder"]() {
|
||||
let me = this;
|
||||
msgbox("custom", translate("Create Folder"), "<div .form> \
|
||||
<div>" + translate("Please enter the folder name") + ":</div> \
|
||||
<div><input|text(name) .outline-focus /></div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) return;
|
||||
if (!res.name) return;
|
||||
var name = res.name.trim();
|
||||
let name = res.name.trim();
|
||||
if (!name) return;
|
||||
if (name.indexOf(me.sep()) >= 0) {
|
||||
handler.msgbox("custom-error", "Create Folder", "Invalid folder name");
|
||||
return;
|
||||
}
|
||||
var path = me.joinPath(name);
|
||||
handler.create_dir(jobIdCounter, path, me.is_remote);
|
||||
let path = me.joinPath(name);
|
||||
handler.xcall("create_dir",jobIdCounter, path, me.is_remote);
|
||||
create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path };
|
||||
jobIdCounter += 1;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshDir() {
|
||||
refreshDir() {
|
||||
this.goto(this.fd.path, false);
|
||||
}
|
||||
|
||||
event click $(.refresh) () {
|
||||
["on click at .refresh"]() {
|
||||
this.refreshDir();
|
||||
}
|
||||
|
||||
event click $(.home) () {
|
||||
var path = this.is_remote ? remote_home_dir : handler.get_home_dir();
|
||||
["on click at .home"]() {
|
||||
let path = this.is_remote ? remote_home_dir : handler.xcall("get_home_dir");
|
||||
if (!path) return;
|
||||
if (path == this.fd.path) return;
|
||||
this.goto(path, true);
|
||||
}
|
||||
|
||||
function getCurrentRow() {
|
||||
var row = this.table.getCurrentRow();
|
||||
getCurrentRow() {
|
||||
let row = this.table.getCurrentRow(); // TEST getCurrentRow
|
||||
if (!row) return;
|
||||
var name = row[1].text;
|
||||
let name = row[1].text;
|
||||
if (!name || name == "..") return;
|
||||
var type = row[0].attributes["type"];
|
||||
let type = row[0].attributes["type"];
|
||||
return [this.joinPath(name), type];
|
||||
}
|
||||
|
||||
event click $(.send) () {
|
||||
var cur = this.getCurrentRow();
|
||||
if (!cur) return;
|
||||
file_transfer.job_table.send(cur[0], this.is_remote);
|
||||
getCurrentRows() {
|
||||
let rows = this.table.getCurrentRows();
|
||||
if (!rows || rows.length== 0) return;
|
||||
|
||||
let records = new Array();
|
||||
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
let name = rows[i][1].text;
|
||||
if (!name || name == "..") continue;
|
||||
|
||||
let type = rows[i][0].attributes["type"];
|
||||
records.push([this.joinPath(name), type]);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
event change $(.select-dir) (_, el) {
|
||||
var x = getTime() - last_key_time;
|
||||
["on click at .send"]() {
|
||||
let rows = this.getCurrentRows();
|
||||
if (!rows || rows.length == 0) return;
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
file_transfer.job_table.send(rows[i][0], this.is_remote);
|
||||
}
|
||||
}
|
||||
|
||||
["on change at .select-dir"](_, el) {
|
||||
var x = getTime() - last_key_time; // TODO getTime
|
||||
if (x < 1000) return;
|
||||
if (this.fd.path != el.value) {
|
||||
this.goto(el.value, true);
|
||||
}
|
||||
}
|
||||
|
||||
event keydown $(.select-dir) (evt, me) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
|
||||
["on keydown at .select-dir"](evt, me) {
|
||||
if (evt.code == "KeyRETURN") { // TODO TEST mac
|
||||
this.goto(me.value, true);
|
||||
}
|
||||
}
|
||||
|
||||
function pushHistory() {
|
||||
var path = this.fd.path;
|
||||
pushHistory() {
|
||||
let path = this.fd.path;
|
||||
if (!path) return;
|
||||
if (path != this.history[this.history.length - 1]) this.history.push(path);
|
||||
}
|
||||
@@ -493,32 +530,37 @@ class FolderView : Reactor.Component {
|
||||
|
||||
var file_transfer;
|
||||
|
||||
class FileTransfer: Reactor.Component {
|
||||
function this(params) {
|
||||
file_transfer = this;
|
||||
class FileTransfer extends Element {
|
||||
this() {
|
||||
file_transfer = this;
|
||||
}
|
||||
// TODO @{}
|
||||
// <FolderView is_remote={false} @{this.local_folder_view} />
|
||||
// <FolderView is_remote={true} @{this.remote_folder_view}/>
|
||||
// <JobTable @{this.job_table} />
|
||||
|
||||
function render() {
|
||||
return <div #file-transfer>
|
||||
<FolderView is_remote={false} @{this.local_folder_view} />
|
||||
<FolderView is_remote={true} @{this.remote_folder_view}/>
|
||||
<JobTable @{this.job_table} />
|
||||
</div>;
|
||||
render() {
|
||||
return (<div id="file-transfer">
|
||||
<FolderView is_remote={false} />
|
||||
<FolderView is_remote={true} />
|
||||
<JobTable />
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFileTransfer()
|
||||
export function initializeFileTransfer()
|
||||
{
|
||||
$(#file-transfer-wrapper).content(<FileTransfer />);
|
||||
$(#video-wrapper).style.set { visibility: "hidden", position: "absolute" };
|
||||
$(#file-transfer-wrapper).style.set { display: "block" };
|
||||
$("#file-transfer-wrapper").content(<FileTransfer />);
|
||||
$("#video-wrapper").style.setProperty("visibility","hidden");
|
||||
$("#video-wrapper").style.setProperty("position","absolute");
|
||||
$("#file-transfer-wrapper").style.setProperty("display","block");
|
||||
}
|
||||
|
||||
handler.updateFolderFiles = function(fd) {
|
||||
fd.entries = fd.entries || [];
|
||||
if (fd.id > 0) {
|
||||
var jt = file_transfer.job_table;
|
||||
var job = jt.job_map[fd.id];
|
||||
let jt = file_transfer.job_table;
|
||||
let job = jt.job_map[fd.id];
|
||||
if (job) {
|
||||
job.file_num = -1;
|
||||
job.total_size = fd.total_size;
|
||||
@@ -536,7 +578,7 @@ handler.jobProgress = function(id, file_num, speed, finished_size) {
|
||||
}
|
||||
|
||||
handler.jobDone = function(id, file_num = -1) {
|
||||
var job = deleting_single_file_jobs[id] || create_dir_jobs[id];
|
||||
let job = deleting_single_file_jobs[id] || create_dir_jobs[id];
|
||||
if (job) {
|
||||
refreshDir(job.is_remote);
|
||||
return;
|
||||
@@ -547,12 +589,12 @@ handler.jobDone = function(id, file_num = -1) {
|
||||
handler.jobError = function(id, err, file_num = -1) {
|
||||
var job = deleting_single_file_jobs[id];
|
||||
if (job) {
|
||||
handler.msgbox("custom-error", "Delete File", err);
|
||||
msgbox("custom-error", "Delete File", err);
|
||||
return;
|
||||
}
|
||||
job = create_dir_jobs[id];
|
||||
if (job) {
|
||||
handler.msgbox("custom-error", "Create Folder", err);
|
||||
msgbox("custom-error", "Create Folder", err);
|
||||
return;
|
||||
}
|
||||
if (file_num < 0) {
|
||||
@@ -570,12 +612,12 @@ var deleting_single_file_jobs = {};
|
||||
var create_dir_jobs = {}
|
||||
|
||||
function confirmDelete(path, is_remote) {
|
||||
handler.msgbox("custom-skip", "Confirm Delete", "<div .form> \
|
||||
<div>Are you sure you want to delete this file?</div> \
|
||||
msgbox("custom-skip", "Confirm Delete", "<div .form> \
|
||||
<div>" + translate('Are you sure you want to delete this file?') + "</div> \
|
||||
<div.ellipsis style=\"font-weight: bold;\">" + path + "</div> \
|
||||
</div>", function(res=null) {
|
||||
if (res) {
|
||||
handler.remove_file(jobIdCounter, path, 0, is_remote);
|
||||
handler.xcall("remove_file",jobIdCounter, path, 0, is_remote);
|
||||
deleting_single_file_jobs[jobIdCounter] = { is_remote: is_remote, path: path };
|
||||
jobIdCounter += 1;
|
||||
}
|
||||
@@ -589,12 +631,12 @@ handler.confirmDeleteFiles = function(id, i, name) {
|
||||
var n = job.num_entries;
|
||||
if (i >= n) return;
|
||||
var file_path = job.path;
|
||||
if (name) file_path += handler.get_path_sep(job.is_remote) + name;
|
||||
handler.msgbox("custom-skip", "Confirm Delete", "<div .form> \
|
||||
<div>Deleting #" + (i + 1) + " of " + n + " files.</div> \
|
||||
<div>Are you sure you want to delete this file?</div> \
|
||||
if (name) file_path += handler.xcall("get_path_sep",job.is_remote) + name;
|
||||
msgbox("custom-skip", "Confirm Delete", "<div .form> \
|
||||
<div>" + translate('Deleting') + " #" + (i + 1) + " / " + n + " " + translate('files') + ".</div> \
|
||||
<div>" + translate('Are you sure you want to delete this file?') + "</div> \
|
||||
<div.ellipsis style=\"font-weight: bold;\" .text>" + name + "</div> \
|
||||
<div><button|checkbox(remember) {ts}>Do this for all conflicts</button></div> \
|
||||
<div><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) {
|
||||
jt.updateJobStatus(id, i - 1, "cancel");
|
||||
@@ -604,18 +646,18 @@ handler.confirmDeleteFiles = function(id, i, name) {
|
||||
} else {
|
||||
job.no_confirm = res.remember;
|
||||
if (job.no_confirm) handler.set_no_confirm(id);
|
||||
handler.remove_file(id, file_path, i, job.is_remote);
|
||||
handler.xcall("remove_file",id, file_path, i, job.is_remote);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save_file_transfer_close_state() {
|
||||
export function save_file_transfer_close_state() {
|
||||
var local_dir = file_transfer.local_folder_view.fd.path || "";
|
||||
var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : "";
|
||||
var remote_dir = file_transfer.remote_folder_view.fd.path || "";
|
||||
var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : "";
|
||||
handler.save_close_state("local_dir", local_dir);
|
||||
handler.save_close_state("local_show_hidden", local_show_hidden);
|
||||
handler.save_close_state("remote_dir", remote_dir);
|
||||
handler.save_close_state("remote_show_hidden", remote_show_hidden);
|
||||
handler.xcall("save_close_state","local_dir", local_dir);
|
||||
handler.xcall("save_close_state","local_show_hidden", local_show_hidden);
|
||||
handler.xcall("save_close_state","remote_dir", remote_dir);
|
||||
handler.xcall("save_close_state","remote_show_hidden", remote_show_hidden);
|
||||
}
|
||||
@@ -24,6 +24,11 @@ class Grid: Behavior {
|
||||
{
|
||||
return this.$(tbody>tr:current);
|
||||
}
|
||||
|
||||
function getCurrentRows()
|
||||
{
|
||||
return this.$$(tbody>tr:checked);
|
||||
}
|
||||
|
||||
function getCurrentColumn()
|
||||
{
|
||||
@@ -42,6 +42,24 @@ header .remote-id {
|
||||
margin: * 0;
|
||||
}
|
||||
|
||||
header span:hover {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
@media platform != "OSX" {
|
||||
header span:hover {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
header #screen:hover {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
|
||||
header #secure:hover {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
header span:active, header #screen:active {
|
||||
color: black;
|
||||
background: color(gray-bg);
|
||||
|
||||
411
src/ui/header.js
Normal file
411
src/ui/header.js
Normal file
@@ -0,0 +1,411 @@
|
||||
import { handler,view,setWindowButontsAndIcon,translate,msgbox,adjustBorder,is_osx,is_xfce,svg_chat,svg_checkmark, is_linux } from "./common.js";
|
||||
import {$,$$} from "@sciter";
|
||||
import { adaptDisplay, audio_enabled, clipboard_enabled, keyboard_enabled } from "./remote.js";
|
||||
var pi = handler.xcall("get_default_pi"); // peer information
|
||||
|
||||
var chat_msgs = [];
|
||||
|
||||
const svg_fullscreen = (<svg viewBox="0 0 357 357">
|
||||
<path d="M51,229.5H0V357h127.5v-51H51V229.5z M0,127.5h51V51h76.5V0H0V127.5z M306,306h-76.5v51H357V229.5h-51V306z M229.5,0v51 H306v76.5h51V0H229.5z"/>
|
||||
</svg>);
|
||||
const svg_action = (<svg viewBox="-91 0 512 512"><path d="M315 211H191L298 22a15 15 0 00-13-22H105c-6 0-12 4-14 10L1 281a15 15 0 0014 20h127L61 491a15 15 0 0025 16l240-271a15 15 0 00-11-25z"/></svg>);
|
||||
const svg_display = (<svg viewBox="0 0 640 512">
|
||||
<path d="M592 0H48A48 48 0 0 0 0 48v320a48 48 0 0 0 48 48h240v32H112a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H352v-32h240a48 48 0 0 0 48-48V48a48 48 0 0 0-48-48zm-16 352H64V64h512z"/>
|
||||
</svg>);
|
||||
const svg_secure = (<svg viewBox="0 0 347.97 347.97">
|
||||
<path fill="#3F7D46" d="m317.31 54.367c-59.376 0-104.86-16.964-143.33-54.367-38.461 37.403-83.947 54.367-143.32 54.367 0 97.405-20.155 236.94 143.32 293.6 163.48-56.666 143.33-196.2 143.33-293.6zm-155.2 171.41-47.749-47.756 21.379-21.378 26.37 26.376 50.121-50.122 21.378 21.378-71.499 71.502z"/>
|
||||
</svg>);
|
||||
const svg_insecure = (<svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M238.802 115.023l-111.573 114.68-8.6-8.367L230.2 106.656z"/><path d="M125.559 108.093l114.68 111.572-8.368 8.601-114.68-111.572z"/></g></svg>);
|
||||
const svg_insecure_relay = (<svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z"/></g></svg>);
|
||||
const svg_secure_relay = (<svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="#3f7d46" stroke="#3f7d46" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z" fill="#fff"/></g></svg>);
|
||||
|
||||
var cur_window_state = view.state;
|
||||
|
||||
|
||||
if (is_linux) {
|
||||
// check_state_change;
|
||||
setInterval(() => {
|
||||
if (view.state != cur_window_state) {
|
||||
stateChanged();
|
||||
}
|
||||
}, 30);
|
||||
} else {
|
||||
view.on("statechange",()=>{
|
||||
stateChanged();
|
||||
})
|
||||
}
|
||||
|
||||
function get_id() {
|
||||
return handler.xcall("get_option","alias") || handler.xcall("get_id")
|
||||
}
|
||||
|
||||
function stateChanged() {
|
||||
console.log('state changed from ' + cur_window_state + ' -> ' + view.state);
|
||||
cur_window_state = view.state;
|
||||
adjustBorder();
|
||||
adaptDisplay();
|
||||
if (cur_window_state != Window.WINDOW_MINIMIZED) {
|
||||
view.focus = handler; // to make focus away from restore/maximize button, so that enter key work
|
||||
}
|
||||
let fs = view.state == Window.WINDOW_FULL_SCREEN;
|
||||
let el = $("#fullscreen");
|
||||
if (el) el.classList.toggle("active", fs);
|
||||
el = $("#maximize");
|
||||
if (el) {
|
||||
el.state.disabled = fs; // TODO TEST
|
||||
}
|
||||
if (fs) {
|
||||
$("header").style.setProperty("display","none");
|
||||
}
|
||||
}
|
||||
|
||||
export var header;
|
||||
var old_window_state = Window.WINDOW_SHOWN;
|
||||
var input_blocked;
|
||||
|
||||
class Header extends Element {
|
||||
this() {
|
||||
header = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
let icon_conn;
|
||||
let title_conn;
|
||||
if (this.secure_connection && this.direct_connection) {
|
||||
icon_conn = svg_secure;
|
||||
title_conn = translate("Direct and encrypted connection");
|
||||
} else if (this.secure_connection && !this.direct_connection) {
|
||||
icon_conn = svg_secure_relay;
|
||||
title_conn = translate("Relayed and encrypted connection");
|
||||
} else if (!this.secure_connection && this.direct_connection) {
|
||||
icon_conn = svg_insecure;
|
||||
title_conn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
icon_conn = svg_insecure_relay;
|
||||
title_conn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
let title = get_id();
|
||||
if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")";
|
||||
if ((pi.displays || []).length == 0) {
|
||||
return (<div class="ellipsis" style="size:*;text-align:center;margin:*;">{title}</div>);
|
||||
}
|
||||
let screens = pi.displays.map(function(d, i) {
|
||||
return <div id="screen" class={pi.current_display == i ? "current" : ""}>
|
||||
{i+1}
|
||||
</div>;
|
||||
});
|
||||
updateWindowToolbarPosition();
|
||||
let style = "flow:horizontal;";
|
||||
if (is_osx) style += "margin:*";
|
||||
setTimeout(toggleMenuState,1);
|
||||
|
||||
return (<div style={style}>
|
||||
{is_osx || is_xfce ? "" : <span id="fullscreen">{svg_fullscreen}</span>}
|
||||
<div id="screens">
|
||||
<span id="secure" title={title_conn}>{icon_conn}</span>
|
||||
<div class="remote-id">{get_id()}</div>
|
||||
<div style="flow:horizontal;border-spacing: 0.5em;">{screens}</div>
|
||||
{this.renderGlobalScreens()}
|
||||
</div>
|
||||
<span id="chat">{svg_chat}</span>
|
||||
<span id="action">{svg_action}</span>
|
||||
<span id="display">{svg_display}</span>
|
||||
{this.renderDisplayPop()}
|
||||
{this.renderActionPop()}
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderDisplayPop() {
|
||||
return (<popup>
|
||||
<menu class="context" id="display-options">
|
||||
<li id="adjust-window" style="display:none">{translate('Adjust Window')}</li>
|
||||
<div id="adjust-window" class="separator" style="display:none"/>
|
||||
<li id="original" type="view-style"><span>{svg_checkmark}</span>{translate('Original')}</li>
|
||||
<li id="shrink" type="view-style"><span>{svg_checkmark}</span>{translate('Shrink')}</li>
|
||||
<li id="stretch" type="view-style"><span>{svg_checkmark}</span>{translate('Stretch')}</li>
|
||||
<div class="separator" />
|
||||
<li id="best" type="image-quality"><span>{svg_checkmark}</span>{translate('Good image quality')}</li>
|
||||
<li id="balanced" type="image-quality"><span>{svg_checkmark}</span>{translate('Balanced')}</li>
|
||||
<li id="low" type="image-quality"><span>{svg_checkmark}</span>{translate('Optimize reaction time')}</li>
|
||||
<li id="custom" type="image-quality"><span>{svg_checkmark}</span>{translate('Custom')}</li>
|
||||
<div class="separator" />
|
||||
<li id="show-remote-cursor" class="toggle-option"><span>{svg_checkmark}</span>{translate('Show remote cursor')}</li>
|
||||
{audio_enabled ? <li id="disable-audio" class="toggle-option"><span>{svg_checkmark}</span>{translate('Mute')}</li> : ""}
|
||||
{keyboard_enabled && clipboard_enabled ? <li id="disable-clipboard" class="toggle-option"><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
|
||||
{keyboard_enabled ? <li id="lock-after-session-end" class="toggle-option"><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
|
||||
{false && pi.platform == "Windows" ? <li id="privacy-mode" class="toggle-option"><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
|
||||
</menu>
|
||||
</popup>);
|
||||
}
|
||||
|
||||
renderActionPop() {
|
||||
return (<popup>
|
||||
<menu class="context" id="action-options">
|
||||
<li id="transfer-file">{translate('Transfer File')}</li>
|
||||
<li id="tunnel">{translate('TCP Tunneling')}</li>
|
||||
<div class="separator" />
|
||||
{keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ? <li id="ctrl-alt-del">{translate('Insert')} Ctrl + Alt + Del</li> : ""}
|
||||
<div class="separator" />
|
||||
{keyboard_enabled ? <li id="lock-screen">{translate('Insert Lock')}</li> : ""}
|
||||
{false && pi.platform == "Windows" ? <li id="block-input">Block user input </li> : ""}
|
||||
{handler.xcall("support_refresh") ? <li id="refresh">{translate('Refresh')}</li> : ""}
|
||||
</menu>
|
||||
</popup>);
|
||||
}
|
||||
|
||||
renderGlobalScreens() {
|
||||
if (pi.displays.length < 3) return "";
|
||||
let x0 = 9999999;
|
||||
let y0 = 9999999;
|
||||
let x = -9999999;
|
||||
let y = -9999999;
|
||||
pi.displays.map(function(d, i) {
|
||||
if (d.x < x0) x0 = d.x;
|
||||
if (d.y < y0) y0 = d.y;
|
||||
let dx = d.x + d.width;
|
||||
if (dx > x) x = dx;
|
||||
let dy = d.y + d.height;
|
||||
if (dy > y) y = dy;
|
||||
});
|
||||
let w = x - x0;
|
||||
let h = y - y0;
|
||||
let scale = 16. / h;
|
||||
let screens = pi.displays.map(function(d, i) {
|
||||
let min_wh = d.width > d.height ? d.height : d.width;
|
||||
let fs = min_wh * 0.9 * scale;
|
||||
let style = "width:" + (d.width * scale) + "px;" +
|
||||
"height:" + (d.height * scale) + "px;" +
|
||||
"left:" + ((d.x - x0) * scale) + "px;" +
|
||||
"top:" + ((d.y - y0) * scale) + "px;" +
|
||||
"font-size:" + fs + "px;";
|
||||
if (is_osx) {
|
||||
style += "line-height:" + fs + "px;";
|
||||
}
|
||||
return <div style={style} class={pi.current_display == i ? "current" : ""}>{i+1}</div>;
|
||||
});
|
||||
|
||||
let style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;";
|
||||
return <div id="global-screens" style={style}>
|
||||
{screens}
|
||||
</div>;
|
||||
}
|
||||
|
||||
["on click at #fullscreen"](_, el) {
|
||||
if (view.state == Window.WINDOW_FULL_SCREEN) {
|
||||
if (old_window_state == Window.WINDOW_MAXIMIZED) {
|
||||
view.state = Window.WINDOW_SHOWN;
|
||||
}
|
||||
view.state = old_window_state;
|
||||
} else {
|
||||
old_window_state = view.state;
|
||||
if (view.state == Window.WINDOW_MAXIMIZED) {
|
||||
view.state = Window.WINDOW_SHOWN;
|
||||
}
|
||||
view.state = Window.WINDOW_FULL_SCREEN;
|
||||
if (is_linux) { setTimeout(()=>view.state = Window.WINDOW_FULL_SCREEN,150); }
|
||||
}
|
||||
}
|
||||
|
||||
["on click at #chat"]() {
|
||||
startChat();
|
||||
}
|
||||
|
||||
["on click at #action"](_, me) {
|
||||
let menu = $("menu#action-options");
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
["on click at #display"](_, me) {
|
||||
let menu = $("menu#display-options");
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
["on click at #screen"](_, me) {
|
||||
if (pi.current_display == me.index) return;
|
||||
handler.xcall("switch_display",me.index);
|
||||
}
|
||||
|
||||
["on click at #transfer-file"]() {
|
||||
handler.xcall("transfer_file");
|
||||
}
|
||||
|
||||
["on click at #tunnel"] () {
|
||||
handler.xcall("tunnel");
|
||||
}
|
||||
|
||||
["on click at #ctrl-alt-del"]() {
|
||||
handler.xcall("ctrl_alt_del");
|
||||
}
|
||||
|
||||
["on click at #lock-screen"]() {
|
||||
handler.xcall("lock_screen");
|
||||
}
|
||||
|
||||
["on click at #refresh"] () {
|
||||
handler.xcall("refresh_video");
|
||||
}
|
||||
|
||||
["on click at #block-input"] (_,me) {
|
||||
if (!input_blocked) {
|
||||
handler.xcall("toggle_option","block-input");
|
||||
input_blocked = true;
|
||||
me.text = "Unblock user input"; // TEST
|
||||
} else {
|
||||
handler.xcall("toggle_option","unblock-input");
|
||||
input_blocked = false;
|
||||
me.text = "Block user input";
|
||||
}
|
||||
}
|
||||
|
||||
["on click at menu#display-options>li"] (_, me) {
|
||||
if (me.id == "custom") {
|
||||
handle_custom_image_quality();
|
||||
} else if (me.attributes.hasClass("toggle-option")) {
|
||||
handler.toggle_option(me.id);
|
||||
toggleMenuState();
|
||||
} else if (!me.attributes.hasClass("selected")) {
|
||||
let type = me.attributes["type"];
|
||||
if (type == "image-quality") {
|
||||
handler.xcall("save_image_quality",me.id);
|
||||
} else if (type == "view-style") {
|
||||
handler.xcall("save_view_style",me.id);
|
||||
adaptDisplay();
|
||||
}
|
||||
toggleMenuState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handle_custom_image_quality() {
|
||||
let tmp = handler.xcall("get_custom_image_quality");
|
||||
let bitrate0 = tmp[0] || 50;
|
||||
let quantizer0 = tmp.length > 1 ? tmp[1] : 100;
|
||||
msgbox("custom", "Custom Image Quality", "<div .form> \
|
||||
<div><input type=\"hslider\" style=\"width: 50%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate0 + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% bitrate</div> \
|
||||
<div><input type=\"hslider\" style=\"width: 50%\" name=\"quantizer\" max=\"100\" min=\"0\" value=\"" + quantizer0 + "\"/ buddy=\"quantizer-buddy\"><b #quantizer-buddy>x</b>% quantizer</div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) return;
|
||||
if (!res.bitrate) return;
|
||||
handler.xcall("save_custom_image_quality",res.bitrate, res.quantizer);
|
||||
toggleMenuState();
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
let values = [];
|
||||
let q = handler.xcall("get_image_quality");
|
||||
if (!q) q = "balanced";
|
||||
values.push(q);
|
||||
let s = handler.xcall("get_view_style");
|
||||
if (!s) s = "original";
|
||||
values.push(s);
|
||||
for (let el of $$("menu#display-options>li")) {
|
||||
el.classList.toggle("selected", values.indexOf(el.id) >= 0);
|
||||
}
|
||||
for (let id of ["show-remote-cursor", "disable-audio", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) {
|
||||
let el = $('#' + id); // TEST
|
||||
if (el) {
|
||||
el.classList.toggle("selected", handler.xcall("get_toggle_option",id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_osx) {
|
||||
$("header").content(<Header />);
|
||||
$("header").attributes["role"] = "window-caption"; // TODO
|
||||
} else {
|
||||
if (handler.is_file_transfer || handler.is_port_forward) {
|
||||
$("caption").content(<Header />);
|
||||
} else {
|
||||
$("div.window-toolbar").content(<Header />);
|
||||
}
|
||||
setWindowButontsAndIcon();
|
||||
}
|
||||
|
||||
if (!(handler.is_file_transfer || handler.is_port_forward)) {
|
||||
$("header").style.setProperty("height","32px");
|
||||
if (!is_osx) {
|
||||
$("div.window-icon").style.setProperty("size","32px");
|
||||
}
|
||||
}
|
||||
|
||||
handler.updatePi = function(v) {
|
||||
pi = v;
|
||||
header.componentUpdate();
|
||||
if (handler.is_port_forward) {
|
||||
view.state = Window.WINDOW_MINIMIZED;
|
||||
}
|
||||
}
|
||||
|
||||
handler.switchDisplay = function(i) {
|
||||
pi.current_display = i;
|
||||
header.componentUpdate();
|
||||
}
|
||||
|
||||
function updateWindowToolbarPosition() {
|
||||
if (is_osx) return;
|
||||
setTimeout(function() {
|
||||
let el = $("div.window-toolbar");
|
||||
let w1 = el.state.box("width", "border"); // TEST
|
||||
let w2 = $("header").state.box("width", "border");
|
||||
let x = (w2 - w1) / 2;
|
||||
el.style.setProperty("left",x + "px");
|
||||
el.style.setProperty("display","block")
|
||||
},1);
|
||||
}
|
||||
|
||||
view.onsizechange = function() {
|
||||
// ensure size is done, so add timer
|
||||
setTimeout(function() {
|
||||
updateWindowToolbarPosition();
|
||||
adaptDisplay();
|
||||
},1);
|
||||
};
|
||||
|
||||
handler.newMessage = function(text) {
|
||||
chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()});
|
||||
startChat();
|
||||
}
|
||||
|
||||
function sendMsg(text) {
|
||||
chat_msgs.push({text: text, name: "me", time: getNowStr()});
|
||||
handler.xcall("send_chat",text);
|
||||
if (chatbox) chatbox.refresh();
|
||||
}
|
||||
|
||||
var chatbox;
|
||||
function startChat() {
|
||||
if (chatbox) {
|
||||
chatbox.state = Window.WINDOW_SHOWN; // TODO TEST el.state
|
||||
chatbox.refresh(); // TODO el.refresh
|
||||
return;
|
||||
}
|
||||
let icon = handler.xcall("get_icon");
|
||||
let [sx, sy, sw, sh] = view.screenBox("workarea", "rectw"); // TEST
|
||||
let w = 300;
|
||||
let h = 400;
|
||||
let x = (sx + sw - w) / 2;
|
||||
let y = sy + 80;
|
||||
let params = {
|
||||
type: Window.FRAME_WINDOW,
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h,
|
||||
client: true,
|
||||
parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon },
|
||||
caption: get_id(),
|
||||
};
|
||||
let html = handler.xcall("get_chatbox");
|
||||
if (html) params.html = html;
|
||||
else params.url = document.url("chatbox.html");
|
||||
chatbox = view.window(params); // TEST
|
||||
}
|
||||
|
||||
handler.setConnectionType = function(secured, direct) {
|
||||
// TEST
|
||||
header.componentUpdate({
|
||||
secure_connection: secured,
|
||||
direct_connection: direct,
|
||||
});
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
var pi = handler.get_default_pi(); // peer information
|
||||
var chat_msgs = [];
|
||||
|
||||
var svg_fullscreen = <svg viewBox="0 0 357 357">
|
||||
<path d="M51,229.5H0V357h127.5v-51H51V229.5z M0,127.5h51V51h76.5V0H0V127.5z M306,306h-76.5v51H357V229.5h-51V306z M229.5,0v51 H306v76.5h51V0H229.5z"/>
|
||||
</svg>;
|
||||
var svg_action = <svg viewBox="-91 0 512 512"><path d="M315 211H191L298 22a15 15 0 00-13-22H105c-6 0-12 4-14 10L1 281a15 15 0 0014 20h127L61 491a15 15 0 0025 16l240-271a15 15 0 00-11-25z"/></svg>;
|
||||
var svg_display = <svg viewBox="0 0 640 512">
|
||||
<path d="M592 0H48A48 48 0 0 0 0 48v320a48 48 0 0 0 48 48h240v32H112a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H352v-32h240a48 48 0 0 0 48-48V48a48 48 0 0 0-48-48zm-16 352H64V64h512z"/>
|
||||
</svg>;
|
||||
var svg_secure = <svg viewBox="0 0 347.97 347.97">
|
||||
<path fill="#3F7D46" d="m317.31 54.367c-59.376 0-104.86-16.964-143.33-54.367-38.461 37.403-83.947 54.367-143.32 54.367 0 97.405-20.155 236.94 143.32 293.6 163.48-56.666 143.33-196.2 143.33-293.6zm-155.2 171.41-47.749-47.756 21.379-21.378 26.37 26.376 50.121-50.122 21.378 21.378-71.499 71.502z"/>
|
||||
</svg>;
|
||||
var svg_insecure = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M238.802 115.023l-111.573 114.68-8.6-8.367L230.2 106.656z"/><path d="M125.559 108.093l114.68 111.572-8.368 8.601-114.68-111.572z"/></g></svg>;
|
||||
var svg_insecure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z"/></g></svg>;
|
||||
var svg_secure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="#3f7d46" stroke="#3f7d46" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z" fill="#fff"/></g></svg>;
|
||||
|
||||
var cur_window_state = view.windowState;
|
||||
function check_state_change() {
|
||||
if (view.windowState != cur_window_state) {
|
||||
stateChanged();
|
||||
}
|
||||
self.timer(30ms, check_state_change);
|
||||
}
|
||||
|
||||
if (is_linux) {
|
||||
check_state_change();
|
||||
} else {
|
||||
view << event statechange {
|
||||
stateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
function stateChanged() {
|
||||
stdout.println('state changed from ' + cur_window_state + ' -> ' + view.windowState);
|
||||
cur_window_state = view.windowState;
|
||||
adjustBorder();
|
||||
adaptDisplay();
|
||||
if (cur_window_state != View.WINDOW_MINIMIZED) {
|
||||
view.focus = handler; // to make focus away from restore/maximize button, so that enter key work
|
||||
}
|
||||
var fs = view.windowState == View.WINDOW_FULL_SCREEN;
|
||||
var el = $(#fullscreen);
|
||||
if (el) el.attributes.toggleClass("active", fs);
|
||||
el = $(#maximize);
|
||||
if (el) {
|
||||
el.state.disabled = fs;
|
||||
}
|
||||
if (fs) {
|
||||
$(header).style.set {
|
||||
display: "none",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var header;
|
||||
var old_window_state = View.WINDOW_SHOWN;
|
||||
var input_blocked;
|
||||
|
||||
class Header: Reactor.Component {
|
||||
function this(params) {
|
||||
header = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var icon_conn;
|
||||
var title_conn;
|
||||
if (this.secure_connection && this.direct_connection) {
|
||||
icon_conn = svg_secure;
|
||||
title_conn = "Direct and secure connection";
|
||||
} else if (this.secure_connection && !this.direct_connection) {
|
||||
icon_conn = svg_secure_relay;
|
||||
title_conn = "Relayed and secure connection";
|
||||
} else if (!this.secure_connection && this.direct_connection) {
|
||||
icon_conn = svg_insecure;
|
||||
title_conn = "Direct and insecure connection";
|
||||
} else {
|
||||
icon_conn = svg_insecure_relay;
|
||||
title_conn = "Relayed and insecure connection";
|
||||
}
|
||||
var title = handler.get_id();
|
||||
if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")";
|
||||
if ((pi.displays || []).length == 0) {
|
||||
return <div .ellipsis style="size:*;text-align:center;margin:*;">{title}</div>;
|
||||
}
|
||||
var screens = pi.displays.map(function(d, i) {
|
||||
return <div #screen class={pi.current_display == i ? "current" : ""}>
|
||||
{i+1}
|
||||
</div>;
|
||||
});
|
||||
updateWindowToolbarPosition();
|
||||
var style = "flow: horizontal;";
|
||||
if (is_osx) style += "margin: *";
|
||||
self.timer(1ms, toggleMenuState);
|
||||
return <div style={style}>
|
||||
{is_osx || is_xfce ? "" : <span #fullscreen>{svg_fullscreen}</span>}
|
||||
<div #screens>
|
||||
<span #secure title={title_conn}>{icon_conn}</span>
|
||||
<div .remote-id>{handler.get_id()}</div>
|
||||
<div style="flow:horizontal;border-spacing: 0.5em;">{screens}</div>
|
||||
{this.renderGlobalScreens()}
|
||||
</div>
|
||||
<span #chat>{svg_chat}</span>
|
||||
<span #action>{svg_action}</span>
|
||||
<span #display>{svg_display}</span>
|
||||
{this.renderDisplayPop()}
|
||||
{this.renderActionPop()}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderDisplayPop() {
|
||||
return <popup>
|
||||
<menu.context #display-options>
|
||||
<li #adjust-window style="display:none">Adjust Window</li>
|
||||
<div #adjust-window .separator style="display:none"/>
|
||||
<li #original type="view-style"><span>{svg_checkmark}</span>Original</li>
|
||||
<li #shrink type="view-style"><span>{svg_checkmark}</span>Shrink</li>
|
||||
<li #stretch type="view-style"><span>{svg_checkmark}</span>Stretch</li>
|
||||
<div .separator />
|
||||
<li #best type="image-quality"><span>{svg_checkmark}</span>Good image quality</li>
|
||||
<li #balanced type="image-quality"><span>{svg_checkmark}</span>Balanced</li>
|
||||
<li #low type="image-quality"><span>{svg_checkmark}</span>Optimize reaction time</li>
|
||||
<li #custom type="image-quality"><span>{svg_checkmark}</span>Custom</li>
|
||||
<div .separator />
|
||||
<li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>Show remote cursor</li>
|
||||
{audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>Mute</li> : ""}
|
||||
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>Disable clipboard</li> : ""}
|
||||
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>Lock after session end</li> : ""}
|
||||
{false && pi.platform == "Windows" ? <li #privacy-mode .toggle-option><span>{svg_checkmark}</span>Privacy mode</li> : ""}
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
||||
function renderActionPop() {
|
||||
return <popup>
|
||||
<menu.context #action-options>
|
||||
<li #transfer-file>Transfer File</li>
|
||||
<li #tunnel>TCP Tunneling</li>
|
||||
<div .separator />
|
||||
{keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ? <li #ctrl-alt-del>Insert Ctrl + Alt + Del</li> : ""}
|
||||
<li #ctrl-space>Insert Ctrl + Space</li>
|
||||
<li #alt-tab>Insert Alt + Tab</li>
|
||||
{false && <li #super-x>Insert Win/Super + ...</li>}
|
||||
<div .separator />
|
||||
{keyboard_enabled ? <li #lock-screen>Insert Lock</li> : ""}
|
||||
{false && pi.platform == "Windows" ? <li #block-input>Block user input </li> : ""}
|
||||
{handler.support_refresh() ? <li #refresh>Refresh</li> : ""}
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
||||
function renderGlobalScreens() {
|
||||
if (pi.displays.length < 2) return "";
|
||||
var x0 = 9999999;
|
||||
var y0 = 9999999;
|
||||
var x = -9999999;
|
||||
var y = -9999999;
|
||||
pi.displays.map(function(d, i) {
|
||||
if (d.x < x0) x0 = d.x;
|
||||
if (d.y < y0) y0 = d.y;
|
||||
var dx = d.x + d.width;
|
||||
if (dx > x) x = dx;
|
||||
var dy = d.y + d.height;
|
||||
if (dy > y) y = dy;
|
||||
});
|
||||
var w = x - x0;
|
||||
var h = y - y0;
|
||||
var scale = 16. / h;
|
||||
var screens = pi.displays.map(function(d, i) {
|
||||
var min_wh = d.width > d.height ? d.height : d.width;
|
||||
var fs = min_wh * 0.9 * scale;
|
||||
var style = "width:" + (d.width * scale) + "px;" +
|
||||
"height:" + (d.height * scale) + "px;" +
|
||||
"left:" + ((d.x - x0) * scale) + "px;" +
|
||||
"top:" + ((d.y - y0) * scale) + "px;" +
|
||||
"font-size:" + fs + "px;";
|
||||
if (is_osx) {
|
||||
style += "line-height:" + fs + "px;";
|
||||
}
|
||||
return <div style={style} class={pi.current_display == i ? "current" : ""}>{i+1}</div>;
|
||||
});
|
||||
|
||||
var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;";
|
||||
return <div #global-screens style={style}>
|
||||
{screens}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#fullscreen) (_, el) {
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
if (old_window_state == View.WINDOW_MAXIMIZED) {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
}
|
||||
view.windowState = old_window_state;
|
||||
} else {
|
||||
old_window_state = view.windowState;
|
||||
if (view.windowState == View.WINDOW_MAXIMIZED) {
|
||||
view.windowState = View.WINDOW_SHOWN;
|
||||
}
|
||||
view.windowState = View.WINDOW_FULL_SCREEN;
|
||||
if (is_linux) { self.timer(150ms, function() { view.windowState = View.WINDOW_FULL_SCREEN; }); }
|
||||
}
|
||||
}
|
||||
|
||||
event click $(#chat) {
|
||||
startChat();
|
||||
}
|
||||
|
||||
event click $(#action) (_, me) {
|
||||
var menu = $(menu#action-options);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
event click $(#display) (_, me) {
|
||||
var menu = $(menu#display-options);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
event click $(#screen) (_, me) {
|
||||
if (pi.current_display == me.index) return;
|
||||
handler.switch_display(me.index);
|
||||
}
|
||||
|
||||
event click $(#transfer-file) {
|
||||
handler.transfer_file();
|
||||
}
|
||||
|
||||
event click $(#tunnel) {
|
||||
handler.tunnel();
|
||||
}
|
||||
|
||||
event click $(#ctrl-alt-del) {
|
||||
handler.ctrl_alt_del();
|
||||
}
|
||||
|
||||
event click $(#alt-tab) {
|
||||
handler.alt_tab();
|
||||
}
|
||||
|
||||
event click $(#ctrl-space) {
|
||||
handler.ctrl_space();
|
||||
}
|
||||
|
||||
event click $(#super-x) {
|
||||
handler.super_x();
|
||||
}
|
||||
|
||||
event click $(#lock-screen) {
|
||||
handler.lock_screen();
|
||||
}
|
||||
|
||||
event click $(#refresh) {
|
||||
handler.refresh_video();
|
||||
}
|
||||
|
||||
event click $(#block-input) {
|
||||
if (!input_blocked) {
|
||||
handler.toggle_option("block-input");
|
||||
input_blocked = true;
|
||||
$(#block-input).text = "Unblock user input";
|
||||
} else {
|
||||
handler.toggle_option("unblock-input");
|
||||
input_blocked = false;
|
||||
$(#block-input).text = "Block user input";
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#display-options>li) (_, me) {
|
||||
if (me.id == "custom") {
|
||||
handle_custom_image_quality();
|
||||
} else if (me.attributes.hasClass("toggle-option")) {
|
||||
handler.toggle_option(me.id);
|
||||
toggleMenuState();
|
||||
} else if (!me.attributes.hasClass("selected")) {
|
||||
var type = me.attributes["type"];
|
||||
if (type == "image-quality") {
|
||||
handler.save_image_quality(me.id);
|
||||
} else if (type == "view-style") {
|
||||
handler.save_view_style(me.id);
|
||||
adaptDisplay();
|
||||
}
|
||||
toggleMenuState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handle_custom_image_quality() {
|
||||
var tmp = handler.get_custom_image_quality();
|
||||
var bitrate0 = tmp[0] || 50;
|
||||
var quantizer0 = tmp.length > 1 ? tmp[1] : 100;
|
||||
handler.msgbox("custom", "Custom Image Quality", "<div .form> \
|
||||
<div><input type=\"hslider\" style=\"width: 66%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate0 + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% bitrate</div> \
|
||||
<div><input type=\"hslider\" style=\"width: 66%\" name=\"quantizer\" max=\"100\" min=\"0\" value=\"" + quantizer0 + "\"/ buddy=\"quantizer-buddy\"><b #quantizer-buddy>x</b>% quantizer</div> \
|
||||
</div>", function(res=null) {
|
||||
if (!res) return;
|
||||
if (!res.bitrate) return;
|
||||
handler.save_custom_image_quality(res.bitrate, res.quantizer);
|
||||
toggleMenuState();
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
var values = [];
|
||||
var q = handler.get_image_quality();
|
||||
if (!q) q = "balanced";
|
||||
values.push(q);
|
||||
var s = handler.get_view_style();
|
||||
if (!s) s = "original";
|
||||
values.push(s);
|
||||
for (var el in $$(menu#display-options>li)) {
|
||||
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
|
||||
}
|
||||
for (var id in ["show-remote-cursor", "disable-audio", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) {
|
||||
var el = self.select('#' + id);
|
||||
if (el) {
|
||||
el.attributes.toggleClass("selected", handler.get_toggle_option(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_osx) {
|
||||
$(header).content(<Header />);
|
||||
$(header).attributes["role"] = "window-caption";
|
||||
} else {
|
||||
if (is_file_transfer || is_port_forward) {
|
||||
$(caption).content(<Header />);
|
||||
} else {
|
||||
$(div.window-toolbar).content(<Header />);
|
||||
}
|
||||
setWindowButontsAndIcon();
|
||||
}
|
||||
|
||||
if (!(is_file_transfer || is_port_forward)) {
|
||||
$(header).style.set {
|
||||
height: "32px",
|
||||
};
|
||||
if (!is_osx) {
|
||||
$(div.window-icon).style.set {
|
||||
size: "32px",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
handler.updatePi = function(v) {
|
||||
pi = v;
|
||||
header.update();
|
||||
if (is_port_forward) {
|
||||
view.windowState = View.WINDOW_MINIMIZED;
|
||||
}
|
||||
}
|
||||
|
||||
handler.switchDisplay = function(i) {
|
||||
pi.current_display = i;
|
||||
header.update();
|
||||
}
|
||||
|
||||
function updateWindowToolbarPosition() {
|
||||
if (is_osx) return;
|
||||
self.timer(1ms, function() {
|
||||
var el = $(div.window-toolbar);
|
||||
var w1 = el.box(#width, #border);
|
||||
var w2 = $(header).box(#width, #border);
|
||||
var x = (w2 - w1) / 2;
|
||||
el.style.set {
|
||||
left: x + "px",
|
||||
display: "block",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
view.on("size", function() {
|
||||
// ensure size is done, so add timer
|
||||
self.timer(1ms, function() {
|
||||
updateWindowToolbarPosition();
|
||||
adaptDisplay();
|
||||
});
|
||||
});
|
||||
|
||||
handler.newMessage = function(text) {
|
||||
chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()});
|
||||
startChat();
|
||||
}
|
||||
|
||||
function sendMsg(text) {
|
||||
chat_msgs.push({text: text, name: "me", time: getNowStr()});
|
||||
handler.send_chat(text);
|
||||
if (chatbox) chatbox.refresh();
|
||||
}
|
||||
|
||||
var chatbox;
|
||||
function startChat() {
|
||||
if (chatbox) {
|
||||
chatbox.windowState = View.WINDOW_SHOWN;
|
||||
chatbox.refresh();
|
||||
return;
|
||||
}
|
||||
var icon = handler.get_icon();
|
||||
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
|
||||
var w = 300;
|
||||
var h = 400;
|
||||
var x = (sx + sw - w) / 2;
|
||||
var y = sy + 80;
|
||||
var params = {
|
||||
type: View.FRAME_WINDOW,
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h,
|
||||
client: true,
|
||||
parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon },
|
||||
caption: handler.get_id(),
|
||||
};
|
||||
var html = handler.get_chatbox();
|
||||
if (html) params.html = html;
|
||||
else params.url = self.url("chatbox.html");
|
||||
chatbox = view.window(params);
|
||||
}
|
||||
|
||||
handler.setConnectionType = function(secured, direct) {
|
||||
header.update({
|
||||
secure_connection: secured,
|
||||
direct_connection: direct,
|
||||
});
|
||||
}
|
||||
127
src/ui/index.css
127
src/ui/index.css
@@ -39,6 +39,77 @@ body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
div.sessions-bar {
|
||||
color: color(light-text);
|
||||
padding-top: 0.5em;
|
||||
border-top: color(border) solid 1px;
|
||||
margin-bottom: 1em;
|
||||
position: relative;
|
||||
flow: horizontal;
|
||||
}
|
||||
|
||||
div.sessions-tab span {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
div.sessions-tab svg {
|
||||
size: 14px;
|
||||
}
|
||||
|
||||
div.sessions-tab span.active {
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.search-id {
|
||||
width: 120px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.search-id input {
|
||||
font-size: 1em;
|
||||
height: 20px;
|
||||
border: none;
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
div.search-id span {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
padding: 6px;
|
||||
color: color(border);
|
||||
}
|
||||
|
||||
div.search-id svg {
|
||||
size: 14px;
|
||||
}
|
||||
|
||||
span.search-icon {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
span.clear-input {
|
||||
display: none;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
div.search-id:hover span.clear-input {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span.clear-input:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.your-desktop {
|
||||
border-spacing: 0.5em;
|
||||
border-left: color(accent) solid 2px;
|
||||
@@ -132,13 +203,6 @@ div.recent-sessions-content {
|
||||
flow: horizontal-flow;
|
||||
}
|
||||
|
||||
div.recent-sessions-title {
|
||||
color: color(light-text);
|
||||
padding-top: 0.5em;
|
||||
border-top: color(border) solid 1px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
div.remote-session {
|
||||
border-radius: 1em;
|
||||
height: 140px;
|
||||
@@ -148,7 +212,7 @@ div.remote-session {
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.remote-session:hover {
|
||||
div.remote-session:hover, div.remote-session-list:hover {
|
||||
outline: color(button) solid 2px -2px;
|
||||
}
|
||||
|
||||
@@ -176,6 +240,41 @@ div.remote-session .platform svg {
|
||||
background: none;
|
||||
}
|
||||
|
||||
div.remote-session-list {
|
||||
background: white;
|
||||
width: 220px;
|
||||
flow: horizontal;
|
||||
}
|
||||
|
||||
div.remote-session-list .platform {
|
||||
size: 42px;
|
||||
}
|
||||
|
||||
div.remote-session-list .platform svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: none;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
div.remote-session-list .name {
|
||||
size: *;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
div.remote-session-list .name >div {
|
||||
margin-top: *;
|
||||
margin-bottom: *;
|
||||
width: *;
|
||||
}
|
||||
|
||||
div.remote-session-list .name .username {
|
||||
margin-top: 3px;
|
||||
font-size: 0.8em;
|
||||
color: color(lighter-text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.remote-session .text {
|
||||
background: white;
|
||||
position: absolute;
|
||||
@@ -200,20 +299,22 @@ svg#menu {
|
||||
color: color(light-text);
|
||||
}
|
||||
|
||||
svg#menu:active {
|
||||
.remote-session-list svg#menu {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg#menu:hover {
|
||||
color: black;
|
||||
border-radius: 1em;
|
||||
background: color(gray-bg);
|
||||
}
|
||||
|
||||
svg#edit:active {
|
||||
opacity: 0.5;
|
||||
svg#edit:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
svg#edit {
|
||||
display: inline-block;
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.install-me, div.trust-me {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@import url(common.css);
|
||||
@import url(index.css);
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<script type="module" src="index.js"></script>
|
||||
<!-- <script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "ab.tis";
|
||||
include "index.tis";
|
||||
</script>
|
||||
</script> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
730
src/ui/index.js
Normal file
730
src/ui/index.js
Normal file
@@ -0,0 +1,730 @@
|
||||
import { is_osx,view,OS,handler,translate,msgbox,is_win,svg_checkmark,svg_edit,isReasonableSize,centerize,svg_eye, PasswordComponent } from "./common";
|
||||
import { SearchBar,SessionStyle,SessionList, MultipleSessions } from "./ab.js";
|
||||
import {$} from "@sciter"; //TEST $$ import
|
||||
|
||||
if (is_osx) view.blurBehind = "light";
|
||||
console.log("current platform:", OS);
|
||||
console.log("wayland",handler.xcall("is_login_wayland"));
|
||||
// html min-width, min-height not working on mac, below works for all
|
||||
view.minSize = [500, 300]; // TODO not work on ubuntu
|
||||
|
||||
export var app; // 注意判空
|
||||
var tmp = handler.xcall("get_connect_status");
|
||||
var connect_status = tmp[0];
|
||||
var service_stopped = false;
|
||||
var software_update_url = "";
|
||||
var key_confirmed = tmp[1];
|
||||
var system_error = "";
|
||||
|
||||
export const svg_menu = <svg id="menu" viewBox="0 0 512 512">
|
||||
<circle cx="256" cy="256" r="64"/>
|
||||
<circle cx="256" cy="448" r="64"/>
|
||||
<circle cx="256" cy="64" r="64"/>
|
||||
</svg>;
|
||||
|
||||
var my_id = "";
|
||||
function get_id() {
|
||||
my_id = handler.xcall("get_id");
|
||||
return my_id;
|
||||
}
|
||||
|
||||
class ConnectStatus extends Element {
|
||||
render() {
|
||||
return(<div class="connect-status">
|
||||
<span class={"connect-status-icon connect-status" + (service_stopped ? 0 : connect_status)} />
|
||||
{this.getConnectStatusStr()}
|
||||
{service_stopped ? <span class="link" id="start-service">{translate('Start Service')}</span> : ""}
|
||||
</div>);
|
||||
}
|
||||
|
||||
getConnectStatusStr() {
|
||||
if (service_stopped) {
|
||||
return translate("Service is not running");
|
||||
} else if (connect_status == -1) {
|
||||
return translate('not_ready_status');
|
||||
} else if (connect_status == 0) {
|
||||
return translate('connecting_status');
|
||||
}
|
||||
return translate("Ready");
|
||||
}
|
||||
|
||||
["on click at #start-service"]() {
|
||||
handler.xcall("set_option","stop-service", "");
|
||||
}
|
||||
}
|
||||
|
||||
export function createNewConnect(id, type) {
|
||||
id = id.replace(/\s/g, "");
|
||||
app.remote_id.value = formatId(id);
|
||||
if (!id) return;
|
||||
if (id == my_id) {
|
||||
msgbox("custom-error", "Error", "You cannot connect to your own computer");
|
||||
return;
|
||||
}
|
||||
handler.xcall("set_remote_id",id);
|
||||
handler.xcall("new_remote",id, type);
|
||||
}
|
||||
|
||||
var direct_server;
|
||||
class DirectServer extends Element {
|
||||
this() {
|
||||
direct_server = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
var text = translate("Enable Direct IP Access");
|
||||
var cls = handler.xcall("get_option", "direct-server") == "Y" ? "selected" : "line-through";
|
||||
return <li class={cls}><span>{svg_checkmark}</span>{text}</li>;
|
||||
}
|
||||
|
||||
onClick() {
|
||||
handler.xcall("set_option", "direct-server", handler.xcall("get_option", "direct-server") == "Y" ? "" : "Y");
|
||||
this.componentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
var myIdMenu;
|
||||
var audioInputMenu;
|
||||
class AudioInputs extends Element {
|
||||
this() {
|
||||
audioInputMenu = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
// TODO this.show
|
||||
if (!this.show) return <li />;
|
||||
let inputs = handler.xcall("get_sound_inputs");
|
||||
if (is_win) inputs = ["System Sound"].concat(inputs);
|
||||
if (!inputs.length) return <div/>;
|
||||
inputs = ["Mute"].concat(inputs);
|
||||
setTimeout(()=>this.toggleMenuState(),1);
|
||||
return (<li>{translate('Audio Input')}
|
||||
<menu id="audio-input" key={inputs.length}>
|
||||
{inputs.map((name)=><li id={name}><span>{svg_checkmark}</span>{translate(name)}</li>)}
|
||||
</menu>
|
||||
</li>);
|
||||
}
|
||||
|
||||
get_default() {
|
||||
if (is_win) return "System Sound";
|
||||
return "";
|
||||
}
|
||||
|
||||
get_value() {
|
||||
return handler.xcall("get_option","audio-input") || this.get_default();
|
||||
}
|
||||
|
||||
toggleMenuState() {
|
||||
let v = this.get_value();
|
||||
for (let el of this.$$("menu#audio-input>li")) {
|
||||
let selected = el.id == v;
|
||||
el.classList.toggle("selected", selected);
|
||||
}
|
||||
}
|
||||
|
||||
["on click at menu#audio-input>li"](_, me) {
|
||||
let v = me.id;
|
||||
if (v == this.get_value()) return;
|
||||
if (v == this.get_default()) v = "";
|
||||
handler.xcall("set_option","audio-input", v);
|
||||
this.toggleMenuState();
|
||||
}
|
||||
}
|
||||
|
||||
class MyIdMenu extends Element {
|
||||
this() {
|
||||
myIdMenu = this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div id="myid">
|
||||
{this.renderPop()}
|
||||
ID{svg_menu}
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderPop() {
|
||||
return (<popup>
|
||||
<menu class="context" id="config-options">
|
||||
<li id="enable-keyboard"><span>{svg_checkmark}</span>{translate('Enable Keyboard/Mouse')}</li>
|
||||
<li id="enable-clipboard"><span>{svg_checkmark}</span>{translate('Enable Clipboard')}</li>
|
||||
<li id="enable-file-transfer"><span>{svg_checkmark}</span>{translate('Enable File Transfer')}</li>
|
||||
<li id="enable-tunnel"><span>{svg_checkmark}</span>{translate('Enable TCP Tunneling')}</li>
|
||||
<AudioInputs />
|
||||
<div class="separator" />
|
||||
<li id="whitelist" title={translate('whitelist_tip')}>{translate('IP Whitelisting')}</li>
|
||||
<li id="custom-server">{translate('ID/Relay Server')}</li>
|
||||
<li id="socks5-server">{translate('Socks5 Proxy')}</li>
|
||||
<div class="separator" />
|
||||
<li id="stop-service" class={service_stopped ? "line-through" : "selected"}><span>{svg_checkmark}</span>{translate("Enable Service")}</li>
|
||||
<DirectServer />
|
||||
<div class="separator" />
|
||||
<li id="about">{translate('About')} {" "} {handler.xcall("get_app_name")}</li>
|
||||
</menu>
|
||||
</popup>);
|
||||
}
|
||||
|
||||
|
||||
["on click at svg#menu"](_, me) {
|
||||
|
||||
audioInputMenu.componentUpdate({ show: true });
|
||||
this.toggleMenuState();
|
||||
let menu = this.$("menu#config-options");
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
toggleMenuState() {
|
||||
for (let el of this.$$("menu#config-options>li")) {
|
||||
if (el.id && el.id.indexOf("enable-") == 0) {
|
||||
let enabled = handler.xcall("get_option",el.id) != "N";
|
||||
console.log(el.id,enabled)
|
||||
el.classList.toggle("selected", enabled);
|
||||
el.classList.toggle("line-through", !enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
["on click at menu#config-options>li"] (_, me) {
|
||||
if (me.id && me.id.indexOf("enable-") == 0) {
|
||||
handler.xcall("set_option",me.id, handler.xcall("get_option",me.id) == "N" ? "" : "N");
|
||||
}
|
||||
if (me.id == "whitelist") {
|
||||
let old_value = handler.xcall("get_option","whitelist").split(",").join("\n");
|
||||
msgbox("custom-whitelist", translate("IP Whitelisting"), "<div class='form'> \
|
||||
<div>" + translate("whitelist_sep") + "</div> \
|
||||
<textarea spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; width:*; height: 160px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
|
||||
</div> \
|
||||
",
|
||||
function(res=null) {
|
||||
if (!res) return;
|
||||
let value = (res.text || "").trim();
|
||||
if (value) {
|
||||
let values = value.split(/[\s,;\n]+/g);
|
||||
for (let ip in values) {
|
||||
if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
return translate("Invalid IP") + ": " + ip;
|
||||
}
|
||||
}
|
||||
value = values.join("\n");
|
||||
}
|
||||
if (value == old_value) return;
|
||||
console.log("whitelist updated");
|
||||
handler.xcall("set_option","whitelist", value.replace("\n", ","));
|
||||
}, 300);
|
||||
} else if (me.id == "custom-server") {
|
||||
let configOptions = handler.xcall("get_options");
|
||||
let old_relay = configOptions["relay-server"] || "";
|
||||
let old_id = configOptions["custom-rendezvous-server"] || "";
|
||||
msgbox("custom-server", "ID/Relay Server", "<div class='form'> \
|
||||
<div><span style='width: 100px; display:inline-block'>" + translate("ID Server") + ": </span><input .outline-focus style='width: 250px' name='id' value='" + old_id + "' /></div> \
|
||||
<div><span style='width: 100px; display:inline-block'>" + translate("Relay Server") + ": </span><input style='width: 250px' name='relay' value='" + old_relay + "' /></div> \
|
||||
</div> \
|
||||
",
|
||||
function(res=null) {
|
||||
if (!res) return;
|
||||
let id = (res.id || "").trim();
|
||||
let relay = (res.relay || "").trim();
|
||||
if (id == old_id && relay == old_relay) return;
|
||||
if (id) {
|
||||
let err = handler.xcall("test_if_valid_server",id);
|
||||
if (err) return translate("ID Server") + ": " + err;
|
||||
}
|
||||
if (relay) {
|
||||
let err = handler.xcall("test_if_valid_server",relay);
|
||||
if (err) return translate("Relay Server") + ": " + err;
|
||||
}
|
||||
configOptions["custom-rendezvous-server"] = id;
|
||||
configOptions["relay-server"] = relay;
|
||||
handler.xcall("set_options",configOptions);
|
||||
}, 240);
|
||||
} else if (me.id == "socks5-server") {
|
||||
var socks5 = handler.xcall("get_socks") || {};
|
||||
var old_proxy = socks5[0] || "";
|
||||
var old_username = socks5[1] || "";
|
||||
var old_password = socks5[2] || "";
|
||||
msgbox("custom-server", "Socks5 Proxy", <div class="form set-password">
|
||||
<div><span>{translate("Hostname")}</span><input class="outline-focus" style='width: *' name='proxy' value={old_proxy} /></div>
|
||||
<div><span>{translate("Username")}</span><input style='width: *' name='username' value={old_username} /></div>
|
||||
<div><span>{translate("Password")}</span><PasswordComponent value={old_password} /></div>
|
||||
</div>
|
||||
, function(res=null) {
|
||||
if (!res) return;
|
||||
var proxy = (res.proxy || "").trim();
|
||||
var username = (res.username || "").trim();
|
||||
var password = (res.password || "").trim();
|
||||
if (proxy == old_proxy && username == old_username && password == old_password) return;
|
||||
if (proxy) {
|
||||
var err = handler.xcall("test_if_valid_server", proxy);
|
||||
if (err) return translate("Server") + ": " + err;
|
||||
}
|
||||
handler.xcall("set_socks", proxy, username, password);
|
||||
}, 240);
|
||||
} else if (me.id == "stop-service") {
|
||||
handler.xcall("set_option","stop-service", service_stopped ? "" : "Y");
|
||||
} else if (me.id == "about") {
|
||||
let name = handler.xcall("get_app_name");
|
||||
msgbox("custom-nocancel-nook-hasclose", "About " + name, "<div style='line-height: 2em'> \
|
||||
<div>Version: " + handler.xcall("get_version") + " \
|
||||
<div class='link custom-event' url='http://rustdesk.com/privacy'>Privacy Statement</div> \
|
||||
<div class='link custom-event' url='http://rustdesk.com'>Website</div> \
|
||||
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright © 2020 CarrieZ Studio \
|
||||
<br /> Author: Carrie \
|
||||
<p style='font-weight: bold'>Made with heart in this chaotic world!</p>\
|
||||
</div>\
|
||||
</div>",
|
||||
function(el) {
|
||||
if (el && el.attributes) {
|
||||
handler.xcall("open_url",el.attributes['url']);
|
||||
};
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Element{
|
||||
remote_id;
|
||||
recent_sessions;
|
||||
connect_status;
|
||||
this() {
|
||||
app = this;
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.remote_id = this.$("#ID");
|
||||
this.multipleSessions = this.$("#multipleSessions");
|
||||
this.connect_status = this.$("#ConnectStatus");
|
||||
}
|
||||
|
||||
render() {
|
||||
let is_can_screen_recording = handler.xcall("is_can_screen_recording",false);
|
||||
return(<div class="app">
|
||||
<popup>
|
||||
<menu class="context" id="edit-password-context">
|
||||
<li id="refresh-password">{translate('Refresh random password')}</li>
|
||||
<li id="set-password">{translate('Set your own password')}</li>
|
||||
</menu>
|
||||
</popup>
|
||||
<div class="left-pane">
|
||||
<div>
|
||||
<div class="title">{translate('Your Desktop')}</div>
|
||||
<div class="lighter-text">{translate('desk_tip')}</div>
|
||||
<div class="your-desktop">
|
||||
<MyIdMenu />
|
||||
{key_confirmed ? <input type="text" readonly value={formatId(get_id())}/> : translate("Generating ...")}
|
||||
</div>
|
||||
<div class="your-desktop">
|
||||
<div>{translate('Password')}</div>
|
||||
<Password />
|
||||
</div>
|
||||
</div>
|
||||
{handler.xcall("is_installed") ? "": <InstallMe />}
|
||||
{handler.xcall("is_installed") && software_update_url ? <UpdateMe /> : ""}
|
||||
{handler.xcall("is_installed") && !software_update_url && handler.xcall("is_installed_lower_version") ? <UpgradeMe /> : ""}
|
||||
{is_can_screen_recording ? "": <CanScreenRecording />}
|
||||
{is_can_screen_recording && !handler.xcall("is_process_trusted",false) ? <TrustMe /> : ""}
|
||||
{system_error ? <SystemError /> : ""}
|
||||
{!system_error && handler.xcall("is_login_wayland") && !handler.xcall("current_is_wayland") ? <FixWayland /> : ""}
|
||||
{!system_error && handler.xcall("current_is_wayland") ? <ModifyDefaultLogin /> : ""}
|
||||
</div>
|
||||
<div class="right-pane">
|
||||
<div class="right-content">
|
||||
<div class="card-connect">
|
||||
<div class="title">{translate('Control Remote Desktop')}</div>
|
||||
<ID id="ID" />
|
||||
<div class="right-buttons">
|
||||
<button class="button outline" id="file-transfer">{translate('Transfer File')}</button>
|
||||
<button class="button" id="connect">{translate('Connect')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<MultipleSessions id="multipleSessions" />
|
||||
</div>
|
||||
<ConnectStatus id="ConnectStatus" />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at button#connect"](){
|
||||
this.newRemote("connect");
|
||||
}
|
||||
|
||||
["on click at button#file-transfer"]() {
|
||||
this.newRemote("file-transfer");
|
||||
}
|
||||
|
||||
["on keydown"](evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
// TODO TEST Windows/Mac
|
||||
if (evt.code == "KeyRETURN") {
|
||||
var el = $("button#connect");
|
||||
view.focus = el;
|
||||
el.click();
|
||||
// simulate button click effect, windows does not have this issue
|
||||
el.classList.toggle("active", true);
|
||||
el.timer(300, ()=> el.classList.toggle("active", false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newRemote(type) {
|
||||
createNewConnect(this.remote_id.value, type);
|
||||
}
|
||||
}
|
||||
|
||||
class InstallMe extends Element {
|
||||
render() {
|
||||
return (<div class="install-me">
|
||||
<span />
|
||||
<div>{translate('install_tip')}</div>
|
||||
<div style="text-align: center; margin-top: 1em;"><button id="install-me" class="button">{translate('Install')}</button></div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #install-me"]() {
|
||||
handler.xcall("goto_install");
|
||||
}
|
||||
}
|
||||
|
||||
const http = function() {
|
||||
function makeRequest(httpverb) {
|
||||
return function( params ) {
|
||||
params.type = httpverb;
|
||||
// TODO request
|
||||
view.request(params);
|
||||
};
|
||||
}
|
||||
function download(from, to, ...args) {
|
||||
// TODO #get
|
||||
let rqp = { type:"get", url: from, toFile: to };
|
||||
let fn = 0;
|
||||
let on = 0;
|
||||
// TODO p in / p of?
|
||||
for( let p in args )
|
||||
if( p instanceof Function )
|
||||
{
|
||||
switch(++fn) {
|
||||
case 1: rqp.success = p; break;
|
||||
case 2: rqp.error = p; break;
|
||||
case 3: rqp.progress = p; break;
|
||||
}
|
||||
} else if( p instanceof Object )
|
||||
{
|
||||
switch(++on) {
|
||||
case 1: rqp.params = p; break;
|
||||
case 2: rqp.headers = p; break;
|
||||
}
|
||||
}
|
||||
// TODO request
|
||||
view.request(rqp);
|
||||
}
|
||||
|
||||
return {
|
||||
get: makeRequest("get"),
|
||||
post: makeRequest("post"),
|
||||
put: makeRequest("put"),
|
||||
del: makeRequest("delete"),
|
||||
download: download
|
||||
};
|
||||
|
||||
}();
|
||||
|
||||
class UpgradeMe extends Element {
|
||||
render() {
|
||||
let update_or_download = is_osx ? "download" : "update";
|
||||
return (<div class="install-me">
|
||||
<div>{translate('Status')}</div>
|
||||
<div>{translate('Your installation is lower version.')}</div>
|
||||
<div id="install-me" class="link" style="padding-top: 1em">{translate('Click to upgrade')}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #install-me"]() {
|
||||
handler.xcall("update_me");
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateMe extends Element {
|
||||
render() {
|
||||
let update_or_download = "download"; // !is_win ? "download" : "update";
|
||||
return (<div class="install-me">
|
||||
<div>{translate('Status')}</div>
|
||||
<div>There is a newer version of {handler.xcall("get_app_name")} ({handler.xcall("get_new_version")}) available.</div>
|
||||
<div id="install-me" class="link" style="padding-top: 1em">Click to {update_or_download}</div>
|
||||
<div id="download-percent" style="display:hidden; padding-top: 1em;" />
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #install-me"]() {
|
||||
handler.xcall("open_url","https://rustdesk.com");
|
||||
return;
|
||||
if (!is_win) {
|
||||
handler.xcall("open_url","https://rustdesk.com");
|
||||
return;
|
||||
}
|
||||
let url = software_update_url + '.' + handler.xcall("get_software_ext");
|
||||
let path = handler.xcall("get_software_store_path");
|
||||
let onsuccess = function(md5) {
|
||||
this.$("#download-percent").content(translate("Installing ..."));
|
||||
handler.xcall("update_me",path);
|
||||
};
|
||||
let onerror = function(err) {
|
||||
msgbox("custom-error", "Download Error", "Failed to download");
|
||||
};
|
||||
let onprogress = function(loaded, total) {
|
||||
if (!total) total = 5 * 1024 * 1024;
|
||||
let el = this.$("#download-percent");
|
||||
el.style.setProperty("display","block");
|
||||
el.content("Downloading %" + (loaded * 100 / total));
|
||||
};
|
||||
console.log("Downloading " + url + " to " + path);
|
||||
http.download(
|
||||
url,
|
||||
document.url(path),
|
||||
onsuccess, onerror, onprogress);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemError extends Element {
|
||||
render() {
|
||||
return (<div class="install-me">
|
||||
<div>{system_error}</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
class TrustMe extends Element {
|
||||
render() {
|
||||
return (<div class="trust-me">
|
||||
<div>{translate('Configuration Permissions')}</div>
|
||||
<div>{translate('config_acc')}</div>
|
||||
<div id="trust-me" class="link">{translate('Configure')}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #trust-me"] () {
|
||||
handler.xcall("is_process_trusted",true);
|
||||
watch_trust();
|
||||
}
|
||||
}
|
||||
|
||||
class CanScreenRecording extends Element {
|
||||
render() {
|
||||
return (<div class="trust-me">
|
||||
<div>{translate('Configuration Permissions')}</div>
|
||||
<div>{translate('config_screen')}</div>
|
||||
<div id="screen-recording" class="link">{translate('Configure')}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #screen-recording"]() {
|
||||
handler.xcall("is_can_screen_recording",true);
|
||||
watch_trust();
|
||||
}
|
||||
}
|
||||
|
||||
class FixWayland extends Element {
|
||||
render() {
|
||||
return (<div class="trust-me">
|
||||
<div>{translate('Warning')}</div>
|
||||
<div>{translate('Login screen using Wayland is not supported')}</div>
|
||||
<div id="fix-wayland" class="link">{translate('Fix it')}</div>
|
||||
<div style="text-align: center">({translate('Reboot required')})</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #fix-wayland"] () {
|
||||
handler.xcall("fix_login_wayland");
|
||||
app.componentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
class ModifyDefaultLogin extends Element {
|
||||
render() {
|
||||
return (<div class="trust-me">
|
||||
<div>{translate('Warning')}</div>
|
||||
<div>{translate('Current Wayland display server is not supported')}</div>
|
||||
<div id="modify-default-login" class="link">{translate('Fix it')}</div>
|
||||
<div style="text-align: center">({translate('Reboot required')})</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at #modify-default-login"]() {
|
||||
let r = handler.xcall("modify_default_login");
|
||||
if (r) {
|
||||
msgbox("custom-error", "Error", r);
|
||||
}
|
||||
app.componentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function watch_trust() {
|
||||
// not use TrustMe::update, because it is buggy
|
||||
let trusted = handler.xcall("is_process_trusted",false);
|
||||
let el = $("div.trust-me");
|
||||
if (el) {
|
||||
el.style.setProperty("display", trusted ? "none" : "block");
|
||||
}
|
||||
// if (trusted) return;
|
||||
// TODO dont have exit?
|
||||
setTimeout(() => {
|
||||
watch_trust()
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
class PasswordEyeArea extends Element {
|
||||
render() {
|
||||
return (<div class="eye-area" style="width: *">
|
||||
<input type="text" readonly value="******" />
|
||||
{svg_eye}
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on mouseenter"]() {
|
||||
this.leaved = false;
|
||||
setTimeout(()=> {
|
||||
if (this.leaved) return;
|
||||
this.$("input").value = handler.xcall("get_password");
|
||||
},300);
|
||||
}
|
||||
|
||||
["on mouseleave"]() {
|
||||
this.leaved = true;
|
||||
this.$("input").value = "******";
|
||||
}
|
||||
}
|
||||
|
||||
class Password extends Element {
|
||||
render() {
|
||||
return (<div class="password" style="flow:horizontal">
|
||||
<PasswordEyeArea />
|
||||
{svg_edit}
|
||||
</div>);
|
||||
}
|
||||
|
||||
["on click at svg#edit"](_,me) {
|
||||
let menu = $("menu#edit-password-context");
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
["on click at li#refresh-password"] () {
|
||||
handler.xcall("update_password");
|
||||
this.componentUpdate();
|
||||
}
|
||||
|
||||
["on click at li#set-password"] () {
|
||||
// option .form .set-password ...
|
||||
msgbox("custom-password", translate("Set Password"), "<div .form .set-password> \
|
||||
<div><span>" + translate('Password') + ":</span><input|password(password) .outline-focus /></div> \
|
||||
<div><span>" + translate('Confirmation') + ":</span><input|password(confirmation) /></div> \
|
||||
</div> \
|
||||
",
|
||||
function(res=null) {
|
||||
if (!res) return;
|
||||
let p0 = (res.password || "").trim();
|
||||
let p1 = (res.confirmation || "").trim();
|
||||
if (p0.length < 6) {
|
||||
return translate("Too short, at least 6 characters.");
|
||||
}
|
||||
if (p0 != p1) {
|
||||
return translate("The confirmation is not identical.");
|
||||
}
|
||||
handler.xcall("update_password",p0);
|
||||
this.componentUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ID extends Element {
|
||||
render() {
|
||||
return <input type="text" id="remote_id" class="outline-focus" novalue={translate("Enter Remote ID")} maxlength="15" value={formatId(handler.xcall("get_remote_id"))} />;
|
||||
}
|
||||
|
||||
// TEST
|
||||
// https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm
|
||||
["on change"]() {
|
||||
let fid = formatId(this.value);
|
||||
let d = this.value.length - (this.old_value || "").length;
|
||||
this.old_value = this.value;
|
||||
let start = this.xcall("selectionStart") || 0;
|
||||
let end = this.xcall("selectionEnd");
|
||||
if (fid == this.value || d <= 0 || start != end) {
|
||||
return;
|
||||
}
|
||||
// fix Caret position
|
||||
this.value = fid;
|
||||
let text_after_caret = this.old_value.substr(start);
|
||||
let n = fid.length - formatId(text_after_caret).length;
|
||||
this.xcall("setSelection", n, n);
|
||||
}
|
||||
}
|
||||
|
||||
var reg = /^\d+$/;
|
||||
export function formatId(id) {
|
||||
id = id.replace(/\s/g, "");
|
||||
if (reg.test(id) && id.length > 3) {
|
||||
let n = id.length;
|
||||
let a = n % 3 || 3;
|
||||
let new_id = id.substr(0, a);
|
||||
for (let i = a; i < n; i += 3) {
|
||||
new_id += " " + id.substr(i, 3);
|
||||
}
|
||||
return new_id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
document.body.content(<App />);
|
||||
|
||||
document.on("ready",()=>{
|
||||
let r = handler.xcall("get_size");
|
||||
if (isReasonableSize(r) && r[2] > 0) {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
} else {
|
||||
centerize(800, 600);
|
||||
}
|
||||
if (!handler.xcall("get_remote_id")) {
|
||||
view.focus = $("#remote_id"); // TEST
|
||||
}
|
||||
})
|
||||
|
||||
document.on("unloadequest",(evt)=>{
|
||||
// evt.preventDefault() // can prevent window close
|
||||
let [x, y, w, h] = view.box("rectw", "border", "desktop");
|
||||
handler.xcall("save_size",x, y, w, h);
|
||||
})
|
||||
|
||||
// check connect status
|
||||
setInterval(() => {
|
||||
let tmp = !!handler.xcall("get_option","stop-service");
|
||||
if (tmp != service_stopped) {
|
||||
service_stopped = tmp;
|
||||
app.connect_status.componentUpdate();
|
||||
myIdMenu.componentUpdate();
|
||||
}
|
||||
tmp = handler.xcall("get_connect_status");
|
||||
if (tmp[0] != connect_status) {
|
||||
connect_status = tmp[0];
|
||||
app.connect_status.componentUpdate();
|
||||
}
|
||||
if (tmp[1] != key_confirmed) {
|
||||
key_confirmed = tmp[1];
|
||||
app.componentUpdate();
|
||||
}
|
||||
if (tmp[2] && tmp[2] != my_id) {
|
||||
console.log("id updated");
|
||||
app.componentUpdate();
|
||||
}
|
||||
tmp = handler.xcall("get_error");
|
||||
if (system_error != tmp) {
|
||||
system_error = tmp;
|
||||
app.componentUpdate();
|
||||
}
|
||||
tmp = handler.xcall("get_software_update_url");
|
||||
if (tmp != software_update_url) {
|
||||
software_update_url = tmp;
|
||||
app.componentUpdate();
|
||||
}
|
||||
if (handler.xcall("recent_sessions_updated")) {
|
||||
console.log("recent sessions updated");
|
||||
app.componentUpdate();
|
||||
}
|
||||
}, 1000);
|
||||
745
src/ui/index.tis
745
src/ui/index.tis
@@ -1,745 +0,0 @@
|
||||
if (is_osx) view.windowBlurbehind = #light;
|
||||
stdout.println("current platform:", OS);
|
||||
|
||||
// html min-width, min-height not working on mac, below works for all
|
||||
view.windowMinSize = (500, 300);
|
||||
|
||||
var app;
|
||||
var tmp = handler.get_connect_status();
|
||||
var connect_status = tmp[0];
|
||||
var service_stopped = false;
|
||||
var software_update_url = "";
|
||||
var key_confirmed = tmp[1];
|
||||
var system_error = "";
|
||||
|
||||
var svg_menu = <svg #menu viewBox="0 0 512 512">
|
||||
<circle cx="256" cy="256" r="64"/>
|
||||
<circle cx="256" cy="448" r="64"/>
|
||||
<circle cx="256" cy="64" r="64"/>
|
||||
</svg>;
|
||||
|
||||
class ConnectStatus: Reactor.Component {
|
||||
function render() {
|
||||
return
|
||||
<div .connect-status>
|
||||
<span class={"connect-status-icon connect-status" + (service_stopped ? 0 : connect_status)} />
|
||||
{this.getConnectStatusStr()}
|
||||
{service_stopped ? <span class="link">Start Service</span> : ""}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getConnectStatusStr() {
|
||||
if (service_stopped) {
|
||||
return "Service is not running";
|
||||
} else if (connect_status == -1) {
|
||||
return "Not ready. Please check your connection";
|
||||
} else if (connect_status == 0) {
|
||||
return "Connecting to the RustDesk network...";
|
||||
}
|
||||
return "Ready";
|
||||
}
|
||||
|
||||
event click $(.connect-status .link) () {
|
||||
var options = handler.get_options();
|
||||
options["stop-service"] = "";
|
||||
handler.set_options(options);
|
||||
}
|
||||
}
|
||||
|
||||
class RecentSessions: Reactor.Component {
|
||||
function render() {
|
||||
var sessions = handler.get_recent_sessions();
|
||||
if (sessions.length == 0) return <span />;
|
||||
sessions = sessions.map(this.getSession);
|
||||
return <div style="width: *">
|
||||
<div .recent-sessions-title>RECENT SESSIONS</div>
|
||||
<div .recent-sessions-content key={sessions.length}>
|
||||
{sessions}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getSession(s) {
|
||||
var id = s[0];
|
||||
var username = s[1];
|
||||
var hostname = s[2];
|
||||
var platform = s[3];
|
||||
var alias = s[4];
|
||||
return <div .remote-session id={id} platform={platform} style={"background:"+string2RGB(id+platform, 0.5)}>
|
||||
<div .platform>
|
||||
{platformSvg(platform, "white")}
|
||||
<div .username>{username}@{hostname}</div>
|
||||
</div>
|
||||
<div .text>
|
||||
<div #alias>{alias ? alias : formatId(id)}</div>
|
||||
{svg_menu}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event dblclick $(div.remote-session) (evt, me) {
|
||||
createNewConnect(me.id, "connect");
|
||||
}
|
||||
|
||||
event click $(#menu) (_, me) {
|
||||
var id = me.parent.parent.id;
|
||||
var platform = me.parent.parent.attributes["platform"];
|
||||
$(#rdp).style.set{
|
||||
display: (platform == "Windows" && is_win) ? "block" : "none",
|
||||
};
|
||||
// https://sciter.com/forums/topic/replacecustomize-context-menu/
|
||||
var menu = $(menu#remote-context);
|
||||
menu.attributes["remote-id"] = id;
|
||||
me.popup(menu);
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#remote-context li) (evt, me) {
|
||||
var action = me.id;
|
||||
var id = me.parent.attributes["remote-id"];
|
||||
if (action == "connect") {
|
||||
createNewConnect(id, "connect");
|
||||
} else if (action == "transfer") {
|
||||
createNewConnect(id, "file-transfer");
|
||||
} else if (action == "remove") {
|
||||
handler.remove_peer(id);
|
||||
app.recent_sessions.update();
|
||||
} else if (action == "shortcut") {
|
||||
handler.create_shortcut(id);
|
||||
} else if (action == "rdp") {
|
||||
createNewConnect(id, "rdp");
|
||||
} else if (action == "tunnel") {
|
||||
createNewConnect(id, "port-forward");
|
||||
} else if (action == "rename") {
|
||||
var old_name = handler.get_peer_option(id, "alias");
|
||||
handler.msgbox("custom-rename", "Rename", "<div .form> \
|
||||
<div><input name='name' style='width: *; height: 23px', value='" + old_name + "' /></div> \
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var name = (res.name || "").trim();
|
||||
if (name != old_name) handler.set_peer_option(id, "alias", name);
|
||||
self.select('#' + id).select('#alias').text = name || id;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createNewConnect(id, type) {
|
||||
id = id.replace(/\s/g, "");
|
||||
app.remote_id.value = formatId(id);
|
||||
if (!id) return;
|
||||
if (id == handler.get_id()) {
|
||||
handler.msgbox("custom-error", "Error", "You cannot connect to your own computer");
|
||||
return;
|
||||
}
|
||||
handler.set_remote_id(id);
|
||||
handler.new_remote(id, type);
|
||||
}
|
||||
|
||||
var myIdMenu;
|
||||
var audioInputMenu;
|
||||
var configOptions = {};
|
||||
class AudioInputs: Reactor.Component {
|
||||
function this() {
|
||||
audioInputMenu = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!this.show) return <li />;
|
||||
var inputs = handler.get_sound_inputs();
|
||||
if (is_win) inputs = ["System Sound"].concat(inputs);
|
||||
if (!inputs.length) return <li style="display:hidden" />;
|
||||
inputs = ["Mute"].concat(inputs);
|
||||
var me = this;
|
||||
self.timer(1ms, function() { me.toggleMenuState() });
|
||||
return <li>Audio Input
|
||||
<menu #audio-input key={inputs.length}>
|
||||
{inputs.map(function(name) {
|
||||
return <li id={name}><span>{svg_checkmark}</span>{name}</li>;
|
||||
})}
|
||||
</menu>
|
||||
</li>;
|
||||
}
|
||||
|
||||
function get_default() {
|
||||
if (is_win) return "System Sound";
|
||||
return "";
|
||||
}
|
||||
|
||||
function get_value() {
|
||||
return configOptions["audio-input"] || this.get_default();
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
var v = this.get_value();
|
||||
for (var el in $$(menu#audio-input>li)) {
|
||||
var selected = el.id == v;
|
||||
el.attributes.toggleClass("selected", selected);
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#audio-input>li) (_, me) {
|
||||
var v = me.id;
|
||||
if (v == this.get_value()) return;
|
||||
if (v == this.get_default()) v = "";
|
||||
configOptions["audio-input"] = v;
|
||||
handler.set_options(configOptions);
|
||||
this.toggleMenuState();
|
||||
}
|
||||
}
|
||||
|
||||
class MyIdMenu: Reactor.Component {
|
||||
function this() {
|
||||
myIdMenu = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var me = this;
|
||||
return <div #myid>
|
||||
{this.renderPop()}
|
||||
ID{svg_menu}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderPop() {
|
||||
return <popup>
|
||||
<menu.context #config-options>
|
||||
<li #enable-keyboard><span>{svg_checkmark}</span>Enable Keyboard/Mouse</li>
|
||||
<li #enable-clipboard><span>{svg_checkmark}</span>Enable Clipboard</li>
|
||||
<li #enable-file-transfer><span>{svg_checkmark}</span>Enable File Transfer</li>
|
||||
<li #enable-tunnel><span>{svg_checkmark}</span>Enable TCP Tunneling</li>
|
||||
<AudioInputs />
|
||||
<div .separator />
|
||||
<li #whitelist title="Only whitelisted IP can access me">IP Whitelisting</li>
|
||||
<li #custom-server>ID/Relay Server</li>
|
||||
<div .separator />
|
||||
<li #stop-service class={service_stopped ? "line-through" : "selected"}><span>{svg_checkmark}</span>Enable service</li>
|
||||
<div .separator />
|
||||
<li #forum>Forum</li>
|
||||
<li #about>About {handler.get_app_name()}</li>
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
||||
event click $(svg#menu) (_, me) {
|
||||
audioInputMenu.update({ show: true });
|
||||
configOptions = handler.get_options();
|
||||
this.toggleMenuState();
|
||||
var menu = $(menu#config-options);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
function toggleMenuState() {
|
||||
for (var el in $$(menu#config-options>li)) {
|
||||
if (el.id && el.id.indexOf("enable-") == 0) {
|
||||
var enabled = configOptions[el.id] != "N";
|
||||
el.attributes.toggleClass("selected", enabled);
|
||||
el.attributes.toggleClass("line-through", !enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event click $(menu#config-options>li) (_, me) {
|
||||
if (me.id && me.id.indexOf("enable-") == 0) {
|
||||
configOptions[me.id] = configOptions[me.id] == "N" ? "" : "N";
|
||||
handler.set_options(configOptions);
|
||||
}
|
||||
if (me.id == "whitelist") {
|
||||
var old_value = (configOptions["whitelist"] || "").split(",").join("\n");
|
||||
handler.msgbox("custom-whitelist", "IP Whitelisting", "<div .form> \
|
||||
<textarea spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; height: 160px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var value = (res.text || "").trim();
|
||||
if (value) {
|
||||
var values = value.split(/[\s,;]+/g);
|
||||
for (var ip in values) {
|
||||
if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||
return "Invalid ip: " + ip;
|
||||
}
|
||||
}
|
||||
value = values.join("\n");
|
||||
}
|
||||
if (value == old_value) return;
|
||||
configOptions["whitelist"] = value.replace("\n", ",");
|
||||
stdout.println("whitelist updated");
|
||||
handler.set_options(configOptions);
|
||||
}, 300);
|
||||
} else if (me.id == "custom-server") {
|
||||
var old_relay = configOptions["relay-server"] || "";
|
||||
var old_id = configOptions["custom-rendezvous-server"] || "";
|
||||
handler.msgbox("custom-server", "ID/Relay Server", "<div .form> \
|
||||
<div><span style='width: 100px; display:inline-block'>ID Server: </span><input style='width: 250px' name='id' value='" + old_id + "' /></div> \
|
||||
<div><span style='width: 100px; display:inline-block'>Relay Server: </span><input style='width: 250px' name='relay' value='" + old_relay + "' /></div> \
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var id = (res.id || "").trim();
|
||||
var relay = (res.relay || "").trim();
|
||||
if (id == old_id && relay == old_relay) return;
|
||||
if (id) {
|
||||
var err = handler.test_if_valid_server(id);
|
||||
if (err) return "ID Server: " + err;
|
||||
}
|
||||
if (relay) {
|
||||
var err = handler.test_if_valid_server(relay);
|
||||
if (err) return "Relay Server: " + err;
|
||||
}
|
||||
configOptions["custom-rendezvous-server"] = id;
|
||||
configOptions["relay-server"] = relay;
|
||||
handler.set_options(configOptions);
|
||||
});
|
||||
} else if (me.id == "forum") {
|
||||
handler.open_url("https:://forum.rustdesk.com");
|
||||
} else if (me.id == "stop-service") {
|
||||
configOptions["stop-service"] = service_stopped ? "" : "Y";
|
||||
handler.set_options(configOptions);
|
||||
} else if (me.id == "about") {
|
||||
var name = handler.get_app_name();
|
||||
handler.msgbox("custom-nocancel-nook-hasclose", "About " + name, "<div style='line-height: 2em'> \
|
||||
<div>Version: " + handler.get_version() + " \
|
||||
<div .link .custom-event url='http://rustdesk.com/privacy'>Privacy Statement</div> \
|
||||
<div .link .custom-event url='http://forum.rustdesk.com'>Forum</div> \
|
||||
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright © 2020 CarrieZ Studio \
|
||||
<br /> Author: Carrie \
|
||||
<p style='font-weight: bold'>Made with heart in this chaotic world!</p>\
|
||||
</div>\
|
||||
</div>", function(el) {
|
||||
if (el && el.attributes) {
|
||||
handler.open_url(el.attributes['url']);
|
||||
};
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class App: Reactor.Component
|
||||
{
|
||||
function this() {
|
||||
app = this;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var is_can_screen_recording = handler.is_can_screen_recording(false);
|
||||
return
|
||||
<div .app>
|
||||
<popup>
|
||||
<menu.context #remote-context>
|
||||
<li #connect>Connect</li>
|
||||
<li #transfer>Transfer File</li>
|
||||
<li #tunnel>TCP Tunneling</li>
|
||||
<li #rdp>RDP</li>
|
||||
<li #rename>Rename</li>
|
||||
<li #remove>Remove</li>
|
||||
{is_win && <li #shortcut>Create Desktop Shortcut</li>}
|
||||
</menu>
|
||||
</popup>
|
||||
<popup>
|
||||
<menu.context #edit-password-context>
|
||||
<li #refresh-password>Refresh random password</li>
|
||||
<li #set-password>Set your own password</li>
|
||||
</menu>
|
||||
</popup>
|
||||
<div .left-pane>
|
||||
<div>
|
||||
<div .title>Your Desktop</div>
|
||||
<div .lighter-text>Your desktop can be accessed with this ID and password.</div>
|
||||
<div .your-desktop>
|
||||
<MyIdMenu />
|
||||
{key_confirmed ? <input type="text" readonly value={formatId(handler.get_id())}/> : "Generating ..."}
|
||||
</div>
|
||||
<div .your-desktop>
|
||||
<div>Password</div>
|
||||
<Password />
|
||||
</div>
|
||||
</div>
|
||||
{handler.is_installed() ? "": <InstalllMe />}
|
||||
{handler.is_installed() && software_update_url ? <UpdateMe /> : ""}
|
||||
{handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? <UpgradeMe /> : ""}
|
||||
{is_can_screen_recording ? "": <CanScreenRecording />}
|
||||
{is_can_screen_recording && !handler.is_process_trusted(false) ? <TrustMe /> : ""}
|
||||
{system_error ? <SystemError /> : ""}
|
||||
{!system_error && handler.is_login_wayland() ? <FixWayland /> : ""}
|
||||
</div>
|
||||
<div .right-pane>
|
||||
<div .right-content>
|
||||
<div .card-connect>
|
||||
<div .title>Control Remote Desktop</div>
|
||||
<ID @{this.remote_id} />
|
||||
<div .right-buttons>
|
||||
<button .button .outline #file-transfer>Transfer File</button>
|
||||
<button .button #connect>Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<RecentSessions @{this.recent_sessions} />
|
||||
</div>
|
||||
<ConnectStatus @{this.connect_status} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(button#connect) {
|
||||
this.newRemote("connect");
|
||||
}
|
||||
|
||||
event click $(button#file-transfer) {
|
||||
this.newRemote("file-transfer");
|
||||
}
|
||||
|
||||
function newRemote(type) {
|
||||
createNewConnect(this.remote_id.value, type);
|
||||
}
|
||||
}
|
||||
|
||||
class InstalllMe: Reactor.Component {
|
||||
function render() {
|
||||
return <div .install-me>
|
||||
<div>Install RustDesk</div>
|
||||
<div #install-me .link>Install RustDesk on this computer ...</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#install-me) {
|
||||
handler.goto_install();
|
||||
}
|
||||
}
|
||||
|
||||
const http = function() {
|
||||
|
||||
function makeRequest(httpverb) {
|
||||
return function( params ) {
|
||||
params.type = httpverb;
|
||||
view.request(params);
|
||||
};
|
||||
}
|
||||
|
||||
function download(from, to, args..)
|
||||
{
|
||||
var rqp = { type:#get, url: from, toFile: to };
|
||||
var fn = 0;
|
||||
var on = 0;
|
||||
for( var p in args )
|
||||
if( p instanceof Function )
|
||||
{
|
||||
switch(++fn) {
|
||||
case 1: rqp.success = p; break;
|
||||
case 2: rqp.error = p; break;
|
||||
case 3: rqp.progress = p; break;
|
||||
}
|
||||
} else if( p instanceof Object )
|
||||
{
|
||||
switch(++on) {
|
||||
case 1: rqp.params = p; break;
|
||||
case 2: rqp.headers = p; break;
|
||||
}
|
||||
}
|
||||
view.request(rqp);
|
||||
}
|
||||
|
||||
return {
|
||||
get: makeRequest(#get),
|
||||
post: makeRequest(#post),
|
||||
put: makeRequest(#put),
|
||||
del: makeRequest(#delete),
|
||||
download: download
|
||||
};
|
||||
|
||||
}();
|
||||
|
||||
class UpgradeMe: Reactor.Component {
|
||||
function render() {
|
||||
var update_or_download = is_osx ? "download" : "update";
|
||||
return <div .install-me>
|
||||
<div>{handler.get_app_name()} Status</div>
|
||||
<div>An update is available for RustDesk.</div>
|
||||
<div #install-me .link style="padding-top: 1em">Click to upgrade</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#install-me) {
|
||||
handler.update_me("");
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateMe: Reactor.Component {
|
||||
function render() {
|
||||
var update_or_download = is_osx ? "download" : "update";
|
||||
return <div .install-me>
|
||||
<div>{handler.get_app_name()} Status</div>
|
||||
<div>There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.</div>
|
||||
<div #install-me .link style="padding-top: 1em">Click to {update_or_download}</div>
|
||||
<div #download-percent style="display:hidden; padding-top: 1em;" />
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#install-me) {
|
||||
if (is_osx) {
|
||||
handler.open_url("https://rustdesk.com");
|
||||
return;
|
||||
}
|
||||
var url = software_update_url + '.' + handler.get_software_ext();
|
||||
var path = handler.get_software_store_path();
|
||||
var onsuccess = function(md5) {
|
||||
$(#download-percent).content("Installing ...");
|
||||
handler.update_me(path);
|
||||
};
|
||||
var onerror = function(err) {
|
||||
handler.msgbox("custom-error", "Download Error", "Failed to download");
|
||||
};
|
||||
var onprogress = function(loaded, total) {
|
||||
if (!total) total = 5 * 1024 * 1024;
|
||||
var el = $(#download-percent);
|
||||
el.style.set{display: "block"};
|
||||
el.content("Downloading %" + (loaded * 100 / total));
|
||||
};
|
||||
stdout.println("Downloading " + url + " to " + path);
|
||||
http.download(
|
||||
url,
|
||||
self.url(path),
|
||||
onsuccess, onerror, onprogress);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemError: Reactor.Component {
|
||||
function render() {
|
||||
return <div .install-me>
|
||||
<div>{system_error}</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class TrustMe: Reactor.Component {
|
||||
function render() {
|
||||
return <div .trust-me>
|
||||
<div>Configuration Permissions</div>
|
||||
<div>In order to control your Desktop remotely, you need to grant RustDesk "Accessibility" permissions</div>
|
||||
<div #trust-me .link>Configure</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#trust-me) {
|
||||
handler.is_process_trusted(true);
|
||||
watch_trust();
|
||||
}
|
||||
}
|
||||
|
||||
class CanScreenRecording: Reactor.Component {
|
||||
function render() {
|
||||
return <div .trust-me>
|
||||
<div>Configuration Permissions</div>
|
||||
<div>In order to access your Desktop remotely, you need to grant RustDesk "Screen Recording" permissions</div>
|
||||
<div #screen-recording .link>Configure</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#screen-recording) {
|
||||
handler.is_can_screen_recording(true);
|
||||
watch_trust();
|
||||
}
|
||||
}
|
||||
|
||||
class FixWayland: Reactor.Component {
|
||||
function render() {
|
||||
return <div .trust-me>
|
||||
<div>Warning</div>
|
||||
<div>Login screen using Wayland is not supported</div>
|
||||
<div #fix-wayland .link>Fix it</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(#fix-wayland) {
|
||||
handler.fix_login_wayland();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
|
||||
function watch_trust() {
|
||||
// not use TrustMe::update, because it is buggy
|
||||
var trusted = handler.is_process_trusted(false);
|
||||
var el = $(div.trust-me);
|
||||
if (el) {
|
||||
el.style.set {
|
||||
display: trusted ? "none" : "block",
|
||||
};
|
||||
}
|
||||
// if (trusted) return;
|
||||
self.timer(1s, watch_trust);
|
||||
}
|
||||
|
||||
class PasswordEyeArea : Reactor.Component {
|
||||
render() {
|
||||
return
|
||||
<div .eye-area style="width: *">
|
||||
<input|text @{this.input} readonly value="******" />
|
||||
{svg_eye}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event mouseenter {
|
||||
var me = this;
|
||||
me.leaved = false;
|
||||
me.timer(300ms, function() {
|
||||
if (me.leaved) return;
|
||||
me.input.value = handler.get_password();
|
||||
});
|
||||
}
|
||||
|
||||
event mouseleave {
|
||||
this.leaved = true;
|
||||
this.input.value = "******";
|
||||
}
|
||||
}
|
||||
|
||||
class Password: Reactor.Component {
|
||||
function render() {
|
||||
return <div .password style="flow:horizontal">
|
||||
<PasswordEyeArea />
|
||||
{svg_edit}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(svg#edit) (_, me) {
|
||||
var menu = $(menu#edit-password-context);
|
||||
me.popup(menu);
|
||||
}
|
||||
|
||||
event click $(li#refresh-password) {
|
||||
handler.update_password("");
|
||||
this.update();
|
||||
}
|
||||
|
||||
event click $(li#set-password) {
|
||||
var me = this;
|
||||
handler.msgbox("custom-password", "Set Password", "<div .form .set-password> \
|
||||
<div><span>Password:</span><input|password(password) /></div> \
|
||||
<div><span>Confirmation:</span><input|password(confirmation) /></div> \
|
||||
</div> \
|
||||
", function(res=null) {
|
||||
if (!res) return;
|
||||
var p0 = (res.password || "").trim();
|
||||
var p1 = (res.confirmation || "").trim();
|
||||
if (p0.length < 6) {
|
||||
return "Too short, at least 6 characters.";
|
||||
}
|
||||
if (p0 != p1) {
|
||||
return "The confirmation is not identical.";
|
||||
}
|
||||
handler.update_password(p0);
|
||||
me.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ID: Reactor.Component {
|
||||
function render() {
|
||||
return <input type="text" #remote_id .outline-focus novalue="Enter Remote ID" maxlength="13"
|
||||
value={formatId(handler.get_remote_id())} />;
|
||||
}
|
||||
|
||||
// https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm
|
||||
event change {
|
||||
var fid = formatId(this.value);
|
||||
var d = this.value.length - (this.old_value || "").length;
|
||||
this.old_value = this.value;
|
||||
var start = this.xcall(#selectionStart) || 0;
|
||||
var end = this.xcall(#selectionEnd);
|
||||
if (fid == this.value || d <= 0 || start != end) {
|
||||
return;
|
||||
}
|
||||
// fix Caret position
|
||||
this.value = fid;
|
||||
var text_after_caret = this.old_value.substr(start);
|
||||
var n = fid.length - formatId(text_after_caret).length;
|
||||
this.xcall(#setSelection, n, n);
|
||||
}
|
||||
}
|
||||
|
||||
var reg = /^\d+$/;
|
||||
function formatId(id) {
|
||||
id = id.replace(/\s/g, "");
|
||||
if (reg.test(id) && id.length > 3) {
|
||||
var n = id.length;
|
||||
var a = n % 3 || 3;
|
||||
var new_id = id.substr(0, a);
|
||||
for (var i = a; i < n; i += 3) {
|
||||
new_id += " " + id.substr(i, 3);
|
||||
}
|
||||
return new_id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
event keydown (evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(is_osx && evt.keyCode == 0x4C) ||
|
||||
(is_linux && evt.keyCode == 65421)) {
|
||||
var el = $(button#connect);
|
||||
view.focus = el;
|
||||
el.sendEvent("click");
|
||||
// simulate button click effect, windows does not have this issue
|
||||
el.attributes.toggleClass("active", true);
|
||||
self.timer(0.3s, function() {
|
||||
el.attributes.toggleClass("active", false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<App />);
|
||||
|
||||
function self.closing() {
|
||||
// return false; // can prevent window close
|
||||
var (x, y, w, h) = view.box(#rectw, #border, #screen);
|
||||
handler.save_size(x, y, w, h);
|
||||
}
|
||||
|
||||
function self.ready() {
|
||||
var r = handler.get_size();
|
||||
if (r[2] == 0) {
|
||||
centerize(800, 600);
|
||||
} else {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
}
|
||||
if (!handler.get_remote_id()) {
|
||||
view.focus = $(#remote_id);
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectStatus() {
|
||||
self.timer(1s, function() {
|
||||
var tmp = !!handler.get_option("stop-service");
|
||||
if (tmp != service_stopped) {
|
||||
service_stopped = tmp;
|
||||
app.connect_status.update();
|
||||
myIdMenu.update();
|
||||
}
|
||||
tmp = handler.get_connect_status();
|
||||
if (tmp[0] != connect_status) {
|
||||
connect_status = tmp[0];
|
||||
app.connect_status.update();
|
||||
}
|
||||
if (tmp[1] != key_confirmed) {
|
||||
key_confirmed = tmp[1];
|
||||
app.update();
|
||||
}
|
||||
tmp = handler.get_error();
|
||||
if (system_error != tmp) {
|
||||
system_error = tmp;
|
||||
app.update();
|
||||
}
|
||||
tmp = handler.get_software_update_url();
|
||||
if (tmp != software_update_url) {
|
||||
software_update_url = tmp;
|
||||
app.update();
|
||||
}
|
||||
if (handler.recent_sessions_updated()) {
|
||||
stdout.println("recent sessions updated");
|
||||
app.recent_sessions.update();
|
||||
}
|
||||
checkConnectStatus();
|
||||
});
|
||||
}
|
||||
|
||||
checkConnectStatus();
|
||||
@@ -5,17 +5,17 @@ function self.ready() {
|
||||
class Install: Reactor.Component {
|
||||
function render() {
|
||||
return <div .content>
|
||||
<div style="font-size: 2em;">Installation</div>
|
||||
<div style="margin: 2em 0;">Installation Path: <input|text disabled value={view.install_path()} /></div>
|
||||
<div><button|checkbox #startmenu checked>Create start menu shortcuts</button></div>
|
||||
<div><button|checkbox #desktopicon checked>Create desktop icon</button></div>
|
||||
<div #aggrement .link style="margin-top: 2em;">End-user license agreement</div>
|
||||
<div>By starting the installation, you accept the license agreement.</div>
|
||||
<div style="font-size: 2em;">{translate('Installation')}</div>
|
||||
<div style="margin: 2em 0;">{translate('Installation Path')} {": "}<input|text disabled value={view.install_path()} /></div>
|
||||
<div><button|checkbox #startmenu checked>{translate('Create start menu shortcuts')}</button></div>
|
||||
<div><button|checkbox #desktopicon checked>{translate('Create desktop icon')}</button></div>
|
||||
<div #aggrement .link style="margin-top: 2em;">{translate('End-user license agreement')}</div>
|
||||
<div>{translate('agreement_tip')}</div>
|
||||
<div style="height: 1px; background: gray; margin-top: 1em" />
|
||||
<div style="text-align: right;">
|
||||
<progress style={"color:" + color} style="display: none" />
|
||||
<button .button id="cancel" .outline style="margin-right: 2em;">Cancel</button>
|
||||
<button .button id="submit">Accept and Install</button>
|
||||
<button .button id="cancel" .outline style="margin-right: 2em;">{translate('Cancel')}</button>
|
||||
<button .button id="submit">{translate('Accept and Install')}</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -42,4 +42,4 @@ class Install: Reactor.Component {
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<Install />);
|
||||
$(body).content(<Install />);
|
||||
@@ -1,7 +1,7 @@
|
||||
<html window-frame="extended">
|
||||
<head>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<style>
|
||||
@import url(common.css);
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
caption {
|
||||
@ELLIPSIS;
|
||||
size: *;
|
||||
size: "*";
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding-top: 0.33em;
|
||||
@@ -48,7 +48,7 @@
|
||||
line-height: 2em;
|
||||
}
|
||||
div.set-password div.password {
|
||||
width: *;
|
||||
width: "*";
|
||||
}
|
||||
div.set-password input {
|
||||
font-size: 1em;
|
||||
@@ -60,10 +60,11 @@
|
||||
@ELLIPSIS;
|
||||
}
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
<script type="module" src="msgbox.js"></script>
|
||||
<!-- <script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "msgbox.tis";
|
||||
</script>
|
||||
</script> -->
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
253
src/ui/msgbox.js
Normal file
253
src/ui/msgbox.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import { PasswordComponent } from "./common";
|
||||
import {$,$$} from "@sciter"; //TEST $$ import
|
||||
const view = Window.this;
|
||||
var type, title, text, getParams, remember, retry, callback, contentStyle;
|
||||
var my_translate; // TEST
|
||||
function updateParams(params) {
|
||||
type = params.type;
|
||||
title = params.title;
|
||||
text = params.text;
|
||||
getParams = params.getParams;
|
||||
remember = params.remember;
|
||||
callback = params.callback;
|
||||
my_translate = params.translate;
|
||||
retry = params.retry;
|
||||
contentStyle = params.contentStyle;
|
||||
|
||||
try { text = translate_text(text); } catch (e) {}
|
||||
if (retry > 0) {
|
||||
setTimeout(()=>view.close({ reconnect: true }),retry * 1000);// TEST
|
||||
}
|
||||
}
|
||||
|
||||
function translate_text(text) {
|
||||
if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) {
|
||||
let fds = text.split(': ');
|
||||
for (let i = 0; i < fds.length; ++i) {
|
||||
fds[i] = my_translate(fds[i]);
|
||||
}
|
||||
text = fds.join(': ');
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
var params = view.parameters; // TEST
|
||||
updateParams(params);
|
||||
|
||||
var body;
|
||||
|
||||
class Body extends Element {
|
||||
this() {
|
||||
body = this;
|
||||
}
|
||||
|
||||
getIcon(color) {
|
||||
if (type == "input-password") {
|
||||
return <svg viewBox="0 0 505 505"><circle cx="252.5" cy="252.5" r="252.5" fill={color}/><path d="M271.9 246.1c29.2 17.5 67.6 13.6 92.7-11.5 29.7-29.7 29.7-77.8 0-107.4s-77.8-29.7-107.4 0c-25.1 25.1-29 63.5-11.5 92.7L118.1 347.4l26.2 26.2 26.4 26.4 10.6-10.6-10.1-10.1 9.7-9.7 10.1 10.1 10.6-10.6-10.1-10 9.7-9.7 10.1 10.1 10.6-10.6-26.4-26.3 76.4-76.5z" fill="#fff"/><circle cx="337.4" cy="154.4" r="17.7" fill={color}/></svg>;
|
||||
}
|
||||
if (type == "connecting") {
|
||||
return <svg viewBox="0 0 300 300"><g fill={color}><path d="m221.76 89.414h-143.51c-1.432 0-2.594 1.162-2.594 2.594v95.963c0 1.432 1.162 2.594 2.594 2.594h143.51c1.432 0 2.594-1.162 2.594-2.594v-95.964c0-1.431-1.162-2.593-2.594-2.593z"/><path d="m150 0c-82.839 0-150 67.161-150 150s67.156 150 150 150 150-67.163 150-150-67.164-150-150-150zm92.508 187.97c0 11.458-9.29 20.749-20.749 20.749h-47.144v11.588h23.801c4.298 0 7.781 3.483 7.781 7.781s-3.483 7.781-7.781 7.781h-96.826c-4.298 0-7.781-3.483-7.781-7.781s3.483-7.781 7.781-7.781h23.801v-11.588h-47.145c-11.458 0-20.749-9.29-20.749-20.749v-95.963c0-11.458 9.29-20.749 20.749-20.749h143.51c11.458 0 20.749 9.29 20.749 20.749v95.963z"/></g><path d="m169.62 154.35c-5.0276-5.0336-11.97-8.1508-19.624-8.1508-7.6551 0-14.597 3.1172-19.624 8.1508l-11.077-11.091c7.8656-7.8752 18.725-12.754 30.701-12.754s22.835 4.8788 30.701 12.754l-11.077 11.091zm-32.184 7.0728 12.56 12.576 12.56-12.576c-3.2147-3.2172-7.6555-5.208-12.56-5.208-4.9054 0-9.3457 1.9908-12.56 5.208zm12.56-39.731c14.403 0 27.464 5.8656 36.923 15.338l11.078-11.091c-12.298-12.314-29.276-19.94-48-19.94-18.724 0-35.703 7.626-48 19.94l11.077 11.091c9.4592-9.4728 22.52-15.338 36.923-15.338z" fill="#fff"/></svg>;
|
||||
}
|
||||
if (type == "success") {
|
||||
return <svg viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill={color} /><path fill="#fff" d="M235.472 392.08l-121.04-94.296 34.416-44.168 74.328 57.904 122.672-177.016 46.032 31.888z"/></svg>;
|
||||
}
|
||||
if (type.indexOf("error") >= 0 || type == "re-input-password") {
|
||||
return <svg viewBox="0 0 512 512"><ellipse cx="256" cy="256" rx="256" ry="255.832" fill={color}/><g fill="#fff"><path d="M376.812 337.18l-39.592 39.593-201.998-201.999 39.592-39.592z"/><path d="M376.818 174.825L174.819 376.824l-39.592-39.592 201.999-201.999z"/></g></svg>;
|
||||
}
|
||||
return <span />;
|
||||
}
|
||||
|
||||
getInputPasswordContent() {
|
||||
var ts = remember ? { checked: true } : {};
|
||||
// <div><button|checkbox(remember) {ts}>{my_translate('Remember password')}</button></div>
|
||||
return <div class="form">
|
||||
<div>{my_translate('Please enter your password')}</div>
|
||||
<PasswordComponent />
|
||||
<div><button type="checkbox">{my_translate('Remember password')}</button></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
if (type == "input-password") {
|
||||
return this.getInputPasswordContent();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
if (type == "input-password") {
|
||||
return "#AD448E";
|
||||
}
|
||||
if (type == "success") {
|
||||
return "#32bea6";
|
||||
}
|
||||
if (type.indexOf("error") >= 0 || type == "re-input-password") {
|
||||
return "#e04f5f";
|
||||
}
|
||||
return "#2C8CFF";
|
||||
}
|
||||
|
||||
hasSkip() {
|
||||
return type.indexOf("skip") >= 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
let color = this.getColor();
|
||||
let icon = this.getIcon(color);
|
||||
let content = this.getContent();
|
||||
let hasCancel = type.indexOf("error") < 0 && type != "success" && type.indexOf("nocancel") < 0;
|
||||
let hasOk = type != "connecting" && type.indexOf("nook") < 0;
|
||||
let hasClose = type.indexOf("hasclose") >= 0;
|
||||
let show_progress = type == "connecting";
|
||||
document.body.style.setProperty("border",(color + " solid 1px"));
|
||||
setTimeout(()=>this.$("#content").content(my_translate(content)),1);
|
||||
return (
|
||||
<div style="size: *">
|
||||
<header style={"height: 2em; background: " + color}>
|
||||
<caption role="window-caption">{my_translate(title)}</caption>
|
||||
</header>
|
||||
<div style="padding: 1em 2em; size: *;">
|
||||
<div style="height: *; flow: horizontal">
|
||||
{icon && <div style="height: *; margin: * 0; padding-right: 2em;">{icon}</div>}
|
||||
<div id="content" style={contentStyle || "size: *; margin: * 0;"} />
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span id="error" style="display:inline-block; max-width: 260px; font-size:12px;" />
|
||||
<progress id="progress" style={"color:" + color + "; display: " + (show_progress ? "inline-block" : "none")} />
|
||||
{hasCancel || retry ? <button id="cancel" class="button outline">{my_translate(retry ? "OK" : "Cancel")}</button> : ""}
|
||||
{this.hasSkip() ? <button id="skip" class="button outline">{my_translate('Skip')}</button> : ""}
|
||||
{hasOk || retry ? <button id="submit" class="button" >{my_translate(retry ? "Retry" : "OK")}</button> : ""}
|
||||
{hasClose ? <button id="cancel" class="button outline">{my_translate('Close')}</button> : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
// TEST
|
||||
["on click at .custom-event"](_, me) {
|
||||
if (callback) callback(me);
|
||||
}
|
||||
|
||||
["on click at button#cancel"]() {
|
||||
view.close();
|
||||
if (callback) callback(null);
|
||||
}
|
||||
|
||||
["on click at button#skip"]() {
|
||||
let values = getValues();
|
||||
values.skip = true;
|
||||
view.close(values);
|
||||
if (callback) callback(values);
|
||||
}
|
||||
|
||||
["on click at button#submit"](){
|
||||
if (type == "error") {
|
||||
if (retry) {
|
||||
view.close({ reconnect: true });
|
||||
} else {
|
||||
view.close();
|
||||
if (callback) callback(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == "re-input-password") {
|
||||
type = "input-password";
|
||||
body.componentUpdate();
|
||||
set_outline_focus();
|
||||
return;
|
||||
}
|
||||
var values = getValues();
|
||||
if (callback) {
|
||||
var err = callback(values, show_progress);
|
||||
if (err && !err.trim()) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
show_progress(false, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
view.close(values);
|
||||
}
|
||||
["on keydown"] (evt) { // TEST
|
||||
if (!evt.shortcutKey) {
|
||||
// TODO TEST Windows/Mac
|
||||
if (evt.code == "KeyRETURN") {
|
||||
submit();
|
||||
}
|
||||
if (evt.code == "KeyESCAPE") {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show_progress(show=1, err="") {
|
||||
if (show == -1) {
|
||||
view.close()
|
||||
return;
|
||||
}
|
||||
$("#progress").style.setProperty("display",show ? "inline-block" : "none");
|
||||
$("#error").text = err;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if ($("button#submit")) {
|
||||
$("button#submit").click(); // TEST
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if ($("button#cancel")) {
|
||||
$("button#cancel").click();
|
||||
}
|
||||
}
|
||||
|
||||
function getValues() {
|
||||
let values = { type: type };
|
||||
for (let el of $$(".form input")) {
|
||||
values[el.getAttribute("name")] = el.value;
|
||||
}
|
||||
for (let el of $$(".form textarea")) {
|
||||
values[el.getAttribute("name")] = el.value;
|
||||
}
|
||||
for (let el of $$(".form button")) {
|
||||
values[el.getAttribute("name")] = el.value;
|
||||
}
|
||||
if (type == "input-password") {
|
||||
values.password = (values.password || "").trim();
|
||||
if (!values.password) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function set_outline_focus() {
|
||||
setTimeout(function() {
|
||||
let el = $(".outline-focus");
|
||||
if (el) view.focus = el;
|
||||
else {
|
||||
el = $("#submit");
|
||||
if (el) view.focus = el;
|
||||
}
|
||||
},30);
|
||||
}
|
||||
|
||||
set_outline_focus();
|
||||
|
||||
// checkParams
|
||||
setInterval(function() {
|
||||
let tmp = getParams();
|
||||
if (!tmp || !tmp.type) {
|
||||
view.close("!alive");
|
||||
return;
|
||||
} else if (tmp != params) {
|
||||
params = tmp;
|
||||
updateParams(params);
|
||||
body.componentUpdate();
|
||||
set_outline_focus();
|
||||
}
|
||||
},30);
|
||||
|
||||
document.body.content(<Body />);
|
||||
@@ -1,265 +0,0 @@
|
||||
var type, title, text, getParams, remember, retry, callback;
|
||||
|
||||
function updateParams(params) {
|
||||
type = params.type;
|
||||
title = params.title;
|
||||
text = params.text;
|
||||
getParams = params.getParams;
|
||||
remember = params.remember;
|
||||
callback = params.callback;
|
||||
retry = params.retry;
|
||||
if (retry > 0) {
|
||||
self.timer(retry * 1000, function() {
|
||||
view.close({ reconnect: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var params = view.parameters;
|
||||
updateParams(params);
|
||||
|
||||
var svg_eye_cross = <svg viewBox="0 -21 511.96 511">
|
||||
<path d="m506.68 261.88c7.043-16.984 7.043-36.461 0-53.461-41.621-100.4-140.03-165.27-250.71-165.27-46.484 0-90.797 11.453-129.64 32.191l-68.605-68.609c-8.3438-8.3398-21.824-8.3398-30.168 0-8.3398 8.3398-8.3398 21.824 0 30.164l271.49 271.49 86.484 86.488 68.676 68.672c4.1797 4.1797 9.6406 6.2695 15.102 6.2695 5.4609 0 10.922-2.0898 15.082-6.25 8.3438-8.3398 8.3438-21.824 0-30.164l-62.145-62.145c36.633-27.883 66.094-65.109 84.438-109.38zm-293.91-100.1c12.648-7.5742 27.391-11.969 43.199-11.969 47.062 0 85.332 38.273 85.332 85.336 0 15.805-4.3945 30.547-11.969 43.199z"/>
|
||||
<path d="m255.97 320.48c-47.062 0-85.336-38.273-85.336-85.332 0-3.0938 0.59766-6.0195 0.91797-9.0039l-106.15-106.16c-25.344 24.707-46.059 54.465-60.117 88.43-7.043 16.98-7.043 36.457 0 53.461 41.598 100.39 140.01 165.27 250.69 165.27 34.496 0 67.797-6.3164 98.559-18.027l-89.559-89.559c-2.9844 0.32031-5.9062 0.91797-9 0.91797z"/>
|
||||
</svg>;
|
||||
|
||||
class Password: Reactor.Component {
|
||||
this var visible = false;
|
||||
|
||||
function render() {
|
||||
return <div .password>
|
||||
<input name="password" type={this.visible ? "text" : "password"} .outline-focus />
|
||||
{this.visible ? svg_eye_cross : svg_eye}
|
||||
</div>;
|
||||
}
|
||||
|
||||
event click $(svg) {
|
||||
var el = this.$(input);
|
||||
var value = el.value;
|
||||
var start = el.xcall(#selectionStart) || 0;
|
||||
var end = el.xcall(#selectionEnd);
|
||||
this.update({ visible: !this.visible });
|
||||
self.timer(30ms, function() {
|
||||
var el = this.$(input);
|
||||
view.focus = el;
|
||||
el.value = value;
|
||||
el.xcall(#setSelection, start, end);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var body;
|
||||
|
||||
class Body: Reactor.Component {
|
||||
function this() {
|
||||
body = this;
|
||||
}
|
||||
|
||||
function getIcon(color) {
|
||||
if (type == "input-password") {
|
||||
return <svg viewBox="0 0 505 505"><circle cx="252.5" cy="252.5" r="252.5" fill={color}/><path d="M271.9 246.1c29.2 17.5 67.6 13.6 92.7-11.5 29.7-29.7 29.7-77.8 0-107.4s-77.8-29.7-107.4 0c-25.1 25.1-29 63.5-11.5 92.7L118.1 347.4l26.2 26.2 26.4 26.4 10.6-10.6-10.1-10.1 9.7-9.7 10.1 10.1 10.6-10.6-10.1-10 9.7-9.7 10.1 10.1 10.6-10.6-26.4-26.3 76.4-76.5z" fill="#fff"/><circle cx="337.4" cy="154.4" r="17.7" fill={color}/></svg>;
|
||||
}
|
||||
if (type == "connecting") {
|
||||
return <svg viewBox="0 0 300 300"><g fill={color}><path d="m221.76 89.414h-143.51c-1.432 0-2.594 1.162-2.594 2.594v95.963c0 1.432 1.162 2.594 2.594 2.594h143.51c1.432 0 2.594-1.162 2.594-2.594v-95.964c0-1.431-1.162-2.593-2.594-2.593z"/><path d="m150 0c-82.839 0-150 67.161-150 150s67.156 150 150 150 150-67.163 150-150-67.164-150-150-150zm92.508 187.97c0 11.458-9.29 20.749-20.749 20.749h-47.144v11.588h23.801c4.298 0 7.781 3.483 7.781 7.781s-3.483 7.781-7.781 7.781h-96.826c-4.298 0-7.781-3.483-7.781-7.781s3.483-7.781 7.781-7.781h23.801v-11.588h-47.145c-11.458 0-20.749-9.29-20.749-20.749v-95.963c0-11.458 9.29-20.749 20.749-20.749h143.51c11.458 0 20.749 9.29 20.749 20.749v95.963z"/></g><path d="m169.62 154.35c-5.0276-5.0336-11.97-8.1508-19.624-8.1508-7.6551 0-14.597 3.1172-19.624 8.1508l-11.077-11.091c7.8656-7.8752 18.725-12.754 30.701-12.754s22.835 4.8788 30.701 12.754l-11.077 11.091zm-32.184 7.0728 12.56 12.576 12.56-12.576c-3.2147-3.2172-7.6555-5.208-12.56-5.208-4.9054 0-9.3457 1.9908-12.56 5.208zm12.56-39.731c14.403 0 27.464 5.8656 36.923 15.338l11.078-11.091c-12.298-12.314-29.276-19.94-48-19.94-18.724 0-35.703 7.626-48 19.94l11.077 11.091c9.4592-9.4728 22.52-15.338 36.923-15.338z" fill="#fff"/></svg>;
|
||||
}
|
||||
if (type == "success") {
|
||||
return <svg viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill={color} /><path fill="#fff" d="M235.472 392.08l-121.04-94.296 34.416-44.168 74.328 57.904 122.672-177.016 46.032 31.888z"/></svg>;
|
||||
}
|
||||
if (type.indexOf("error") >= 0 || type == "re-input-password") {
|
||||
return <svg viewBox="0 0 512 512"><ellipse cx="256" cy="256" rx="256" ry="255.832" fill={color}/><g fill="#fff"><path d="M376.812 337.18l-39.592 39.593-201.998-201.999 39.592-39.592z"/><path d="M376.818 174.825L174.819 376.824l-39.592-39.592 201.999-201.999z"/></g></svg>;
|
||||
}
|
||||
return <span />;
|
||||
}
|
||||
|
||||
function getInputPasswordContent() {
|
||||
var ts = remember ? { checked: true } : {};
|
||||
return <div .form>
|
||||
<div>Please enter your password</div>
|
||||
<Password />
|
||||
<div><button|checkbox(remember) {ts}>Remember password</button></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
if (type == "input-password") {
|
||||
return this.getInputPasswordContent();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function getColor() {
|
||||
if (type == "input-password") {
|
||||
return "#AD448E";
|
||||
}
|
||||
if (type == "success") {
|
||||
return "#32bea6";
|
||||
}
|
||||
if (type.indexOf("error") >= 0 || type == "re-input-password") {
|
||||
return "#e04f5f";
|
||||
}
|
||||
return "#2C8CFF";
|
||||
}
|
||||
|
||||
function hasSkip() {
|
||||
return type.indexOf("skip") >= 0;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var color = this.getColor();
|
||||
var icon = this.getIcon(color);
|
||||
var content = this.getContent();
|
||||
var hasCancel = type.indexOf("error") < 0 && type != "success" && type.indexOf("nocancel") < 0;
|
||||
var hasOk = type != "connecting" && type.indexOf("nook") < 0;
|
||||
var hasClose = type.indexOf("hasclose") >= 0;
|
||||
var show_progress = type == "connecting";
|
||||
self.style.set { border: color + " solid 1px" };
|
||||
var me = this;
|
||||
self.timer(1ms, function() {
|
||||
if (typeof content == "string")
|
||||
me.$(#content).html = content;
|
||||
else
|
||||
me.$(#content).content(content);
|
||||
});
|
||||
return (
|
||||
<div style="size: *">
|
||||
<header style={"height: 2em; background: " + color}>
|
||||
<caption role="window-caption">{title}</caption>
|
||||
</header>
|
||||
<div style="padding: 1em 2em; size: *;">
|
||||
<div style="height: *; flow: horizontal">
|
||||
{icon && <div style="height: *; margin: * 0; padding-right: 2em;">{icon}</div>}
|
||||
<div style="size: *; margin: * 0;" #content />
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span #error />
|
||||
{show_progress ? <progress style={"color:" + color} /> : ""}
|
||||
{hasCancel || hasRetry ? <button .button #cancel .outline>{hasRetry ? "OK" : "Cancel"}</button> : ""}
|
||||
{this.hasSkip() ? <button .button #skip .outline>Skip</button> : ""}
|
||||
{hasOk || hasRetry ? <button .button #submit>{hasRetry ? "Retry" : "OK"}</button> : ""}
|
||||
{hasClose ? <button .button #cancel .outline>Close</button> : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
event click $(.custom-event) (_, me) {
|
||||
if (callback) callback(me);
|
||||
}
|
||||
}
|
||||
|
||||
$(body).content(<Body />);
|
||||
|
||||
function submit() {
|
||||
if ($(button#submit)) {
|
||||
$(button#submit).sendEvent("click");
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if ($(button#cancel)) {
|
||||
$(button#cancel).sendEvent("click");
|
||||
}
|
||||
}
|
||||
|
||||
event click $(button#cancel) {
|
||||
view.close();
|
||||
if (callback) callback(null);
|
||||
}
|
||||
|
||||
event click $(button#skip) {
|
||||
var values = getValues();
|
||||
values.skip = true;
|
||||
view.close(values);
|
||||
if (callback) callback(values);
|
||||
}
|
||||
|
||||
function getValues() {
|
||||
var values = { type: type };
|
||||
for (var el in $$(.form input)) {
|
||||
values[el.attributes["name"]] = el.value;
|
||||
}
|
||||
for (var el in $$(.form textarea)) {
|
||||
values[el.attributes["name"]] = el.value;
|
||||
}
|
||||
for (var el in $$(.form button)) {
|
||||
values[el.attributes["name"]] = el.value;
|
||||
}
|
||||
if (type == "input-password") {
|
||||
values.password = (values.password || "").trim();
|
||||
if (!values.password) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
event click $(button#submit) {
|
||||
if (type == "error") {
|
||||
if (hasRetry) {
|
||||
view.close({ reconnect: true });
|
||||
} else {
|
||||
view.close();
|
||||
if (callback) callback(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == "re-input-password") {
|
||||
type = "input-password";
|
||||
body.update();
|
||||
set_outline_focus();
|
||||
return;
|
||||
}
|
||||
var values = getValues();
|
||||
if (callback) {
|
||||
var err = callback(values);
|
||||
if (err) {
|
||||
$(#error).text = err;
|
||||
return;
|
||||
}
|
||||
}
|
||||
view.close(values);
|
||||
}
|
||||
|
||||
event keydown (evt) {
|
||||
if (!evt.shortcutKey) {
|
||||
if (evt.keyCode == Event.VK_ENTER ||
|
||||
(is_osx && evt.keyCode == 0x4C) ||
|
||||
(is_linux && evt.keyCode == 65421)) {
|
||||
submit();
|
||||
}
|
||||
if (evt.keyCode == Event.VK_ESCAPE) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function set_outline_focus() {
|
||||
self.timer(30ms, function() {
|
||||
var el = $(input.outline-focus);
|
||||
if (el) view.focus = el;
|
||||
else {
|
||||
el = $(#submit);
|
||||
if (el) view.focus = el;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_outline_focus();
|
||||
|
||||
function checkParams() {
|
||||
self.timer(30ms, function() {
|
||||
var tmp = getParams();
|
||||
if (!tmp || !tmp.type) {
|
||||
view.close("!alive");
|
||||
return;
|
||||
} else if (tmp != params) {
|
||||
params = tmp;
|
||||
updateParams(params);
|
||||
body.update();
|
||||
set_outline_focus();
|
||||
}
|
||||
checkParams();
|
||||
});
|
||||
}
|
||||
|
||||
checkParams();
|
||||
81
src/ui/port_forward.js
Normal file
81
src/ui/port_forward.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { translate, handler } from "./common.js";
|
||||
import { svg_arrow, svg_cancel } from "./file_transfer.js";
|
||||
|
||||
class PortForward extends Element {
|
||||
render() {
|
||||
let args = handler.xcall("get_args");
|
||||
let is_rdp = handler.xcall("is_rdp");
|
||||
if (is_rdp) {
|
||||
this.pfs = [["", "", "RDP"]];
|
||||
args = ["rdp"];
|
||||
} else if (args.length) {
|
||||
this.pfs = [args];
|
||||
} else {
|
||||
this.pfs = handler.xcall("get_port_forwards");
|
||||
}
|
||||
let pfs = this.pfs.map(function(pf, i) {
|
||||
return (<tr key={i} class="value">
|
||||
<td>{is_rdp ? <button class="button" id="new-rdp">New RDP</button> : pf[0]}</td>
|
||||
<td class="right-arrow" style="text-align: center; padding-left: 0">{args.length ? svg_arrow : ""}</td>
|
||||
<td>{pf[1] || "localhost"}</td>
|
||||
<td>{pf[2]}</td>
|
||||
{args.length ? "" : <td class="remove">{svg_cancel}</td>}
|
||||
</tr>);
|
||||
});
|
||||
return <div id="file-transfer"><section>
|
||||
{pfs.length ? <div style="background: green; color: white; text-align: center; padding: 0.5em;">
|
||||
<span style="font-size: 1.2em">{translate('Listening ...')}</span><br/>
|
||||
<span style="font-size: 0.8em; color: #ddd">{translate('not_close_tcp_tip')}</span>
|
||||
</div> : ""}
|
||||
<table id="port-forward">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{translate('Local Port')}</th>
|
||||
<th style="width: 1em" />
|
||||
<th>{translate('Remote Host')}</th>
|
||||
<th>{translate('Remote Port')}</th>
|
||||
{args.length ? "" : <th style="width: 6em">{translate('Action')}</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody key={pfs.length}>
|
||||
{args.length ? "" :
|
||||
<tr>
|
||||
<td><input type="number" id="port" /></td>
|
||||
<td class="right-arrow" style="text-align: center">{svg_arrow}</td>
|
||||
<td><input type="text" id="remote-host" novalue="localhost" /></td>
|
||||
<td><input type="number" id="remote-port" /></td>
|
||||
<td style="margin:0;"><button class="button" id="add">{translate('Add')}</button></td>
|
||||
</tr>
|
||||
}
|
||||
{pfs}
|
||||
</tbody>
|
||||
</table></section></div>;
|
||||
}
|
||||
|
||||
["on click at #add"] () {
|
||||
let port = ($("#port").value || "").toInteger() || 0; // TODO toInteger
|
||||
let remote_host = $("#remote-host").value || "";
|
||||
let remote_port = ($("#remote-port").value || "").toInteger() || 0; // TODO toInteger
|
||||
if (port <= 0 || remote_port <= 0) return;
|
||||
handler.xcall("add_port_forward",port, remote_host, remote_port);
|
||||
this.componentUpdate();
|
||||
}
|
||||
|
||||
["on click at #new-rdp"] () {
|
||||
handler.xcall("new_rdp");
|
||||
}
|
||||
|
||||
["on click at .remove svg"](_, me) {
|
||||
let pf = this.pfs[me.parentElement.parentElement.index - 1];
|
||||
handler.xcall("remove_port_forward",pf[0]);
|
||||
this.componentUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export function initializePortForward()
|
||||
{
|
||||
document.$("#file-transfer-wrapper").content(<PortForward />);
|
||||
document.$("#video-wrapper").style.setProperty("visibility","hidden");
|
||||
document.$("#video-wrapper").style.setProperty("position","absolute")
|
||||
document.$("#file-transfer-wrapper").style.setProperty("display","block");
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
class PortForward: Reactor.Component {
|
||||
function render() {
|
||||
var args = handler.get_args();
|
||||
var is_rdp = handler.is_rdp();
|
||||
if (is_rdp) {
|
||||
this.pfs = [["", "", "RDP"]];
|
||||
args = ["rdp"];
|
||||
} else if (args.length) {
|
||||
this.pfs = [args];
|
||||
} else {
|
||||
this.pfs = handler.get_port_forwards();
|
||||
}
|
||||
var pfs = this.pfs.map(function(pf, i) {
|
||||
return <tr key={i} .value>
|
||||
<td>{is_rdp ? <button .button #new-rdp>New RDP</button> : pf[0]}</td>
|
||||
<td .right-arrow style="text-align: center; padding-left: 0">{args.length ? svg_arrow : ""}</td>
|
||||
<td>{pf[1] || "localhost"}</td>
|
||||
<td>{pf[2]}</td>
|
||||
{args.length ? "" : <td .remove>{svg_cancel}</td>}
|
||||
</tr>;
|
||||
});
|
||||
return <div #file-transfer><section>
|
||||
{pfs.length ? <div style="background: green; color: white; text-align: center; padding: 0.5em;">
|
||||
<span style="font-size: 1.2em">Listening ...</span><br/>
|
||||
<span style="font-size: 0.8em; color: #ddd">Don't close this window while you are using the tunnel</span>
|
||||
</div> : ""}
|
||||
<table #port-forward>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Local Port</th>
|
||||
<th style="width: 1em" />
|
||||
<th>Remote Host</th>
|
||||
<th>Remote Port</th>
|
||||
{args.length ? "" : <th style="width: 6em">Action</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody key={pfs.length}>
|
||||
{args.length ? "" :
|
||||
<tr>
|
||||
<td><input|number #port /></td>
|
||||
<td .right-arrow style="text-align: center">{svg_arrow}</td>
|
||||
<td><input|text #remote-host novalue="localhost" /></td>
|
||||
<td><input|number #remote-port /></td>
|
||||
<td style="margin:0;"><button .button #add>Add</button></td>
|
||||
</tr>
|
||||
}
|
||||
{pfs}
|
||||
</tbody>
|
||||
</table></section></div>;
|
||||
}
|
||||
|
||||
event click $(#add) () {
|
||||
var port = ($(#port).value || "").toInteger() || 0;
|
||||
var remote_host = $(#remote-host).value || "";
|
||||
var remote_port = ($(#remote-port).value || "").toInteger() || 0;
|
||||
if (port <= 0 || remote_port <= 0) return;
|
||||
handler.add_port_forward(port, remote_host, remote_port);
|
||||
this.update();
|
||||
}
|
||||
|
||||
event click $(#new-rdp) {
|
||||
handler.new_rdp();
|
||||
}
|
||||
|
||||
event click $(.remove svg) (_, me) {
|
||||
var pf = this.pfs[me.parent.parent.index - 1];
|
||||
handler.remove_port_forward(pf[0]);
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
function initializePortForward()
|
||||
{
|
||||
$(#file-transfer-wrapper).content(<PortForward />);
|
||||
$(#video-wrapper).style.set { visibility: "hidden", position: "absolute" };
|
||||
$(#file-transfer-wrapper).style.set { display: "block" };
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
<html window-resizable window-frame="extended">
|
||||
<head>
|
||||
<style>
|
||||
<link rel="stylesheet" href="common.css">
|
||||
<link rel="stylesheet" href="remote.css">
|
||||
<link rel="stylesheet" href="file_transfer.css">
|
||||
<link rel="stylesheet" href="header.css">
|
||||
<!-- <style>
|
||||
@import url(common.css);
|
||||
@import url(remote.css);
|
||||
@import url(file_transfer.css);
|
||||
@import url(header.css);
|
||||
</style>
|
||||
<script type="text/tiscript">
|
||||
</style> -->
|
||||
<script type="module" src="remote.js"></script>
|
||||
<!-- <script type="text/tiscript">
|
||||
include "common.tis";
|
||||
include "remote.tis";
|
||||
include "file_transfer.tis";
|
||||
include "port_forward.tis";
|
||||
include "grid.tis";
|
||||
include "header.tis";
|
||||
</script>
|
||||
</script> -->
|
||||
</head>
|
||||
<header>
|
||||
<div.window-icon role="window-icon"><icon /></div>
|
||||
|
||||
470
src/ui/remote.js
Normal file
470
src/ui/remote.js
Normal file
@@ -0,0 +1,470 @@
|
||||
import { $ } from "@sciter";
|
||||
import { handler,view,isReasonableSize,msgbox,is_osx, is_linux, centerize, connecting } from "./common.js";
|
||||
import { initializeFileTransfer, save_file_transfer_close_state } from "./file_transfer.js";
|
||||
import { initializePortForward } from "./port_forward.js";
|
||||
import { header } from "./header.js";
|
||||
|
||||
const body = document.body;
|
||||
var cursor_img = $("img#cursor");
|
||||
var last_key_time = 0;
|
||||
var display_width = 0;
|
||||
var display_height = 0;
|
||||
var display_origin_x = 0;
|
||||
var display_origin_y = 0;
|
||||
var display_scale = 1;
|
||||
export var keyboard_enabled = true; // server side
|
||||
export var clipboard_enabled = true; // server side
|
||||
export var audio_enabled = true; // server side
|
||||
|
||||
handler.is_port_forward = handler.xcall("is_port_forward");
|
||||
handler.is_file_transfer = handler.xcall("is_file_transfer");
|
||||
|
||||
handler.setDisplay = function(x, y, w, h) {
|
||||
display_width = w;
|
||||
display_height = h;
|
||||
display_origin_x = x;
|
||||
display_origin_y = y;
|
||||
adaptDisplay();
|
||||
}
|
||||
|
||||
export function adaptDisplay() {
|
||||
let w = display_width;
|
||||
let h = display_height;
|
||||
if (!w || !h) return;
|
||||
let style = handler.xcall("get_view_style");
|
||||
display_scale = 1.;
|
||||
let [sx, sy, sw, sh] = view.screenBox(view.state == Window.WINDOW_FULL_SCREEN ? "frame" : "workarea", "rectw");
|
||||
if (sw >= w && sh > h) {
|
||||
let hh = $("header").state.box("height", "border");
|
||||
let el = $("div#adjust-window");
|
||||
if (sh > h + hh && el) {
|
||||
el.style.setProperty("display","block");
|
||||
el = $("li#adjust-window");
|
||||
el.style.setProperty("display","block");
|
||||
el.on("click",function() {
|
||||
view.state = Window.WINDOW_SHOWN;
|
||||
let [x, y] = body.state.box("position", "border", "screen"); // TEST
|
||||
// extra for border
|
||||
let extra = 2;
|
||||
view.move(x, y, w + extra, h + hh + extra);
|
||||
})
|
||||
}
|
||||
}
|
||||
if (style != "original") {
|
||||
let bw = body.state.box("width", "border");
|
||||
let bh = body.state.box("height", "border");
|
||||
if (view.state == Window.WINDOW_FULL_SCREEN) {
|
||||
bw = sw;
|
||||
bh = sh;
|
||||
}
|
||||
if (bw > 0 && bh > 0) {
|
||||
// TEST del toFloat()
|
||||
let scale_x = bw / w;
|
||||
let scale_y = bh / h;
|
||||
let scale = scale_x < scale_y ? scale_x : scale_y;
|
||||
if ((scale > 1 && style == "stretch") ||
|
||||
(scale < 1 && style == "shrink")) {
|
||||
display_scale = scale;
|
||||
w = w * scale;
|
||||
h = h * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.style.setProperty("width",w + "px");
|
||||
handler.style.setProperty("height",h + "px")
|
||||
}
|
||||
|
||||
// https://sciter.com/event-handling/
|
||||
// https://sciter.com/docs/content/sciter/Event.htm
|
||||
|
||||
var entered = false;
|
||||
|
||||
// TODO !
|
||||
// var keymap = {};
|
||||
// for (var (k, v) in Event) {
|
||||
// k = k + ""
|
||||
// if (k[0] == "V" && k[1] == "K") {
|
||||
// keymap[v] = k;
|
||||
// }
|
||||
// }
|
||||
|
||||
// VK_ENTER = VK_RETURN
|
||||
// somehow, handler.onKey and view.onKey not working
|
||||
// function self.onKey(evt) {
|
||||
// last_key_time = getTime();
|
||||
// if (is_file_transfer || is_port_forward) return false;
|
||||
// if (!entered) return false;
|
||||
// if (!keyboard_enabled) return false;
|
||||
// switch (evt.type) {
|
||||
// case Event.KEY_DOWN:
|
||||
// handler.key_down_or_up(1, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
// evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
// if (is_osx && evt.commandKey) {
|
||||
// handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
// evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
// }
|
||||
// break;
|
||||
// case Event.KEY_UP:
|
||||
// handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
// evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
// break;
|
||||
// case Event.KEY_CHAR:
|
||||
// // the keypress event is fired when the element receives character value. Event.keyCode is a UNICODE code point of the character
|
||||
// handler.key_down_or_up(2, "", evt.keyCode, evt.altKey,
|
||||
// evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
// break;
|
||||
// default:
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
var wait_window_toolbar = false;
|
||||
var last_mouse_mask;
|
||||
var acc_wheel_delta_x = 0;
|
||||
var acc_wheel_delta_y = 0;
|
||||
var last_wheel_time = 0;
|
||||
var inertia_velocity_x = 0;
|
||||
var inertia_velocity_y = 0;
|
||||
var acc_wheel_delta_x0 = 0;
|
||||
var acc_wheel_delta_y0 = 0;
|
||||
var total_wheel_time = 0;
|
||||
var wheeling = false;
|
||||
var dragging = false;
|
||||
|
||||
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
|
||||
function resetWheel() {
|
||||
acc_wheel_delta_x = 0;
|
||||
acc_wheel_delta_y = 0;
|
||||
last_wheel_time = 0;
|
||||
inertia_velocity_x = 0;
|
||||
inertia_velocity_y = 0;
|
||||
acc_wheel_delta_x0 = 0;
|
||||
acc_wheel_delta_y0 = 0;
|
||||
total_wheel_time = 0;
|
||||
wheeling = false;
|
||||
}
|
||||
|
||||
var INERTIA_ACCELERATION = 30;
|
||||
|
||||
// not good, precision not enough to simulate accelation effect,
|
||||
// seems have to use pixel based rather line based delta
|
||||
function accWheel(v, is_x) {
|
||||
if (wheeling) return;
|
||||
var abs_v = Math.abs(v);
|
||||
var max_t = abs_v / INERTIA_ACCELERATION;
|
||||
for (var t = 0.1; t < max_t; t += 0.1) {
|
||||
var d = Math.round((abs_v - t * INERTIA_ACCELERATION / 2) * t).toInteger();
|
||||
if (d >= 1) {
|
||||
abs_v -= t * INERTIA_ACCELERATION;
|
||||
if (v < 0) {
|
||||
d = -d;
|
||||
v = -abs_v;
|
||||
} else {
|
||||
v = abs_v;
|
||||
}
|
||||
handler.xcall("send_mouse",3, is_x ? d : 0, !is_x ? d : 0, false, false, false, false);
|
||||
accWheel(v, is_x);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // TODO
|
||||
// handler.onMouse(evt)
|
||||
// {
|
||||
// if (is_file_transfer || is_port_forward) return false;
|
||||
// if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
||||
// var dy = evt.y - scroll_body.scroll(#top);
|
||||
// if (dy <= 1) {
|
||||
// if (!wait_window_toolbar) {
|
||||
// wait_window_toolbar = true;
|
||||
// self.timer(300ms, function() {
|
||||
// if (!wait_window_toolbar) return;
|
||||
// var extra = 0;
|
||||
// // workaround for stupid Sciter, without this, click
|
||||
// // event not triggered on top part of buttons on toolbar
|
||||
// if (is_osx) extra = 10;
|
||||
// if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
// $(header).style.set {
|
||||
// display: "block",
|
||||
// padding: (2 * workarea_offset + extra) + "px 0 0 0",
|
||||
// };
|
||||
// }
|
||||
// wait_window_toolbar = false;
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// wait_window_toolbar = false;
|
||||
// var h = $(header).style;
|
||||
// if (dy > 20 && h#display != "none") {
|
||||
// h.set {
|
||||
// display: "none",
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (!got_mouse_control) {
|
||||
// if (Math.abs(evt.x - cur_local_x) > 12 || Math.abs(evt.y - cur_local_y) > 12) {
|
||||
// got_mouse_control = true;
|
||||
// } else {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// var mask = 0;
|
||||
// var wheel_delta_x;
|
||||
// var wheel_delta_y;
|
||||
// switch(evt.type) {
|
||||
// case Event.MOUSE_DOWN:
|
||||
// mask = 1;
|
||||
// dragging = true;
|
||||
// break;
|
||||
// case Event.MOUSE_UP:
|
||||
// mask = 2;
|
||||
// dragging = false;
|
||||
// break;
|
||||
// case Event.MOUSE_MOVE:
|
||||
// if (cursor_img.style#display != "none" && keyboard_enabled) {
|
||||
// cursor_img.style#display = "none";
|
||||
// handler.style#cursor = '';
|
||||
// }
|
||||
// break;
|
||||
// case Event.MOUSE_WHEEL:
|
||||
// // mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
|
||||
// mask = 3;
|
||||
// {
|
||||
// var (dx, dy) = evt.wheelDeltas;
|
||||
// if (dx > 0) dx = 1;
|
||||
// else if (dx < 0) dx = -1;
|
||||
// if (dy > 0) dy = 1;
|
||||
// else if (dy < 0) dy = -1;
|
||||
// if (Math.abs(dx) > Math.abs(dy)) {
|
||||
// dy = 0;
|
||||
// } else {
|
||||
// dx = 0;
|
||||
// }
|
||||
// acc_wheel_delta_x += dx;
|
||||
// acc_wheel_delta_y += dy;
|
||||
// wheel_delta_x = acc_wheel_delta_x.toInteger();
|
||||
// wheel_delta_y = acc_wheel_delta_y.toInteger();
|
||||
// acc_wheel_delta_x -= wheel_delta_x;
|
||||
// acc_wheel_delta_y -= wheel_delta_y;
|
||||
// var now = getTime();
|
||||
// var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
|
||||
// if (dt > 0) {
|
||||
// var vx = dx / dt;
|
||||
// var vy = dy / dt;
|
||||
// if (vx != 0 || vy != 0) {
|
||||
// inertia_velocity_x = vx;
|
||||
// inertia_velocity_y = vy;
|
||||
// }
|
||||
// }
|
||||
// acc_wheel_delta_x0 += dx;
|
||||
// acc_wheel_delta_y0 += dy;
|
||||
// total_wheel_time += dt;
|
||||
// if (dx == 0 && dy == 0) {
|
||||
// wheeling = false;
|
||||
// if (dt < 0.1 && total_wheel_time > 0) {
|
||||
// var v2 = (acc_wheel_delta_y0 / total_wheel_time) * inertia_velocity_y;
|
||||
// if (v2 > 0) {
|
||||
// v2 = Math.sqrt(v2);
|
||||
// inertia_velocity_y = inertia_velocity_y < 0 ? -v2 : v2;
|
||||
// accWheel(inertia_velocity_y, false);
|
||||
// }
|
||||
// v2 = (acc_wheel_delta_x0 / total_wheel_time) * inertia_velocity_x;
|
||||
// if (v2 > 0) {
|
||||
// v2 = Math.sqrt(v2);
|
||||
// inertia_velocity_x = inertia_velocity_x < 0 ? -v2 : v2;
|
||||
// accWheel(inertia_velocity_x, true);
|
||||
// }
|
||||
// }
|
||||
// resetWheel();
|
||||
// } else {
|
||||
// wheeling = true;
|
||||
// }
|
||||
// last_wheel_time = now;
|
||||
// if (wheel_delta_x == 0 && wheel_delta_y == 0) return keyboard_enabled;
|
||||
// }
|
||||
// break;
|
||||
// case Event.MOUSE_DCLICK: // seq: down, up, dclick, up
|
||||
// mask = 1;
|
||||
// break;
|
||||
// case Event.MOUSE_ENTER:
|
||||
// entered = true;
|
||||
// console.log("enter");
|
||||
// return keyboard_enabled;
|
||||
// case Event.MOUSE_LEAVE:
|
||||
// entered = false;
|
||||
// console.log("leave");
|
||||
// return keyboard_enabled;
|
||||
// default:
|
||||
// return false;
|
||||
// }
|
||||
// var x = evt.x;
|
||||
// var y = evt.y;
|
||||
// if (mask != 0) {
|
||||
// // to gain control of the mouse, user must move mouse
|
||||
// if (cur_x != x || cur_y != y) {
|
||||
// return keyboard_enabled;
|
||||
// }
|
||||
// // save bandwidth
|
||||
// x = 0;
|
||||
// y = 0;
|
||||
// } else {
|
||||
// cur_local_x = cur_x = x;
|
||||
// cur_local_y = cur_y = y;
|
||||
// }
|
||||
// if (mask != 3) {
|
||||
// resetWheel();
|
||||
// }
|
||||
// if (!keyboard_enabled) return false;
|
||||
// x = (x / display_scale).toInteger();
|
||||
// y = (y / display_scale).toInteger();
|
||||
// // insert down between two up, osx has this behavior for triple click
|
||||
// if (last_mouse_mask == 2 && mask == 2) {
|
||||
// handler.send_mouse((evt.buttons << 3) | 1, x + display_origin_x, y + display_origin_y, evt.altKey,
|
||||
// evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
// }
|
||||
// last_mouse_mask = mask;
|
||||
// // to-do: altKey, ctrlKey etc
|
||||
// handler.send_mouse((evt.buttons << 3) | mask,
|
||||
// mask == 3 ? wheel_delta_x : x + display_origin_x,
|
||||
// mask == 3 ? wheel_delta_y : y + display_origin_y,
|
||||
// evt.altKey,
|
||||
// evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
// return true;
|
||||
// };
|
||||
|
||||
var cur_hotx = 0;
|
||||
var cur_hoty = 0;
|
||||
var cur_img = null;
|
||||
var cur_x = 0;
|
||||
var cur_y = 0;
|
||||
var cur_local_x = 0;
|
||||
var cur_local_y = 0;
|
||||
var cursors = {};
|
||||
var image_binded;
|
||||
|
||||
handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
||||
cur_hotx = hotx;
|
||||
cur_hoty = hoty;
|
||||
cursor_img.style.setProperty("width",width + "px");
|
||||
cursor_img.style.setProperty("height",height + "px");
|
||||
|
||||
let img = Image.fromBytes(colors);
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cursors[id] = [img, hotx, hoty, width, height];
|
||||
this.bindImage("in-memory:cursor", img);
|
||||
if (cursor_img.style.getProperty("display") == 'none') { // TEST getProperty
|
||||
setTimeout(function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty) },1); // TODO cursor
|
||||
}
|
||||
cur_img = img;
|
||||
}
|
||||
}
|
||||
|
||||
handler.setCursorId = function(id) {
|
||||
let img = cursors[id];
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cur_hotx = img[1];
|
||||
cur_hoty = img[2];
|
||||
cursor_img.style.setProperty("width",img[3] + "px");
|
||||
cursor_img.style.setProperty("height",img[4] + "px");
|
||||
img = img[0];
|
||||
this.bindImage("in-memory:cursor", img);
|
||||
if (cursor_img.style.getProperty("display") == 'none') {
|
||||
setTimeout(function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty) },1);
|
||||
}
|
||||
cur_img = img;
|
||||
}
|
||||
}
|
||||
|
||||
var got_mouse_control = true;
|
||||
handler.setCursorPosition = function(x, y) {
|
||||
if (!image_binded) return;
|
||||
got_mouse_control = false;
|
||||
cur_x = x - display_origin_x;
|
||||
cur_y = y - display_origin_y;
|
||||
var x = cur_x - cur_hotx;
|
||||
var y = cur_y - cur_hoty;
|
||||
x *= display_scale;
|
||||
y *= display_scale;
|
||||
cursor_img.style.setProperty("width",x + "px");
|
||||
cursor_img.style.setProperty("width",y + "px");
|
||||
if (cursor_img.style.getProperty("display") == 'none') {
|
||||
handler.style.setProperty("cursor",'none');
|
||||
cursor_img.style.setProperty("display","block");
|
||||
}
|
||||
}
|
||||
|
||||
document.on("ready",()=> {
|
||||
let w = 960;
|
||||
let h = 640;
|
||||
if (handler.is_file_transfer || handler.is_port_forward) {
|
||||
let r = handler.xcall("get_size");
|
||||
if (isReasonableSize(r) && r[2] > 0) {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
} else {
|
||||
centerize(w, h);
|
||||
}
|
||||
} else {
|
||||
centerize(w, h);
|
||||
}
|
||||
if (!handler.is_port_forward) connecting();
|
||||
if (handler.is_file_transfer) initializeFileTransfer();
|
||||
if (handler.is_port_forward) initializePortForward();
|
||||
})
|
||||
|
||||
var workarea_offset = 0;
|
||||
var size_adapted;
|
||||
handler.adaptSize = function() {
|
||||
if (size_adapted) return;
|
||||
size_adapted = true;
|
||||
let [sx, sy, sw, sh] = view.screenBox("workarea", "rectw");
|
||||
let [fx, fy, fw, fh] = view.screenBox("frame", "rectw");
|
||||
if (is_osx) workarea_offset = sy;
|
||||
let r = handler.xcall("get_size");
|
||||
if (isReasonableSize(r) && r[2] > 0) {
|
||||
if (r[2] >= fw && r[3] >= fh && !is_linux) {
|
||||
view.state = Window.WINDOW_FULL_SCREEN;
|
||||
console.log("Initialize to full screen");
|
||||
} else if (r[2] >= sw && r[3] >= sh) {
|
||||
view.state = Window.WINDOW_MAXIMIZED;
|
||||
console.log("Initialize to full screen");
|
||||
} else {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
}
|
||||
} else {
|
||||
let w = handler.state.box("width", "border")
|
||||
let h = handler.state.box("height", "border")
|
||||
if (w >= sw || h >= sh) {
|
||||
view.state = Window.WINDOW_MAXIMIZED;
|
||||
return;
|
||||
}
|
||||
// extra for border
|
||||
let extra = 2;
|
||||
centerize(w + extra, handler.state.box("height", "border") + h + extra);
|
||||
}
|
||||
}
|
||||
|
||||
document.on("unloadequest",()=> {
|
||||
let [x, y, w, h] = body.state.box("rectw", "border", "screen");
|
||||
console.log("remote.js unloadequest",x, y, w, h)
|
||||
if (handler.is_file_transfer) save_file_transfer_close_state();
|
||||
if (handler.is_file_transfer || handler.is_port_forward || size_adapted) handler.xcall("save_size",x, y, w, h);
|
||||
})
|
||||
|
||||
handler.setPermission = function(name, enabled) {
|
||||
if (name == "keyboard") keyboard_enabled = enabled;
|
||||
if (name == "audio") audio_enabled = enabled;
|
||||
if (name == "clipboard") clipboard_enabled = enabled;
|
||||
header.componentUpdate();
|
||||
}
|
||||
|
||||
// TODO
|
||||
handler.closeSuccess = function() {
|
||||
// handler.msgbox("success", "Successful", "Ready to go.");
|
||||
console.log("remote.js handler.closeSuccess");
|
||||
handler.msgbox("", "", "");
|
||||
}
|
||||
@@ -54,7 +54,6 @@ pub struct HandlerInner {
|
||||
sender: Option<mpsc::UnboundedSender<Data>>,
|
||||
thread: Option<std::thread::JoinHandle<()>>,
|
||||
close_state: HashMap<String, String>,
|
||||
last_down_key: Option<(String, i32, bool)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -64,7 +63,6 @@ pub struct Handler {
|
||||
id: String,
|
||||
args: Vec<String>,
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
super_on: bool,
|
||||
}
|
||||
|
||||
impl Deref for Handler {
|
||||
@@ -77,7 +75,7 @@ impl Deref for Handler {
|
||||
|
||||
impl sciter::EventHandler for Handler {
|
||||
fn get_subscription(&mut self) -> Option<EVENT_GROUPS> {
|
||||
Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT)
|
||||
Some(EVENT_GROUPS::HANDLE_METHOD_CALL | EVENT_GROUPS::HANDLE_SCRIPTING_METHOD_CALL | EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT)
|
||||
}
|
||||
|
||||
fn attached(&mut self, root: HELEMENT) {
|
||||
@@ -146,6 +144,8 @@ impl sciter::EventHandler for Handler {
|
||||
fn get_id();
|
||||
fn get_default_pi();
|
||||
fn get_option(String);
|
||||
fn t(String);
|
||||
fn set_option(String, String);
|
||||
fn save_close_state(String, String);
|
||||
fn is_file_transfer();
|
||||
fn is_port_forward();
|
||||
@@ -155,9 +155,6 @@ impl sciter::EventHandler for Handler {
|
||||
fn send_mouse(i32, i32, i32, bool, bool, bool, bool);
|
||||
fn key_down_or_up(bool, String, i32, bool, bool, bool, bool, bool);
|
||||
fn ctrl_alt_del();
|
||||
fn ctrl_space();
|
||||
fn alt_tab();
|
||||
fn super_x();
|
||||
fn transfer_file();
|
||||
fn tunnel();
|
||||
fn lock_screen();
|
||||
@@ -286,6 +283,10 @@ impl Handler {
|
||||
self.lc.read().unwrap().remember
|
||||
}
|
||||
|
||||
fn t(&self, name: String) -> String {
|
||||
crate::client::translate(name)
|
||||
}
|
||||
|
||||
fn is_xfce(&self) -> bool {
|
||||
crate::platform::is_xfce()
|
||||
}
|
||||
@@ -408,6 +409,10 @@ impl Handler {
|
||||
self.lc.read().unwrap().get_option(&k)
|
||||
}
|
||||
|
||||
fn set_option(&self, k: String, v: String) {
|
||||
self.lc.write().unwrap().set_option(k, v);
|
||||
}
|
||||
|
||||
fn save_close_state(&self, k: String, v: String) {
|
||||
self.write().unwrap().close_state.insert(k, v);
|
||||
}
|
||||
@@ -534,7 +539,6 @@ impl Handler {
|
||||
fn reconnect(&mut self) {
|
||||
let cloned = self.clone();
|
||||
let mut lock = self.write().unwrap();
|
||||
lock.last_down_key.take();
|
||||
lock.thread.take().map(|t| t.join());
|
||||
lock.thread = Some(std::thread::spawn(move || {
|
||||
io_loop(cloned);
|
||||
@@ -667,7 +671,6 @@ impl Handler {
|
||||
let evt_type = mask & 0x7;
|
||||
if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" {
|
||||
self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -694,8 +697,8 @@ impl Handler {
|
||||
let mut key_event = KeyEvent::new();
|
||||
if down_or_up == 2 {
|
||||
/* windows send both keyup/keydown and keychar, so here we avoid keychar
|
||||
for <= 0xFF, best practise should only avoid those not on keyboard, but
|
||||
for now, we have no way to test, so avoid <= 0xFF totaly
|
||||
for <= 0xFF, best practice should only avoid those not on keyboard, but
|
||||
for now, we have no way to test, so avoid <= 0xFF totally
|
||||
*/
|
||||
if code <= 0xFF {
|
||||
return None;
|
||||
@@ -753,6 +756,7 @@ impl Handler {
|
||||
65300 => key_event.set_control_key(ControlKey::Scroll),
|
||||
65421 => key_event.set_control_key(ControlKey::NumpadEnter), // numpad enter
|
||||
65407 => key_event.set_control_key(ControlKey::NumLock),
|
||||
65515 => key_event.set_control_key(ControlKey::Meta),
|
||||
65516 => key_event.set_control_key(ControlKey::RWin),
|
||||
65513 => key_event.set_control_key(ControlKey::Alt),
|
||||
65514 => key_event.set_control_key(ControlKey::RAlt),
|
||||
@@ -819,20 +823,6 @@ impl Handler {
|
||||
}
|
||||
}
|
||||
|
||||
fn super_x(&mut self) {
|
||||
self.super_on = true;
|
||||
}
|
||||
|
||||
fn ctrl_space(&mut self) {
|
||||
let key = "VK_SPACE".to_owned();
|
||||
self.key_down_or_up(3, key, 0, false, true, false, false, false);
|
||||
}
|
||||
|
||||
fn alt_tab(&mut self) {
|
||||
let key = "VK_TAB".to_owned();
|
||||
self.key_down_or_up(3, key, 0, true, false, false, false, false);
|
||||
}
|
||||
|
||||
fn lock_screen(&mut self) {
|
||||
let lock = "LOCK_SCREEN".to_owned();
|
||||
self.key_down_or_up(1, lock, 0, false, false, false, false, false);
|
||||
@@ -865,6 +855,17 @@ impl Handler {
|
||||
command: bool,
|
||||
extended: bool,
|
||||
) {
|
||||
if self.peer_platform() == "Windows" {
|
||||
if ctrl && alt && name == "VK_DELETE" {
|
||||
self.ctrl_alt_del();
|
||||
return;
|
||||
}
|
||||
if command && name == "VK_L" {
|
||||
self.lock_screen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// extended: e.g. ctrl key on right side, https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-keybd_event
|
||||
// not found api of osx and xdo
|
||||
log::debug!(
|
||||
@@ -880,18 +881,10 @@ impl Handler {
|
||||
extended,
|
||||
);
|
||||
|
||||
let mut command = command;
|
||||
if self.super_on {
|
||||
command = true;
|
||||
}
|
||||
|
||||
if down_or_up == 0 {
|
||||
self.super_on = false;
|
||||
}
|
||||
|
||||
let mut name = name;
|
||||
#[cfg(target_os = "linux")]
|
||||
if code == 65383 { // VK_MENU
|
||||
if code == 65383 {
|
||||
// VK_MENU
|
||||
name = "Apps".to_owned();
|
||||
}
|
||||
|
||||
@@ -1435,16 +1428,16 @@ impl Remote {
|
||||
if elapsed <= 0 {
|
||||
return;
|
||||
}
|
||||
let transfered = job.transfered();
|
||||
let last_transfered = {
|
||||
let transferred = job.transferred();
|
||||
let last_transferred = {
|
||||
if let Some(v) = last_update_jobs_status.1.get(&job.id()) {
|
||||
v.to_owned()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
last_update_jobs_status.1.insert(job.id(), transfered);
|
||||
let speed = (transfered - last_transfered) as f64 / (elapsed as f64 / 1000.);
|
||||
last_update_jobs_status.1.insert(job.id(), transferred);
|
||||
let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.);
|
||||
let file_num = job.file_num() - 1;
|
||||
handler.call(
|
||||
"jobProgress",
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
var cursor_img = $(img#cursor);
|
||||
var last_key_time = 0;
|
||||
is_file_transfer = handler.is_file_transfer();
|
||||
var is_port_forward = handler.is_port_forward();
|
||||
var display_width = 0;
|
||||
var display_height = 0;
|
||||
var display_origin_x = 0;
|
||||
var display_origin_y = 0;
|
||||
var display_scale = 1;
|
||||
var keyboard_enabled = true; // server side
|
||||
var clipboard_enabled = true; // server side
|
||||
var audio_enabled = true; // server side
|
||||
var scroll_body = $(body);
|
||||
|
||||
handler.setDisplay = function(x, y, w, h) {
|
||||
display_width = w;
|
||||
display_height = h;
|
||||
display_origin_x = x;
|
||||
display_origin_y = y;
|
||||
adaptDisplay();
|
||||
}
|
||||
|
||||
function adaptDisplay() {
|
||||
var w = display_width;
|
||||
var h = display_height;
|
||||
if (!w || !h) return;
|
||||
var style = handler.get_view_style();
|
||||
display_scale = 1.;
|
||||
var (sx, sy, sw, sh) = view.screenBox(view.windowState == View.WINDOW_FULL_SCREEN ? #frame : #workarea, #rectw);
|
||||
if (sw >= w && sh > h) {
|
||||
var hh = $(header).box(#height, #border);
|
||||
var el = $(div#adjust-window);
|
||||
if (sh > h + hh && el) {
|
||||
el.style.set{ display: "block" };
|
||||
el = $(li#adjust-window);
|
||||
el.style.set{ display: "block" };
|
||||
el.onClick = function() {
|
||||
view.windowState == View.WINDOW_SHOWN;
|
||||
var (x, y) = view.box(#position, #border, #screen);
|
||||
// extra for border
|
||||
var extra = 2;
|
||||
view.move(x, y, w + extra, h + hh + extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (style != "original") {
|
||||
var bw = $(body).box(#width, #border);
|
||||
var bh = $(body).box(#height, #border);
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
bw = sw;
|
||||
bh = sh;
|
||||
}
|
||||
if (bw > 0 && bh > 0) {
|
||||
var scale_x = bw.toFloat() / w;
|
||||
var scale_y = bh.toFloat() / h;
|
||||
var scale = scale_x < scale_y ? scale_x : scale_y;
|
||||
if ((scale > 1 && style == "stretch") ||
|
||||
(scale < 1 && style == "shrink")) {
|
||||
display_scale = scale;
|
||||
w = w * scale;
|
||||
h = h * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.style.set {
|
||||
width: w + "px",
|
||||
height: h + "px",
|
||||
};
|
||||
}
|
||||
|
||||
// https://sciter.com/event-handling/
|
||||
// https://sciter.com/docs/content/sciter/Event.htm
|
||||
|
||||
var entered = false;
|
||||
|
||||
var keymap = {};
|
||||
for (var (k, v) in Event) {
|
||||
k = k + ""
|
||||
if (k[0] == "V" && k[1] == "K") {
|
||||
keymap[v] = k;
|
||||
}
|
||||
}
|
||||
|
||||
// VK_ENTER = VK_RETURN
|
||||
// somehow, handler.onKey and view.onKey not working
|
||||
function self.onKey(evt) {
|
||||
last_key_time = getTime();
|
||||
if (is_file_transfer || is_port_forward) return false;
|
||||
if (!entered) return false;
|
||||
if (!keyboard_enabled) return false;
|
||||
switch (evt.type) {
|
||||
case Event.KEY_DOWN:
|
||||
handler.key_down_or_up(1, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
if (is_osx && evt.commandKey) {
|
||||
handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
}
|
||||
break;
|
||||
case Event.KEY_UP:
|
||||
handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
break;
|
||||
case Event.KEY_CHAR:
|
||||
// the keypress event is fired when the element receives character value. Event.keyCode is a UNICODE code point of the character
|
||||
handler.key_down_or_up(2, "", evt.keyCode, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var wait_window_toolbar = false;
|
||||
var last_mouse_mask;
|
||||
var acc_wheel_delta_x = 0;
|
||||
var acc_wheel_delta_y = 0;
|
||||
var last_wheel_time = 0;
|
||||
var inertia_velocity_x = 0;
|
||||
var inertia_velocity_y = 0;
|
||||
var acc_wheel_delta_x0 = 0;
|
||||
var acc_wheel_delta_y0 = 0;
|
||||
var total_wheel_time = 0;
|
||||
var wheeling = false;
|
||||
var dragging = false;
|
||||
|
||||
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
|
||||
function resetWheel() {
|
||||
acc_wheel_delta_x = 0;
|
||||
acc_wheel_delta_y = 0;
|
||||
last_wheel_time = 0;
|
||||
inertia_velocity_x = 0;
|
||||
inertia_velocity_y = 0;
|
||||
acc_wheel_delta_x0 = 0;
|
||||
acc_wheel_delta_y0 = 0;
|
||||
total_wheel_time = 0;
|
||||
wheeling = false;
|
||||
}
|
||||
|
||||
var INERTIA_ACCELERATION = 30;
|
||||
|
||||
// not good, precision not enough to simulate accelation effect,
|
||||
// seems have to use pixel based rather line based delta
|
||||
function accWheel(v, is_x) {
|
||||
if (wheeling) return;
|
||||
var abs_v = Math.abs(v);
|
||||
var max_t = abs_v / INERTIA_ACCELERATION;
|
||||
for (var t = 0.1; t < max_t; t += 0.1) {
|
||||
var d = Math.round((abs_v - t * INERTIA_ACCELERATION / 2) * t).toInteger();
|
||||
if (d >= 1) {
|
||||
abs_v -= t * INERTIA_ACCELERATION;
|
||||
if (v < 0) {
|
||||
d = -d;
|
||||
v = -abs_v;
|
||||
} else {
|
||||
v = abs_v;
|
||||
}
|
||||
handler.send_mouse(3, is_x ? d : 0, !is_x ? d : 0, false, false, false, false);
|
||||
accWheel(v, is_x);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handler.onMouse(evt)
|
||||
{
|
||||
if (is_file_transfer || is_port_forward) return false;
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
||||
var dy = evt.y - scroll_body.scroll(#top);
|
||||
if (dy <= 1) {
|
||||
if (!wait_window_toolbar) {
|
||||
wait_window_toolbar = true;
|
||||
self.timer(300ms, function() {
|
||||
if (!wait_window_toolbar) return;
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN) {
|
||||
$(header).style.set {
|
||||
display: "block",
|
||||
padding: (2 * workarea_offset) + "px 0 0 0",
|
||||
};
|
||||
}
|
||||
wait_window_toolbar = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
wait_window_toolbar = false;
|
||||
var h = $(header).style;
|
||||
if (dy > 20 && h#display != "none") {
|
||||
h.set {
|
||||
display: "none",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!got_mouse_control) {
|
||||
if (Math.abs(evt.x - cur_local_x) > 12 || Math.abs(evt.y - cur_local_y) > 12) {
|
||||
got_mouse_control = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var mask = 0;
|
||||
var wheel_delta_x;
|
||||
var wheel_delta_y;
|
||||
switch(evt.type) {
|
||||
case Event.MOUSE_DOWN:
|
||||
mask = 1;
|
||||
dragging = true;
|
||||
break;
|
||||
case Event.MOUSE_UP:
|
||||
mask = 2;
|
||||
dragging = false;
|
||||
break;
|
||||
case Event.MOUSE_MOVE:
|
||||
if (cursor_img.style#display != "none" && keyboard_enabled) {
|
||||
cursor_img.style#display = "none";
|
||||
handler.style#cursor = '';
|
||||
}
|
||||
break;
|
||||
case Event.MOUSE_WHEEL:
|
||||
// mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
|
||||
mask = 3;
|
||||
{
|
||||
var (dx, dy) = evt.wheelDeltas;
|
||||
if (dx > 0) dx = 1;
|
||||
else if (dx < 0) dx = -1;
|
||||
if (dy > 0) dy = 1;
|
||||
else if (dy < 0) dy = -1;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
dy = 0;
|
||||
} else {
|
||||
dx = 0;
|
||||
}
|
||||
acc_wheel_delta_x += dx;
|
||||
acc_wheel_delta_y += dy;
|
||||
wheel_delta_x = acc_wheel_delta_x.toInteger();
|
||||
wheel_delta_y = acc_wheel_delta_y.toInteger();
|
||||
acc_wheel_delta_x -= wheel_delta_x;
|
||||
acc_wheel_delta_y -= wheel_delta_y;
|
||||
var now = getTime();
|
||||
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
|
||||
if (dt > 0) {
|
||||
var vx = dx / dt;
|
||||
var vy = dy / dt;
|
||||
if (vx != 0 || vy != 0) {
|
||||
inertia_velocity_x = vx;
|
||||
inertia_velocity_y = vy;
|
||||
}
|
||||
}
|
||||
acc_wheel_delta_x0 += dx;
|
||||
acc_wheel_delta_y0 += dy;
|
||||
total_wheel_time += dt;
|
||||
if (dx == 0 && dy == 0) {
|
||||
wheeling = false;
|
||||
if (dt < 0.1 && total_wheel_time > 0) {
|
||||
var v2 = (acc_wheel_delta_y0 / total_wheel_time) * inertia_velocity_y;
|
||||
if (v2 > 0) {
|
||||
v2 = Math.sqrt(v2);
|
||||
inertia_velocity_y = inertia_velocity_y < 0 ? -v2 : v2;
|
||||
accWheel(inertia_velocity_y, false);
|
||||
}
|
||||
v2 = (acc_wheel_delta_x0 / total_wheel_time) * inertia_velocity_x;
|
||||
if (v2 > 0) {
|
||||
v2 = Math.sqrt(v2);
|
||||
inertia_velocity_x = inertia_velocity_x < 0 ? -v2 : v2;
|
||||
accWheel(inertia_velocity_x, true);
|
||||
}
|
||||
}
|
||||
resetWheel();
|
||||
} else {
|
||||
wheeling = true;
|
||||
}
|
||||
last_wheel_time = now;
|
||||
if (wheel_delta_x == 0 && wheel_delta_y == 0) return keyboard_enabled;
|
||||
}
|
||||
break;
|
||||
case Event.MOUSE_DCLICK: // seq: down, up, dclick, up
|
||||
mask = 1;
|
||||
break;
|
||||
case Event.MOUSE_ENTER:
|
||||
entered = true;
|
||||
stdout.println("enter");
|
||||
return keyboard_enabled;
|
||||
case Event.MOUSE_LEAVE:
|
||||
entered = false;
|
||||
stdout.println("leave");
|
||||
return keyboard_enabled;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
var x = evt.x;
|
||||
var y = evt.y;
|
||||
if (mask != 0) {
|
||||
// to gain control of the mouse, user must move mouse
|
||||
if (cur_x != x || cur_y != y) {
|
||||
return keyboard_enabled;
|
||||
}
|
||||
// save bandwidth
|
||||
x = 0;
|
||||
y = 0;
|
||||
} else {
|
||||
cur_local_x = cur_x = x;
|
||||
cur_local_y = cur_y = y;
|
||||
}
|
||||
if (mask != 3) {
|
||||
resetWheel();
|
||||
}
|
||||
if (!keyboard_enabled) return false;
|
||||
x = (x / display_scale).toInteger();
|
||||
y = (y / display_scale).toInteger();
|
||||
// insert down between two up, osx has this behavior for triple click
|
||||
if (last_mouse_mask == 2 && mask == 2) {
|
||||
handler.send_mouse((evt.buttons << 3) | 1, x + display_origin_x, y + display_origin_y, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
}
|
||||
last_mouse_mask = mask;
|
||||
// to-do: altKey, ctrlKey etc
|
||||
handler.send_mouse((evt.buttons << 3) | mask,
|
||||
mask == 3 ? wheel_delta_x : x + display_origin_x,
|
||||
mask == 3 ? wheel_delta_y : y + display_origin_y,
|
||||
evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
return true;
|
||||
};
|
||||
|
||||
var cur_hotx = 0;
|
||||
var cur_hoty = 0;
|
||||
var cur_img = null;
|
||||
var cur_x = 0;
|
||||
var cur_y = 0;
|
||||
var cur_local_x = 0;
|
||||
var cur_local_y = 0;
|
||||
var cursors = {};
|
||||
var image_binded;
|
||||
|
||||
handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
||||
cur_hotx = hotx;
|
||||
cur_hoty = hoty;
|
||||
cursor_img.style.set {
|
||||
width: width + "px",
|
||||
height: height + "px",
|
||||
};
|
||||
var img = Image.fromBytes(colors);
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cursors[id] = [img, hotx, hoty, width, height];
|
||||
this.bindImage("in-memory:cursor", img);
|
||||
if (cursor_img.style#display == 'none') {
|
||||
self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); });
|
||||
}
|
||||
cur_img = img;
|
||||
}
|
||||
}
|
||||
|
||||
handler.setCursorId = function(id) {
|
||||
var img = cursors[id];
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cur_hotx = img[1];
|
||||
cur_hoty = img[2];
|
||||
cursor_img.style.set {
|
||||
width: img[3] + "px",
|
||||
height: img[4] + "px",
|
||||
};
|
||||
img = img[0];
|
||||
this.bindImage("in-memory:cursor", img);
|
||||
if (cursor_img.style#display == 'none') {
|
||||
self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); });
|
||||
}
|
||||
cur_img = img;
|
||||
}
|
||||
}
|
||||
|
||||
var got_mouse_control = true;
|
||||
handler.setCursorPosition = function(x, y) {
|
||||
if (!image_binded) return;
|
||||
got_mouse_control = false;
|
||||
cur_x = x - display_origin_x;
|
||||
cur_y = y - display_origin_y;
|
||||
var x = cur_x - cur_hotx;
|
||||
var y = cur_y - cur_hoty;
|
||||
x *= display_scale;
|
||||
y *= display_scale;
|
||||
cursor_img.style.set {
|
||||
left: x + "px",
|
||||
top: y + "px",
|
||||
};
|
||||
if (cursor_img.style#display == 'none') {
|
||||
handler.style#cursor = 'none';
|
||||
cursor_img.style#display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
function self.ready() {
|
||||
var w = 960;
|
||||
var h = 640;
|
||||
if (is_file_transfer || is_port_forward) {
|
||||
var r = handler.get_size();
|
||||
if (r[0] > 0) {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
} else {
|
||||
centerize(w, h);
|
||||
}
|
||||
} else {
|
||||
centerize(w, h);
|
||||
}
|
||||
if (!is_port_forward) connecting();
|
||||
if (is_file_transfer) initializeFileTransfer();
|
||||
if (is_port_forward) initializePortForward();
|
||||
}
|
||||
|
||||
var workarea_offset = 0;
|
||||
var size_adapted;
|
||||
handler.adaptSize = function() {
|
||||
if (size_adapted) return;
|
||||
size_adapted = true;
|
||||
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
|
||||
var (fx, fy, fw, fh) = view.screenBox(#frame, #rectw);
|
||||
if (is_osx) workarea_offset = sy;
|
||||
var r = handler.get_size();
|
||||
if (r[2] > 0) {
|
||||
if (r[2] >= fw && r[3] >= fh && !is_linux) {
|
||||
view.windowState = View.WINDOW_FULL_SCREEN;
|
||||
stdout.println("Initialize to full screen");
|
||||
} else if (r[2] >= sw && r[3] >= sh) {
|
||||
view.windowState = View.WINDOW_MAXIMIZED;
|
||||
stdout.println("Initialize to full screen");
|
||||
} else {
|
||||
view.move(r[0], r[1], r[2], r[3]);
|
||||
}
|
||||
} else {
|
||||
var w = handler.box(#width, #border)
|
||||
var h = handler.box(#height, #border)
|
||||
if (w >= sw || h >= sh) {
|
||||
view.windowState = View.WINDOW_MAXIMIZED;
|
||||
return;
|
||||
}
|
||||
// extra for border
|
||||
var extra = 2;
|
||||
centerize(w + extra, handler.box(#height, #border) + h + extra);
|
||||
}
|
||||
}
|
||||
|
||||
function self.closing() {
|
||||
var (x, y, w, h) = view.box(#rectw, #border, #screen);
|
||||
if (is_file_transfer) save_file_transfer_close_state();
|
||||
if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h);
|
||||
}
|
||||
|
||||
handler.setPermission = function(name, enabled) {
|
||||
if (name == "keyboard") keyboard_enabled = enabled;
|
||||
if (name == "audio") audio_enabled = enabled;
|
||||
if (name == "clipboard") clipboard_enabled = enabled;
|
||||
header.update();
|
||||
}
|
||||
|
||||
handler.closeSuccess = function() {
|
||||
// handler.msgbox("success", "Successful", "Ready to go.");
|
||||
handler.msgbox("", "", "");
|
||||
}
|
||||
@@ -89,7 +89,7 @@ extern "C"
|
||||
|
||||
CreateEnvironmentBlock(&lpEnvironment, // Environment block
|
||||
hToken, // New token
|
||||
TRUE); // Inheritence
|
||||
TRUE); // Inheritance
|
||||
}
|
||||
if (lpEnvironment)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user