Compare commits

..

96 Commits
1.3.0 ... 1.3.1

Author SHA1 Message Date
Dmytro
a516f01feb Update uk.rs (#9416) 2024-09-20 21:38:16 +08:00
rustdesk
2e314bf032 disable init clipboard sync by default 2024-09-20 17:38:29 +08:00
jkh0kr
b93d4ce3fc Update ko.rs (#9411)
Add Korean language
2024-09-20 15:16:20 +08:00
fufesou
21bcfd173d fix: wayland, rdp input, mouse, scale (#9402)
* fix: wayland, rdp input, mouse, scale

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: rdp input, mouse, scale, check 0

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-20 11:15:19 +08:00
21pages
3d5262c36f opt gtk sudo ui, fix edit button show (#9399)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-20 11:06:56 +08:00
Mr-Update
cfd801c5d6 Update de.rs (#9401) 2024-09-20 10:54:50 +08:00
bovirus
216a72592d Update Italian language (#9403) 2024-09-20 10:54:37 +08:00
fufesou
ddd3401bd7 fix: keyboard, translate mode (#9406)
hotkey, linux -> win

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-20 10:52:19 +08:00
solokot
47139edd81 Update Russian translation (#9391)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-09-19 18:48:54 +08:00
fufesou
c6e3f60a6b refact: flutter, ChangeNotifier, reduce rebuild (#9392)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-19 18:48:01 +08:00
21pages
88a99211f3 replace pkexec with gtk sudo (#9383)
* Fix https://github.com/rustdesk/rustdesk/issues/9286, replace pkexec
  with gtk sudo. Tested on gnome (ubuntu 22.04, debian 13), xfce (manjaro, suse), kde (kubuntu 23), lxqt (lubuntu 22), Cinnamon (mint 21.3), Mate (mint 21.2)
* Fix incorrect config of the main window opened by the tray, replace
  xdg-open with run_me, replace with dbus + run_me
* Fix `check_if_stop_service`, it causes the problem fixed in
  https://github.com/rustdesk/rustdesk/pull/8414, now revert that fix and fix itself.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-19 18:47:37 +08:00
fufesou
d08c335fdf fix: file transfer, show error, msgbox (#9389)
* fix: file transfer, show error, msgbox

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: translation

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-18 19:29:35 +08:00
fufesou
e5ec6957fe fix: option OPTION_ONE_WAY_FILE_TRANSFER (#9387)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-18 18:22:12 +08:00
rustdesk
e20f5dd001 fix ci 2024-09-18 13:00:15 +08:00
rustdesk
e1a6ccc100 fix ci 2024-09-18 12:37:26 +08:00
rustdesk
cc288272d3 OPTION_ONE_WAY_CLIPBOARD_REDIRECTION,
OPTION_ENABLE_CLIPBOARD_INIT_SYNC,
        OPTION_ALLOW_LOGON_SCREEN_PASSWORD,
        OPTION_ONE_WAY_FILE_TRANSFER,
2024-09-18 12:18:26 +08:00
Lumiphare
49ce4edb8a Chinese versions of CONTRIBUTING.md and CODE_OF_CONDUCT-ZH.md (#9386)
* Update CONTRIBUTING.md links to point to the Chinese version

* translated with AI assistance and manual refinement

* Adapted from the official Chinese translation of the Contributor Covenant

---------

Co-authored-by: sea <api@sea.com>
2024-09-18 10:39:26 +08:00
お餅のCreeeper
29c3b29bda Fix ja.rs typo (#9378) 2024-09-17 13:26:49 +08:00
お餅のCreeeper
8a8f708c3e update ja.rs (#9376) 2024-09-17 12:32:32 +08:00
fufesou
c5038b1a78 Fix/virtual display do not plug out if not plugged in (#9372)
* fix: win VD, do not plug out if not plugged in

Signed-off-by: fufesou <linlong1266@gmail.com>

* Forcibly virtual display on clicking button "-"

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-16 15:57:40 +08:00
21pages
f4c038ea93 update appindicator and recommends install it (#9364)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-15 14:33:59 +08:00
21pages
d9ea717056 fix last commit (#9355)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-14 10:03:50 +08:00
21pages
40af9dc78b not run window focus service on wayland (#9354)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-14 09:59:14 +08:00
Alex Rijckaert
81fc22a156 Update nl.rs (#9344)
* Update nl.rs

* Update nl.rs

file updated after adjusting (as test) @FastAct to RijckAlex (same person, new account)
2024-09-13 21:04:04 +08:00
rustdesk
2e7bd26e4c fix leak fix 2024-09-13 15:42:51 +08:00
rustdesk
179b562472 another leak 2024-09-13 15:41:42 +08:00
21pages
ab246fdcbf password lowercase check like uppercase (#9343)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-13 09:29:00 +08:00
rustdesk
d65d3b7326 fix ci 2024-09-13 09:21:50 +08:00
rustdesk
9f9a22ec63 uppercase for all 2024-09-13 08:46:21 +08:00
m-hume
a8f1a66043 Trim whitespace from Import server config (#9341) 2024-09-13 08:06:40 +08:00
21pages
0b3e7bf33e update hwcodec, fix linux ci (#9335)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-12 17:34:54 +08:00
fufesou
c358399eca refact: reduce try_get_displays() on login (#9333)
* refact: reduce try_get_displays() on login

Signed-off-by: fufesou <linlong1266@gmail.com>

* Function rename

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-12 14:44:40 +08:00
rustdesk
cacca7295c fix memory leak on mac because of wrong use of objc, by wrapping autoreleasepool 2024-09-12 14:26:29 +08:00
fufesou
d2e98cc620 fix: reduce rebuild for online state (#9331)
* fix: reduce rebuild for online state

Signed-off-by: fufesou <linlong1266@gmail.com>

* Query online, update  on focus changed

Signed-off-by: fufesou <linlong1266@gmail.com>

* Use  to ensure  is right

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact, window events on peer view

Signed-off-by: fufesou <linlong1266@gmail.com>

* chore

Signed-off-by: fufesou <linlong1266@gmail.com>

* Remove unused code

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-12 11:26:01 +08:00
fufesou
2e81bcb447 fix: Query online, remove loop retry (#9326)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-11 17:17:32 +08:00
fufesou
cbca0eb340 fix: keyboard, move tab to new window (#9322)
Do not disable keyboard when moving tab to new window on dispose.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-11 10:01:03 +08:00
fufesou
9380f33d7c Refact/options (#9318)
* refact options

Signed-off-by: fufesou <linlong1266@gmail.com>

* Remove unused msg

Signed-off-by: fufesou <linlong1266@gmail.com>

* web, toggle virtual display

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-10 23:54:59 +08:00
Gheorghi
519539ed0a Update bg.rs with more translations (#9317) 2024-09-10 23:22:14 +08:00
rustdesk
1f2a75fbd8 revert back pub lock because it does not fix leak 2024-09-10 21:28:07 +08:00
fufesou
51055a7e5b fix: tokio, call future in context of runtime (#9310)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-10 17:39:22 +08:00
fufesou
13effe7f14 fix: web, switch display (#9307)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-10 11:29:20 +08:00
21pages
943f96ef8c downgrade url_launcher/uni_links for linux ci (#9306)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-10 10:57:01 +08:00
rustdesk
260a82ee5c upgrade pub for flutter memory leak 2024-09-09 17:25:35 +08:00
fufesou
a2792d1527 comments (#9297)
* comments

Signed-off-by: fufesou <linlong1266@gmail.com>

* comments

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-08 23:07:42 +08:00
fufesou
2922ebe22a Fix/clipboard retry if is occupied (#9293)
* fix: clipboard, retry if is occupied

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: clipboard service, hold runtime to cm ipc

Signed-off-by: fufesou <linlong1266@gmail.com>

* update arboard

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: log

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: get formats, return only not

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-08 21:13:05 +08:00
rustdesk
1e6944b380 apply --cm-no-ui to non-windows 2024-09-08 12:54:27 +08:00
21pages
993862c103 quit cm process if ipc connection to ipc server closed (#9292)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-08 12:37:41 +08:00
Andrzej Rudnik
c8cd564e69 Update pl.rs (#9285) 2024-09-07 17:58:07 +08:00
21pages
a4cd64f0d5 fix qsv memory leak by updating ffmpeg (#9266)
* fix qsv memory leak by updating ffmpeg

* Memory leaks occur when destroying FFmpeg QSV VRAM encoders. This issue is resolved with FFmpeg version 7.
* FFmpeg requires ffnvcodec version 12.1.14.0 or higher, and an NVIDIA driver version greater than 530. For more details, https://github.com/FFmpeg/nv-codec-headers/tree/n12.1.14.0.
* The code of NVIDIA VRAM encoder is not changed, still use Video Codec SDK version 11, which is unaffected by FFmpeg. Drivers newer than 470 can support this, but we may consider an update later, as the support check by sdk code may not be accurate for FFmpeg RAM encoders.
* The issue is related to FFmpeg, not libmfx. FFmpeg version 7 recommends using libvpl, but vcpkg currently lacks ports for libvpl. We can add these in the future.
* D3D11 Texture Rendering: The "Shared GPU Memory" in the task manager continue increasing when using D3D11 texture render, which can exceed the GPU memory limit (e.g., reaching up to 100GB). I don't know what it is and will try to find it out.
* Roughly tests on Windows, Linux, macOS, and Android for quick fix. Further testing will be performed, and I will share the results in this pr.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* update flutter_gpu_texture_render, fix shared gpu memory leak while
rendering

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-07 10:20:52 +08:00
21pages
f0ca4b9fee --no-server parameter to avoid server ipc occupied by ui (#9272)
Signed-off-by: 21pages <pages21@163.com>
2024-09-06 14:43:38 +08:00
Xp96
aa3402b44a Update ptbr.rs (#9271) 2024-09-06 01:15:07 +08:00
fufesou
26ebd0deb9 fix: clipboard, cmd ipc (#9270)
1. Send raw contents if `content_len` > 1024*3.
2. Send raw contents if it is not empty.
3. Try read clipboard again if no data from cm.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-05 23:39:07 +08:00
21pages
4150036589 remove first frame fallback if repeat (#9267)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-05 22:48:20 +08:00
fufesou
7a1157f1b0 refact: quality status event (#9268)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-05 22:37:14 +08:00
21pages
3bd34bf0b9 increase interval for restart linux ui, try fix loop start (#9264)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-05 18:34:48 +08:00
fufesou
5f29016861 fix: build web (#9259)
1. Web, build.
2. Web and mobile, `onSubmitted` for ID text field.
3. Web, remove unused key 'toggle_option'.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-04 22:27:52 +08:00
fufesou
e40243b55d Fix/wf cliprdr c bugs (#9253)
* fix: ResetEvent() after WaitForSingleObject()

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: check and free mem

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-04 17:04:48 +08:00
fufesou
dbbbd08934 fix: clipboard, support excel xml spreadsheet (#9252)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-04 16:44:36 +08:00
21pages
29e12b84a9 password max length prompt (#9248)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-04 11:31:13 +08:00
fufesou
04c0f66ca9 fix: set to OK if recv flag is TRUE (#9244)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-03 21:15:35 +08:00
fufesou
ec28567362 fix: win, file clipboard (#9243)
1. Return the result of `wait_response_event()` in
   `cliprdr_send_format_list()`
2. Add recv flags to avoid waiting a long time.

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-03 20:55:45 +08:00
fufesou
d4377a13c5 refact: peer card, orientation (#9235)
* refact: peer card, orientation

Signed-off-by: fufesou <linlong1266@gmail.com>

* Do not change landscape/portrait on Desktop

Signed-off-by: fufesou <linlong1266@gmail.com>

* comments

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-03 19:06:11 +08:00
21pages
39e713838f Use fallback codec if first frame fails (#9242)
* Both encoding and decoding use fallback if first frame fails
* More aggresive max fail counter
* Update hwcodec, add judgement when length of the encoded data is zero, https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525997
* Fix serde hwcodec config toml fails when the non-first vec![] is empty, https://github.com/toml-rs/toml-rs/issues/384, the config file is used for cache, when check process is not finished, the cache is used.
* Allow cm not start for pro user

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-09-03 18:48:17 +08:00
SimonHanel
75a4671bda Update da.rs (#9238)
* Update da.rs

Filled out every empty string and adjusted some for better translation.

Some translations might be janky due to my lack of context for what the string is used for, but it's good enough for now.

* Update da.rs

Minor tweaks
2024-09-03 10:09:25 +08:00
fufesou
827efabbc0 refact: remove fingerprint for web (#9226)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-02 02:00:59 +08:00
fufesou
532fe6aefb refact: web ui, login (#9225)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-01 23:14:57 +08:00
fufesou
ae339f039d refact: web ui (#9217)
* refact: web ui

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: remove AppBar shadow

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-09-01 00:30:07 +08:00
fufesou
bf390611ab fix: keyboard, sciter (#9216)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-31 19:02:50 +08:00
fufesou
e3f6829d02 refact: android ios, lan discovery (#9207)
* refact: android ios, lan discovery

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: build and runtime error

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-30 00:37:38 +08:00
fufesou
832002a10f refact: web, remote toolbar, pin (#9206)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-29 23:38:08 +08:00
RustDesk
d335cdbb0c Update README.md (#9196) 2024-08-28 12:25:00 +08:00
RustDesk
6a5d5875c8 Logo broken (#9195) 2024-08-28 12:23:40 +08:00
fufesou
cf06d1028f fix: web, reset cursor on disconn, back to main page (#9192)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 23:58:04 +08:00
9amm
fd178a7b6c Fix minor typo (#9191)
Co-authored-by: 9amm <>
2024-08-27 23:38:40 +08:00
rustdesk
f3a2733d75 start mac service more later 2024-08-27 20:25:01 +08:00
fufesou
55de573a01 fix: keyboard input, mulit windows (#9189)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 19:30:51 +08:00
fufesou
40239a1c41 feat: macos, mouse button, back&forward (#9185)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 15:20:29 +08:00
fufesou
c68ce7dd84 fix: web v2, keyboard mode (#9180)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-27 00:00:33 +08:00
rustdesk
690a2c8399 still find delegate failure when my mac restarted automatically sometimes 2024-08-26 17:07:02 +08:00
fufesou
4b4fd94f3e feat: web v2 keyboard (#9175)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-26 12:13:11 +08:00
21pages
5abe42f66c not run get window focus if no multiple displays (#9174)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-26 10:37:35 +08:00
21pages
48aec6484c refresh file transfer table on resume (#9167)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-25 21:29:41 +08:00
21pages
a946d4d0c9 file transfer status text overflow at start (#9166)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-25 20:46:21 +08:00
ELForcer
24f4b94082 Update ru.rs (#9163) 2024-08-25 15:12:08 +08:00
fufesou
aa1e122532 fix: revert key events to raw key events on Linux (#9161)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-25 00:03:31 +08:00
rustdesk
d400999b9c bump to 1.3.1 2024-08-24 19:02:04 +08:00
fufesou
1d416f6626 refact: flutter keyboard, map mode (#9160)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-24 12:10:36 +08:00
Kleofass
9d9741f18e Update lv.rs (#9155) 2024-08-23 21:27:52 +08:00
21pages
50aa8e12ad desktop file transfer, all columns respond to tap, add right click item border (#9153)
When right click selected item, the border is not obvious but can feel
some change.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-23 10:00:36 +08:00
jxdv
5931af460e Update trs (#9144)
* update sk tr

* update cz tr
2024-08-22 09:36:03 +08:00
fufesou
fc607d6789 fix: privacy mode 2, restore (#9141)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-22 09:35:50 +08:00
rustdesk
529e70910d build 47 2024-08-21 18:29:43 +08:00
fufesou
f300d797e2 Fix/ios query onlines (#9134)
* fix: ios query onlines

Signed-off-by: fufesou <linlong1266@gmail.com>

* comments

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2024-08-21 18:21:25 +08:00
Daniel Ehrhardt
e3cce2824d Added Public Server to Readme (#9132) 2024-08-21 09:28:02 +08:00
21pages
f34b8411a7 Fix new cm tab not replace the old persisted tab (#9127)
* This happens when after changing DesktopTab to StatefulWidget, 1.2.7
  and 1.3.0 have this problem.
* When `addConnection` in server_model.dart is called, the old closed
  client is removed, the client parameter of buildConnectionCard is new,
but client id inside Consumer is old.
* The only state in cm page is timer, its value is kept in test.
* There may be a better way to solve the ui update.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-20 15:34:10 +08:00
21pages
8745fcbb6a opt desktop file manager status list (#9117)
* Show delete file/dir log
* Show full path rather than base file name
* Show files count
* Opt status card layout
* Change selected color to accent

Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-08-20 10:53:55 +08:00
156 changed files with 4659 additions and 2185 deletions

View File

@@ -6,7 +6,7 @@ on:
workflow_call:
env:
FLUTTER_VERSION: "3.16.9"
FLUTTER_VERSION: "3.19.6"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503

View File

@@ -33,7 +33,7 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
VERSION: "1.3.0"
VERSION: "1.3.1"
NDK_VERSION: "r27"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -884,7 +884,7 @@ jobs:
git \
g++ \
g++-multilib \
libappindicator3-dev \
libayatana-appindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-10-dev \
@@ -976,8 +976,11 @@ jobs:
- name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
cd flutter/lib
cd flutter
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
flutter pub get
cd lib
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk lib
@@ -1144,7 +1147,7 @@ jobs:
git \
g++ \
g++-multilib \
libappindicator3-dev \
libayatana-appindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-10-dev \
@@ -1210,8 +1213,11 @@ jobs:
- name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
cd flutter/lib
cd flutter
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
flutter pub get
cd lib
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk
@@ -1418,7 +1424,7 @@ jobs:
gcc \
git \
g++ \
libappindicator3-dev \
libayatana-appindicator3-dev \
libasound2-dev \
libclang-10-dev \
libgstreamer1.0-dev \
@@ -1675,7 +1681,7 @@ jobs:
gcc \
git \
g++ \
libappindicator3-dev \
libayatana-appindicator3-dev \
libasound2-dev \
libclang-dev \
libdbus-1-dev \

View File

@@ -18,7 +18,7 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.3.0"
VERSION: "1.3.1"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -262,7 +262,7 @@ jobs:
git \
g++ \
g++-multilib \
libappindicator3-dev \
libayatana-appindicator3-dev\
libasound2-dev \
libc6-dev \
libclang-10-dev \

33
Cargo.lock generated
View File

@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arboard"
version = "3.4.0"
source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35"
source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
dependencies = [
"clipboard-win",
"core-graphics 0.23.2",
@@ -860,6 +860,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.38"
@@ -3045,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.7.0"
source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267"
source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -3967,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"cfg_aliases",
"cfg_aliases 0.1.1",
"libc",
"memoffset 0.9.1",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"cfg_aliases 0.2.1",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -5187,7 +5205,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488"
source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e"
dependencies = [
"cocoa 0.24.1",
"core-foundation 0.9.4",
@@ -5462,7 +5480,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.3.0"
version = "1.3.1"
dependencies = [
"android-wakelock",
"android_logger",
@@ -5494,6 +5512,7 @@ dependencies = [
"flutter_rust_bridge",
"fon",
"fruitbasket",
"gtk",
"hbb_common",
"hex",
"hound",
@@ -5508,6 +5527,7 @@ dependencies = [
"libpulse-simple-binding",
"mac_address",
"magnum-opus",
"nix 0.29.0",
"num_cpus",
"objc",
"objc_id",
@@ -5539,6 +5559,7 @@ dependencies = [
"system_shutdown",
"tao",
"tauri-winrt-notification",
"termios",
"totp-rs",
"tray-icon",
"url",
@@ -5559,7 +5580,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.3.0"
version = "1.3.1"
dependencies = [
"brotli",
"dirs 5.0.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.3.0"
version = "1.3.1"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -161,6 +161,9 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", optional = true}
nix = { version = "0.29", features = ["term", "process"]}
gtk = "0.18"
termios = "0.3"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13"

View File

@@ -1,6 +1,6 @@
<p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Servers</a> •
<a href="#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> •
@@ -171,3 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk
![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Public Servers](#public-servers)
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.3.0
version: 1.3.1
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.3.0
version: 1.3.1
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -287,7 +287,8 @@ Version: %s
Architecture: %s
Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire%s
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software.
""" % (version, get_deb_arch(), get_deb_extra_depends())
@@ -330,8 +331,6 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2(
@@ -375,8 +374,6 @@ def build_deb_from_folder(version, binary_folder):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")

View File

@@ -0,0 +1,87 @@
# 贡献者公约行为准则
## 我们的承诺
身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
## 我们的标准
有助于为我们的社区创造积极环境的行为例子包括但不限于:
* 表现出对他人的同情和善意
* 尊重不同的主张、观点和感受
* 提出和大方接受建设性意见
* 承担责任并向受我们错误影响的人道歉
* 注重社区共同诉求,而非个人得失
不当行为例子包括:
* 使用情色化的语言或图像,及性引诱或挑逗
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
* 公开或私下的骚扰行为
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
* 其他有理由认定为违反职业操守的不当行为
## 责任和权力
社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论comment、提交commits、代码、维基wiki编辑、议题issues或其他贡献并在适当时机知采取措施的理由。
## 适用范围
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
## 监督
辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。
所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
## 处理方针
社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
### 1. 纠正
**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
### 2. 警告
**社区影响**: 单个或一系列违规行为。
**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
### 3. 临时封禁
**社区影响**: 严重违反社区准则,包括持续的不当行为。
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
### 4. 永久封禁
**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
**处理意见**: 永久禁止在社区内进行任何形式的公开互动。
## 参见
本行为准则改编自[参与者公约][homepage]2.0 版, 参见
[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0].
指导方针借鉴自[Mozilla纪检分级][Mozilla CoC].
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

32
docs/CONTRIBUTING-ZH.md Normal file
View File

@@ -0,0 +1,32 @@
# 为RustDesk做贡献
Rust欢迎每一位贡献者如果您有意向为我们做出贡献请遵循以下指南
## 贡献方式
对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。
如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。
## PR 注意事项
- 从 master 分支创建一个新的分支并在提交PR之前如果需要将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。
- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。
- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名
- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。
- 请为修复的 bug 或新增的功能添加相应的测试用例。
有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## 行为准则
请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。
## 沟通渠道
RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。

View File

@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)

View File

@@ -50,6 +50,9 @@ final isLinux = isLinux_;
final isDesktop = isDesktop_;
final isWeb = isWeb_;
final isWebDesktop = isWebDesktop_;
final isWebOnWindows = isWebOnWindows_;
final isWebOnLinux = isWebOnLinux_;
final isWebOnMacOs = isWebOnMacOS_;
var isMobile = isAndroid || isIOS;
var version = '';
int androidVersion = 0;
@@ -347,6 +350,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 224, 224, 224),
scaffoldBackgroundColor: Colors.white,
dialogBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme(
elevation: 15,
shape: RoundedRectangleBorder(
@@ -442,6 +448,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 45, 46, 53),
scaffoldBackgroundColor: Color(0xFF18191E),
dialogBackgroundColor: Color(0xFF18191E),
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme(
elevation: 15,
shape: RoundedRectangleBorder(
@@ -547,7 +556,7 @@ class MyTheme {
static void changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode);
if (desktopType == DesktopType.main || isAndroid || isIOS) {
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
if (mode == ThemeMode.system) {
await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: defaultOptionTheme);
@@ -555,7 +564,7 @@ class MyTheme {
await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
await bind.mainChangeTheme(dark: mode.toShortString());
if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
// Synchronize the window theme of the system.
updateSystemWindowTheme();
}
@@ -3145,6 +3154,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
String? text) {
text = text?.trim();
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
@@ -61,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded(
child: (isDesktop || isWebDesktop)
? _buildAddressBookDesktop()
: _buildAddressBookMobile())
child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildAddressBookPortrait()
: _buildAddressBookLandscape()),
),
],
);
}
});
Widget _buildAddressBookDesktop() {
Widget _buildAddressBookLandscape() {
return Row(
children: [
Offstage(
@@ -106,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
);
}
Widget _buildAddressBookMobile() {
Widget _buildAddressBookPortrait() {
const padding = 8.0;
return Column(
children: [
@@ -239,14 +241,14 @@ class _AddressBookState extends State<AddressBook> {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
}
},
customButton: Container(
height: isDesktop ? 48 : 40,
customButton: Obx(()=>Container(
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
child: Row(children: [
Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)),
Icon(Icons.arrow_drop_down),
]),
),
)),
underline: Container(
height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1),
@@ -335,8 +337,8 @@ class _AddressBookState extends State<AddressBook> {
showActionMenu: editPermission);
}
final gridView = DynamicGridView.builder(
shrinkWrap: isMobile,
gridView(bool isPortrait) => DynamicGridView.builder(
shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length,
itemBuilder: (BuildContext context, int index) {
@@ -344,9 +346,9 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop)
? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView);
return Obx(() => stateGlobal.isPortrait.isFalse
? gridView(false)
: LimitedBox(maxHeight: maxHeight, child: gridView(true)));
});
}
@@ -506,9 +508,9 @@ class _AddressBookState extends State<AddressBook> {
double marginBottom = 4;
row({required Widget lable, required Widget input}) {
return Row(
makeChild(bool isPortrait) => Row(
children: [
!isMobile
!isPortrait
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10))
@@ -519,7 +521,8 @@ class _AddressBookState extends State<AddressBook> {
child: input),
),
],
).marginOnly(bottom: !isMobile ? 8 : 0);
).marginOnly(bottom: !isPortrait ? 8 : 0);
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
}
return CustomAlertDialog(
@@ -542,24 +545,24 @@ class _AddressBookState extends State<AddressBook> {
),
],
),
input: TextField(
input: Obx(() => TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'),
labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'),
errorText: errorMsg,
errorMaxLines: 5),
)),
))),
row(
lable: Text(
translate('Alias'),
style: style,
),
input: TextField(
input: Obx(() => TextField(
controller: aliasController,
decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'),
)),
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'),
),)),
),
if (isCurrentAbShared)
row(
@@ -567,11 +570,11 @@ class _AddressBookState extends State<AddressBook> {
translate('Password'),
style: style,
),
input: TextField(
input: Obx(() => TextField(
controller: passwordController,
obscureText: !passwordVisible,
decoration: InputDecoration(
labelText: !isMobile ? null : translate('Password'),
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'),
suffixIcon: IconButton(
icon: Icon(
passwordVisible
@@ -585,7 +588,7 @@ class _AddressBookState extends State<AddressBook> {
},
),
),
)),
),)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,

View File

@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
message: isMobile
message: !(isDesktop || isWebDesktop)
? ''
: widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'

View File

@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
String get name => translate('uppercase');
@override
bool validate(String value) {
return value.contains(RegExp(r'[A-Z]'));
return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toUpperCase() == character &&
character.toLowerCase() != character;
});
}
}
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
@override
bool validate(String value) {
return value.contains(RegExp(r'[a-z]'));
return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toLowerCase() == character &&
character.toUpperCase() != character;
});
}
}

View File

@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
@@ -380,6 +381,7 @@ class DialogTextField extends StatelessWidget {
final FocusNode? focusNode;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -397,6 +399,7 @@ class DialogTextField extends StatelessWidget {
this.hintText,
this.keyboardType,
this.inputFormatters,
this.maxLength,
required this.title,
required this.controller})
: super(key: key);
@@ -423,6 +426,7 @@ class DialogTextField extends StatelessWidget {
obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
maxLength: maxLength,
),
),
],
@@ -680,6 +684,7 @@ class PasswordWidget extends StatefulWidget {
this.hintText,
this.errorText,
this.title,
this.maxLength,
}) : super(key: key);
final TextEditingController controller;
@@ -688,6 +693,7 @@ class PasswordWidget extends StatefulWidget {
final String? hintText;
final String? errorText;
final String? title;
final int? maxLength;
@override
State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -750,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
obscureText: !_passwordVisible,
errorText: widget.errorText,
focusNode: _focusNode,
maxLength: widget.maxLength,
);
}
}
@@ -1123,7 +1130,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value,
),
],
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10),
],
),
@@ -2244,6 +2251,7 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
final confirmController = TextEditingController(text: oldPin);
String? pinErrorText;
String? confirmationErrorText;
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) {
submit() async {
pinErrorText = null;
@@ -2277,12 +2285,14 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
controller: pinController,
obscureText: true,
errorText: pinErrorText,
maxLength: maxLength,
),
DialogTextField(
title: translate('Confirmation'),
controller: confirmController,
obscureText: true,
errorText: confirmationErrorText,
maxLength: maxLength,
)
],
).marginOnly(bottom: 12),

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -45,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded(
child: (isDesktop || isWebDesktop)
? _buildDesktop()
: _buildMobile())
child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildPortrait()
: _buildLandscape())),
],
);
});
}
Widget _buildDesktop() {
Widget _buildLandscape() {
return Row(
children: [
Container(
@@ -89,7 +90,7 @@ class _MyGroupState extends State<MyGroup> {
);
}
Widget _buildMobile() {
Widget _buildPortrait() {
return Column(
children: [
Container(
@@ -159,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
}
return true;
}).toList();
final listView = ListView.builder(
shrinkWrap: isMobile,
listView(bool isPortrait) => ListView.builder(
shrinkWrap: isPortrait,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop)
? listView
: LimitedBox(maxHeight: maxHeight, child: listView);
return Obx(() => stateGlobal.isPortrait.isFalse
? listView(false)
: LimitedBox(maxHeight: maxHeight, child: listView(true)));
});
}

View File

@@ -595,7 +595,8 @@ class QualityMonitor extends StatelessWidget {
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
_row(
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
if (!isWeb)
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
],
),
)

View File

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -53,14 +54,11 @@ class _PeerCardState extends State<_PeerCard>
@override
Widget build(BuildContext context) {
super.build(context);
if (isDesktop || isWebDesktop) {
return _buildDesktop();
} else {
return _buildMobile();
}
return Obx(() =>
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
}
Widget _buildMobile() {
Widget _buildPortrait() {
final peer = super.widget.peer;
final PeerTabModel peerTabModel = Provider.of(context);
return Card(
@@ -87,7 +85,7 @@ class _PeerCardState extends State<_PeerCard>
));
}
Widget _buildDesktop() {
Widget _buildLandscape() {
final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>(
@@ -140,13 +138,13 @@ class _PeerCardState extends State<_PeerCard>
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Row(
makeChild(bool isPortrait) => Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile
borderRadius: isPortrait
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
@@ -154,11 +152,11 @@ class _PeerCardState extends State<_PeerCard>
),
),
alignment: Alignment.center,
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
width: isPortrait ? 50 : 42,
height: isPortrait ? 50 : null,
child: Stack(
children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6),
if (_shouldBuildPasswordIcon(peer))
Positioned(
@@ -183,19 +181,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column(
children: [
Row(children: [
getOnline(isMobile ? 4 : 8, peer.online),
getOnline(isPortrait ? 4 : 8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(top: isMobile ? 0 : 2),
]).marginOnly(top: isPortrait ? 0 : 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isMobile ? null : greyStyle,
style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
@@ -203,9 +201,9 @@ class _PeerCardState extends State<_PeerCard>
],
).marginOnly(top: 2),
),
isMobile
? checkBoxOrActionMoreMobile(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true),
isPortrait
? checkBoxOrActionMorePortrait(peer)
: checkBoxOrActionMoreLandscape(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
@@ -216,28 +214,27 @@ class _PeerCardState extends State<_PeerCard>
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList();
return Tooltip(
message: isMobile
message: !(isDesktop || isWebDesktop)
? ''
: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
deco == null
? child
: Obx(
() => Container(
Obx(() => deco == null
? makeChild(stateGlobal.isPortrait.isTrue)
: Container(
foregroundDecoration: deco.value,
child: child,
child: makeChild(stateGlobal.isPortrait.isTrue),
),
),
if (colors.isNotEmpty)
Positioned(
Obx(()=> Positioned(
top: 2,
right: isMobile ? 20 : 10,
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
)
))
]),
);
}
@@ -253,6 +250,9 @@ class _PeerCardState extends State<_PeerCard>
color: Colors.transparent,
elevation: 0,
margin: EdgeInsets.zero,
// to-do: memory leak here, more investigation needed.
// Continious rebuilds of `Obx()` will cause memory leak here.
// The simple demo does not have this issue.
child: Obx(
() => Container(
foregroundDecoration: deco.value,
@@ -316,7 +316,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall,
)),
]).paddingSymmetric(vertical: 8)),
checkBoxOrActionMoreDesktop(peer, isTile: false),
checkBoxOrActionMoreLandscape(peer, isTile: false),
],
).paddingSymmetric(horizontal: 12.0),
)
@@ -362,7 +362,7 @@ class _PeerCardState extends State<_PeerCard>
}
}
Widget checkBoxOrActionMoreMobile(Peer peer) {
Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) {
@@ -390,7 +390,7 @@ class _PeerCardState extends State<_PeerCard>
}
}
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) {
@@ -1203,6 +1203,7 @@ class MyGroupPeerCard extends BasePeerCard {
}
void _rdpDialog(String id) async {
final maxLength = bind.mainMaxEncryptLen();
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port);
@@ -1257,9 +1258,9 @@ void _rdpDialog(String id) async {
),
],
).marginOnly(bottom: isDesktop ? 8 : 0),
Row(
Obx(() => Row(
children: [
(isDesktop || isWebDesktop)
stateGlobal.isPortrait.isFalse
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
@@ -1270,17 +1271,17 @@ void _rdpDialog(String id) async {
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop)
labelText: isDesktop
? null
: translate('Username')),
controller: userController,
),
),
],
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
Row(
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
Obx(() => Row(
children: [
(isDesktop || isWebDesktop)
stateGlobal.isPortrait.isFalse
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
@@ -1291,8 +1292,9 @@ void _rdpDialog(String id) async {
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
maxLength: maxLength,
decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop)
labelText: isDesktop
? null
: translate('Password'),
suffixIcon: IconButton(
@@ -1304,7 +1306,7 @@ void _rdpDialog(String id) async {
)),
),
],
)
))
],
),
),

View File

@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -107,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget build(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
Widget selectionWrap(Widget widget) {
return model.multiSelectionMode ? createMultiSelectionBar() : widget;
return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
}
return Column(
textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 32,
child: Container(
padding: (isDesktop || isWebDesktop)
? null
: EdgeInsets.symmetric(horizontal: 2),
child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child:
visibleContextMenuListener(_createSwitchBar(context))),
if (isMobile)
..._mobileRightActions(context)
else
..._desktopRightActions(context)
],
)),
),
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
Obx(() => SizedBox(
height: 32,
child: Container(
padding: stateGlobal.isPortrait.isTrue
? EdgeInsets.symmetric(horizontal: 2)
: null,
child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
if (stateGlobal.isPortrait.isTrue)
..._portraitRightActions(context)
else
..._landscapeRightActions(context)
],
)),
),
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(),
],
);
@@ -299,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget visibleContextMenuListener(Widget child) {
if (isMobile) {
if (!(isDesktop || isWebDesktop)) {
return GestureDetector(
onLongPressDown: (e) {
final x = e.globalPosition.dx;
@@ -361,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
.toList());
}
Widget createMultiSelectionBar() {
final model = Provider.of<PeerTabModel>(context);
Widget createMultiSelectionBar(PeerTabModel model) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -380,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Row(
children: [
selectionCount(model.selectedPeers.length),
selectAll(),
selectAll(model),
closeSelection(),
],
)
@@ -456,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
showToast(translate('Successful'));
},
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
).marginOnly(left: isMobile ? 11 : 6),
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
);
}
@@ -477,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false);
},
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
).marginOnly(left: isMobile ? 11 : 6),
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
);
}
@@ -500,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
});
},
child: Icon(Icons.tag))
.marginOnly(left: isMobile ? 11 : 6),
.marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
);
}
@@ -511,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
);
}
Widget selectAll() {
final model = Provider.of<PeerTabModel>(context);
Widget selectAll(PeerTabModel model) {
return Offstage(
offstage:
model.selectedPeers.length >= model.currentTabCachedPeers.length,
@@ -556,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
});
}
List<Widget> _desktopRightActions(BuildContext context) {
List<Widget> _landscapeRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
const PeerSearchBar().marginOnly(right: 13),
_createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh(
@@ -580,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
];
}
List<Widget> _mobileRightActions(BuildContext context) {
List<Widget> _portraitRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
@@ -701,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length);
});
return Container(
width: isMobile ? 120 : 140,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
),
child: Obx(() => Row(
return Obx(() => Container(
width: stateGlobal.isPortrait.isTrue ? 120 : 140,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Expanded(
child: Row(
@@ -768,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
),
)
],
)),
);
),
));
}
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -88,6 +89,7 @@ class _PeersViewState extends State<_PeersView>
var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now();
var _lastWindowRestoreTime = DateTime.now();
var _queryCount = 0;
var _exit = false;
bool _isActive = true;
@@ -116,11 +118,37 @@ class _PeersViewState extends State<_PeersView>
@override
void onWindowFocus() {
_queryCount = 0;
_isActive = true;
}
@override
void onWindowBlur() {
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
// Maybe it's a bug of the window manager, but the source code seems to be correct.
//
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
// we need the following comparison to ensure that `_isActive` is true in the end.
if (isWindows && DateTime.now().difference(_lastWindowRestoreTime) <
const Duration(milliseconds: 300)) {
return;
}
_queryCount = _maxQueryCount;
_isActive = false;
}
@override
void onWindowRestore() {
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
if (!isWindows) return;
_queryCount = 0;
_isActive = true;
_lastWindowRestoreTime = DateTime.now();
}
@override
void onWindowMinimize() {
_queryCount = _maxQueryCount;
// Window minimize also triggers `onWindowBlur()`.
}
// This function is required for mobile.
@@ -128,7 +156,7 @@ class _PeersViewState extends State<_PeersView>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (isDesktop) return;
if (isDesktop || isWebDesktop) return;
if (state == AppLifecycleState.resumed) {
_isActive = true;
_queryCount = 0;
@@ -139,6 +167,9 @@ class _PeersViewState extends State<_PeersView>
@override
Widget build(BuildContext context) {
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
// Simple demo can reproduce this issue.
return ChangeNotifierProvider<Peers>(
create: (context) => widget.peers,
child: Consumer<Peers>(builder: (context, peers, child) {
@@ -194,7 +225,7 @@ class _PeersViewState extends State<_PeersView>
var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
buildOnePeer(Peer peer) {
buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged,
@@ -206,7 +237,7 @@ class _PeersViewState extends State<_PeersView>
// No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView().
return (isDesktop || isWebDesktop)
return !isPortrait
? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid
@@ -217,44 +248,45 @@ class _PeersViewState extends State<_PeersView>
: Container(child: visibilityChild);
}
final Widget child;
if (isMobile) {
child = ListView.builder(
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]).marginOnly(
top: index == 0 ? 0 : space / 2, bottom: space / 2);
},
);
} else {
child = Obx(() => peerCardUiType.value == PeerUiType.list
? DesktopScrollWrapper(
scrollController: _scrollController,
child: ListView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]).marginOnly(
right: space,
top: index == 0 ? 0 : space / 2,
bottom: space / 2);
}),
)
: DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2,
crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
));
}
// We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
// Continious rebuilds of `ListView.builder` will cause memory leak.
// Simple demo can reproduce this issue.
final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
? ListView.builder(
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index], true).marginOnly(
top: index == 0 ? 0 : space / 2, bottom: space / 2);
},
)
: peerCardUiType.value == PeerUiType.list
? DesktopScrollWrapper(
scrollController: _scrollController,
child: ListView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index], false)
.marginOnly(
right: space,
top: index == 0 ? 0 : space / 2,
bottom: space / 2);
}),
)
: DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2,
crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index], false);
}),
));
if (updateEvent == UpdateEvent.load) {
_curPeers.clear();

View File

@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
@override
Widget build(BuildContext context) {
// https://github.com/flutter/flutter/issues/154053
final useRawKeyEvents = isLinux && !isWeb;
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
// while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope(
autofocus: true,
child: Focus(
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true,
focusNode: focusNode,
onFocusChange: onFocusChange,
onKey: (FocusNode data, RawKeyEvent e) =>
inputModel.handleRawKeyEvent(e),
onKey: useRawKeyEvents
? (FocusNode data, RawKeyEvent event) =>
inputModel.handleRawKeyEvent(event)
: null,
onKeyEvent: useRawKeyEvents
? null
: (FocusNode node, KeyEvent event) =>
inputModel.handleKeyEvent(event),
child: child));
}
}
@@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
if (isDesktop) {
if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
// Workaround for the issue that the first pan event is sent a long time after the start event.
@@ -275,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (isDesktop) {
if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
inputModel.sendMouse('up', MouseButtons.left);

View File

@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
const String kPeerPlatformWebDesktop = "WebDesktop";
const double kScrollbarThickness = 12.0;

View File

@@ -857,6 +857,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
// SpecialCharacterValidationRule(),
MinCharactersValidationRule(8),
];
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) {
submit() {
@@ -915,6 +916,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg0 = '';
});
},
maxLength: maxLength,
),
),
],
@@ -941,6 +943,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg1 = '';
});
},
maxLength: maxLength,
),
),
],

View File

@@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (!bind.isOutgoingOnly() &&
if (!isWeb &&
!bind.isOutgoingOnly() &&
!bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety,
@@ -216,7 +217,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
width: _kTabWidth,
child: Column(
children: [
_header(),
_header(context),
Flexible(child: _listView(tabs: _settingTabs())),
],
),
@@ -239,21 +240,40 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
);
}
Widget _header() {
Widget _header(BuildContext context) {
final settingsText = Text(
translate('Settings'),
textAlign: TextAlign.left,
style: const TextStyle(
color: _accentColor,
fontSize: _kTitleFontSize,
fontWeight: FontWeight.w400,
),
);
return Row(
children: [
SizedBox(
height: 62,
child: Text(
translate('Settings'),
textAlign: TextAlign.left,
style: const TextStyle(
color: _accentColor,
fontSize: _kTitleFontSize,
fontWeight: FontWeight.w400,
if (isWeb)
IconButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
icon: Icon(Icons.arrow_back),
).marginOnly(left: 5),
if (isWeb)
SizedBox(
height: 62,
child: Align(
alignment: Alignment.center,
child: settingsText,
),
),
).marginOnly(left: 20, top: 10),
).marginOnly(left: 20),
if (!isWeb)
SizedBox(
height: 62,
child: settingsText,
).marginOnly(left: 20, top: 10),
const Spacer(),
],
);
@@ -322,7 +342,8 @@ class _General extends StatefulWidget {
}
class _GeneralState extends State<_General> {
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
final RxBool serviceStop =
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs;
@override
@@ -334,13 +355,13 @@ class _GeneralState extends State<_General> {
physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
service(),
if (!isWeb) service(),
theme(),
_Card(title: 'Language', children: [language()]),
hwcodec(),
audio(context),
record(context),
WaylandCard(),
if (!isWeb) hwcodec(),
if (!isWeb) audio(context),
if (!isWeb) record(context),
if (!isWeb) WaylandCard(),
other()
],
).marginOnly(bottom: _kListViewBottomMargin));
@@ -394,13 +415,13 @@ class _GeneralState extends State<_General> {
Widget other() {
final children = <Widget>[
if (!bind.isIncomingOnly())
if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
wallpaper(),
if (!bind.isIncomingOnly()) ...[
if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[
_OptionCheckBox(
context,
'Open connection in new tab',
@@ -417,18 +438,19 @@ class _GeneralState extends State<_General> {
kOptionAllowAlwaysSoftwareRender,
),
),
Tooltip(
message: translate('texture_render_tip'),
child: _OptionCheckBox(
context,
"Use texture rendering",
kOptionTextureRender,
optGetter: bind.mainGetUseTextureRender,
optSetter: (k, v) async =>
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
if (!isWeb)
Tooltip(
message: translate('texture_render_tip'),
child: _OptionCheckBox(
context,
"Use texture rendering",
kOptionTextureRender,
optGetter: bind.mainGetUseTextureRender,
optSetter: (k, v) async =>
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
),
),
),
if (!bind.isCustomClient())
if (!isWeb && !bind.isCustomClient())
_OptionCheckBox(
context,
'Check for software update on startup',
@@ -443,7 +465,7 @@ class _GeneralState extends State<_General> {
)
],
];
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
}
@@ -641,8 +663,9 @@ class _GeneralState extends State<_General> {
initialKey: currentKey,
onChanged: (key) async {
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
reloadAllWindows();
bind.mainChangeLanguage(lang: key);
if (isWeb) reloadCurrentWindow();
if (!isWeb) reloadAllWindows();
if (!isWeb) bind.mainChangeLanguage(lang: key);
},
enabled: !isOptFixed,
).marginOnly(left: _kContentHMargin);
@@ -1337,7 +1360,7 @@ class _Network extends StatefulWidget {
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled();
bool locked = !isWeb && bind.mainIsInstalled();
@override
Widget build(BuildContext context) {
@@ -1346,8 +1369,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
final scrollController = ScrollController();
final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
// TODO: support web proxy
final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
@@ -1467,7 +1491,7 @@ class _DisplayState extends State<_Display> {
scrollStyle(context),
imageQuality(context),
codec(context),
privacyModeImpl(context),
if (!isWeb) privacyModeImpl(context),
other(context),
]).marginOnly(bottom: _kListViewBottomMargin));
}
@@ -1878,9 +1902,10 @@ class _AboutState extends State<_About> {
SelectionArea(
child: Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0)),
SelectionArea(
child: Text('${translate('Fingerprint')}: $fingerprint')
.marginSymmetric(vertical: 4.0)),
if (!isWeb)
SelectionArea(
child: Text('${translate('Fingerprint')}: $fingerprint')
.marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com/privacy.html');
@@ -2487,6 +2512,7 @@ void changeSocks5Proxy() async {
: Icons.visibility))),
controller: pwdController,
enabled: !isOptFixed,
maxLength: bind.mainMaxEncryptLen(),
)),
),
],

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart';
@@ -68,7 +69,7 @@ class FileManagerPage extends StatefulWidget {
}
class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _dropMaskVisible = false.obs; // TODO impl drop mask
@@ -102,6 +103,7 @@ class _FileManagerPageState extends State<FileManagerPage>
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
});
WidgetsBinding.instance.addObserver(this);
}
@override
@@ -114,12 +116,21 @@ class _FileManagerPageState extends State<FileManagerPage>
}
Get.delete<FFI>(tag: 'ft_${widget.id}');
});
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
bool get wantKeepAlive => true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
jobController.jobTable.refresh();
}
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -173,10 +184,25 @@ class _FileManagerPageState extends State<FileManagerPage>
/// transfer status list
/// watch transfer status
Widget statusList() {
Widget getIcon(JobProgress job) {
final color = Theme.of(context).tabBarTheme.labelColor;
switch (job.type) {
case JobType.deleteDir:
case JobType.deleteFile:
return Icon(Icons.delete_outline, color: color);
default:
return Transform.rotate(
angle: job.isRemoteToLocal ? pi : 0,
child: Icon(Icons.arrow_forward_ios, color: color),
);
}
}
statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(),
itemBuilder: (BuildContext context, int index) {
final item = jobs[index];
final status = item.getStatus();
return Padding(
padding: const EdgeInsets.only(bottom: 5),
child: generateCard(
@@ -186,15 +212,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Transform.rotate(
angle: item.isRemoteToLocal ? pi : 0,
child: SvgPicture.asset("assets/arrow.svg",
colorFilter: svgColor(
Theme.of(context).tabBarTheme.labelColor)),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
getIcon(item)
.marginSymmetric(horizontal: 10, vertical: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -203,45 +222,28 @@ class _FileManagerPageState extends State<FileManagerPage>
Tooltip(
waitDuration: Duration(milliseconds: 500),
message: item.jobName,
child: Text(
item.fileName,
child: ExtendedText(
item.jobName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).paddingSymmetric(vertical: 10),
),
Text(
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
overflowWidget: TextOverflowWidget(
child: Text("..."),
position: TextOverflowPosition.start),
),
),
Offstage(
offstage: item.state != JobState.inProgress,
child: Text(
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
Tooltip(
waitDuration: Duration(milliseconds: 500),
message: status,
child: Text(status,
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
)).marginOnly(top: 6),
),
Offstage(
offstage: item.state == JobState.inProgress,
child: Text(
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage: item.state != JobState.inProgress,
offstage: item.type != JobType.transfer ||
item.state != JobState.inProgress,
child: LinearPercentIndicator(
padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true,
center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
@@ -251,7 +253,7 @@ class _FileManagerPageState extends State<FileManagerPage>
progressColor: MyTheme.accent,
backgroundColor: Theme.of(context).hoverColor,
lineHeight: kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15),
).paddingSymmetric(vertical: 8),
),
],
),
@@ -276,7 +278,6 @@ class _FileManagerPageState extends State<FileManagerPage>
),
MenuButton(
tooltip: translate("Delete"),
padding: EdgeInsets.only(right: 15),
child: SvgPicture.asset(
"assets/close.svg",
colorFilter: svgColor(Colors.white),
@@ -289,11 +290,11 @@ class _FileManagerPageState extends State<FileManagerPage>
hoverColor: MyTheme.accent80,
),
],
),
).marginAll(12),
],
),
],
).paddingSymmetric(vertical: 10),
),
),
);
},
@@ -943,6 +944,7 @@ class _FileManagerViewState extends State<FileManagerView> {
BuildContext context, ScrollController scrollController) {
final fd = controller.directory.value;
final entries = fd.entries;
Rx<Entry?> rightClickEntry = Rx(null);
return ListSearchActionListener(
node: _keyboardNode,
@@ -1002,16 +1004,69 @@ class _FileManagerViewState extends State<FileManagerView> {
? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
onTap() {
final items = selectedItems;
// handle double click
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(items, filteredEntries, entry, isLocal);
}
onSecondaryTap() {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
mod_menu.PopupMenuItem(
child: Text("Rename"),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
rightClickEntry.value = entry;
final future = mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
future.then((value) {
rightClickEntry.value = null;
});
future.onError((error, stackTrace) {
rightClickEntry.value = null;
});
}
}
onSecondaryTapDown(details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
}
return Padding(
padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container(
decoration: BoxDecoration(
color: selectedItems.items.contains(entry)
? Theme.of(context).hoverColor
? MyTheme.button
: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(
Radius.circular(5.0),
),
border: rightClickEntry.value == entry
? Border.all(
color: MyTheme.button,
width: 1.0,
)
: null,
),
key: ValueKey(entry.name),
height: kDesktopFileTransferRowHeight,
@@ -1050,51 +1105,19 @@ class _FileManagerViewState extends State<FileManagerView> {
),
Expanded(
child: Text(entry.name.nonBreaking,
style: TextStyle(
color: selectedItems.items
.contains(entry)
? Colors.white
: null),
overflow:
TextOverflow.ellipsis))
]),
)),
),
onTap: () {
final items = selectedItems;
// handle double click
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
onSecondaryTap: () {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version,
"1.3.0") >=
0)
mod_menu.PopupMenuItem(
child: Text("Rename"),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
}
},
onSecondaryTapDown: (details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
},
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
),
SizedBox(
width: 2.0,
@@ -1111,11 +1134,17 @@ class _FileManagerViewState extends State<FileManagerView> {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
color: selectedItems.items
.contains(entry)
? Colors.white70
: MyTheme.darkGray,
),
)),
),
),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
),
// Divider from header.
SizedBox(
@@ -1131,9 +1160,16 @@ class _FileManagerViewState extends State<FileManagerView> {
sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10, color: MyTheme.darkGray),
fontSize: 10,
color:
selectedItems.items.contains(entry)
? Colors.white70
: MyTheme.darkGray),
),
),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
),
),
],

View File

@@ -245,8 +245,10 @@ class _RemotePageState extends State<RemotePage>
super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onRemotePageDispose(closeSession);
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
if (closeSession) {
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();

View File

@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
localPlatform = kPeerPlatformWindows;
} else if (isLinux) {
localPlatform = kPeerPlatformLinux;
} else if (isWebOnWindows || isWebOnLinux) {
localPlatform = kPeerPlatformWebDesktop;
}
// to-do: web desktop support ?
return localPlatform;
}

View File

@@ -452,8 +452,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
Widget _buildToolbar(BuildContext context) {
final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) {
toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
}
@@ -1612,7 +1612,9 @@ class _KeyboardMenu extends StatelessWidget {
// If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported.
String? modeOnly;
if (isInputSourceFlutter) {
// Keep both map and legacy mode on web at the moment.
// TODO: Remove legacy mode after web supports translate mode on web.
if (isInputSourceFlutter && isDesktop) {
if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode;
@@ -1716,7 +1718,9 @@ class _KeyboardMenu extends StatelessWidget {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value);
final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value);
}
: null,
ffi: ffi,

View File

@@ -552,6 +552,13 @@ class _DesktopTabState extends State<DesktopTab>
controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(),
children: () {
if (DesktopTabType.cm == tabType) {
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
return state.value.tabs.map((tab) {
return tab.page;
}).toList();
}
/// to-do refactor, separate connection state and UI state for remote session.
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
final tabLen = state.value.tabs.length;

View File

@@ -372,7 +372,7 @@ class App extends StatefulWidget {
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
class _AppState extends State<App> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
@@ -396,6 +396,34 @@ class _AppState extends State<App> {
bind.mainChangeTheme(dark: to.toShortString());
}
};
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
_updateOrientation();
}
void _updateOrientation() {
if (isDesktop) return;
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
// https://github.com/flutter/flutter/issues/60899
// stateGlobal.isPortrait.value =
// MediaQuery.of(context).orientation == Orientation.portrait;
final orientation = View.of(context).physicalSize.aspectRatio > 1
? Orientation.landscape
: Orientation.portrait;
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
}
@override

View File

@@ -9,19 +9,16 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import 'home_page.dart';
import 'scan_page.dart';
import 'settings_page.dart';
/// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget implements PageShape {
ConnectionPage({Key? key}) : super(key: key);
ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
@override
final icon = const Icon(Icons.connected_tv);
@@ -30,7 +27,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection");
@override
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
final List<Widget> appBarActions;
@override
State<ConnectionPage> createState() => _ConnectionPageState();
@@ -252,6 +249,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
),
),
inputFormatters: [IDTextInputFormatter()],
onSubmitted: (_) {
onConnect();
},
);
},
onSelected: (option) {
@@ -356,73 +356,3 @@ class _ConnectionPageState extends State<ConnectionPage> {
super.dispose();
}
}
class WebMenu extends StatefulWidget {
const WebMenu({Key? key}) : super(key: key);
@override
State<WebMenu> createState() => _WebMenuState();
}
class _WebMenuState extends State<WebMenu> {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
tooltip: "",
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
return (isIOS
? [
const PopupMenuItem(
value: "scan",
child: Icon(Icons.qr_code_scanner, color: Colors.black),
)
]
: <PopupMenuItem<String>>[]) +
[
PopupMenuItem(
value: "server",
child: Text(translate('ID/Relay Server')),
)
] +
[
PopupMenuItem(
value: "login",
child: Text(gFFI.userModel.userName.value.isEmpty
? translate("Login")
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
)
] +
[
PopupMenuItem(
value: "about",
child: Text(translate('About RustDesk')),
)
];
},
onSelected: (value) {
if (value == 'server') {
showServerSettings(gFFI.dialogManager);
}
if (value == 'about') {
showAbout(gFFI.dialogManager);
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
} else {
logOutConfirmDialog();
}
}
if (value == 'scan') {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ScanPage(),
),
);
}
});
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/pages/server_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:flutter_hbb/web/settings_page.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../common/widgets/chat_page.dart';
@@ -45,7 +46,11 @@ class HomePageState extends State<HomePage> {
void initPages() {
_pages.clear();
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
if (!bind.isIncomingOnly()) {
_pages.add(ConnectionPage(
appBarActions: [],
));
}
if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
@@ -149,7 +154,8 @@ class HomePageState extends State<HomePage> {
}
class WebHomePage extends StatelessWidget {
final connectionPage = ConnectionPage();
final connectionPage =
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
@override
Widget build(BuildContext context) {

View File

@@ -181,6 +181,7 @@ class TextureModel {
}
updateCurrentDisplay(int curDisplay) {
if (isWeb) return;
final ffi = parent.target;
if (ffi == null) return;
tryCreateTexture(int idx) {

View File

@@ -34,6 +34,7 @@ class JobID {
}
typedef GetSessionID = SessionID Function();
typedef GetDialogManager = OverlayDialogManager? Function();
class FileModel {
final WeakReference<FFI> parent;
@@ -45,13 +46,15 @@ class FileModel {
late final FileController remoteController;
late final GetSessionID getSessionID;
late final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID();
late final FileDialogEventLoop evtLoop;
FileModel(this.parent) {
getSessionID = () => parent.target!.sessionId;
getDialogManager = () => parent.target?.dialogManager;
fileFetcher = FileFetcher(getSessionID);
jobController = JobController(getSessionID);
jobController = JobController(getSessionID, getDialogManager);
localController = FileController(
isLocal: true,
getSessionID: getSessionID,
@@ -451,7 +454,7 @@ class FileController {
final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden;
for (var from in items.items) {
final jobID = jobController.add(from, isRemoteToLocal);
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
bind.sessionSendFiles(
sessionId: sessionId,
actId: jobID,
@@ -494,13 +497,21 @@ class FileController {
fd.format(isWindows);
dialogManager?.dismissAll();
if (fd.entries.isEmpty) {
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
final confirm = await showRemoveDialog(
translate(
"Are you sure you want to delete this empty directory?"),
item.name,
false);
if (confirm == true) {
sendRemoveEmptyDir(item.path, 0);
sendRemoveEmptyDir(
item.path,
0,
deleteJobId,
);
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel", state: JobState.done);
}
return;
}
@@ -508,6 +519,13 @@ class FileController {
} else {
entries = [];
}
int deleteJobId;
if (item.isDirectory) {
deleteJobId =
jobController.addDeleteDirJob(item, !isLocal, entries.length);
} else {
deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
}
for (var i = 0; i < entries.length; i++) {
final dirShow = item.isDirectory
@@ -522,24 +540,32 @@ class FileController {
);
try {
if (confirm == true) {
sendRemoveFile(entries[i].path, i);
sendRemoveFile(entries[i].path, i, deleteJobId);
final res = await jobController.jobResultListener.start();
// handle remove res;
if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i);
sendRemoveEmptyDir(item.path, i, deleteJobId);
}
} else {
jobController.updateJobStatus(deleteJobId,
file_num: i, error: "cancel");
}
if (_removeCheckboxRemember) {
if (confirm == true) {
for (var j = i + 1; j < entries.length; j++) {
sendRemoveFile(entries[j].path, j);
sendRemoveFile(entries[j].path, j, deleteJobId);
final res = await jobController.jobResultListener.start();
if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i);
sendRemoveEmptyDir(item.path, i, deleteJobId);
}
}
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel",
file_num: entries.length,
state: JobState.done);
}
break;
}
@@ -618,22 +644,19 @@ class FileController {
}, useAnimation: false);
}
void sendRemoveFile(String path, int fileNum) {
void sendRemoveFile(String path, int fileNum, int actId) {
bind.sessionRemoveFile(
sessionId: sessionId,
actId: JobController.jobID.next(),
actId: actId,
path: path,
isRemote: !isLocal,
fileNum: fileNum);
}
void sendRemoveEmptyDir(String path, int fileNum) {
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
history.removeWhere((element) => element.contains(path));
bind.sessionRemoveAllEmptyDirs(
sessionId: sessionId,
actId: JobController.jobID.next(),
path: path,
isRemote: !isLocal);
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
}
Future<void> createDir(String path) async {
@@ -716,27 +739,29 @@ class FileController {
}
}
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
class JobController {
static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>();
final GetSessionID getSessionID;
final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID();
OverlayDialogManager? get alogManager => getDialogManager();
int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
JobController(this.getSessionID);
JobController(this.getSessionID, this.getDialogManager);
int getJob(int id) {
return jobTable.indexWhere((element) => element.id == id);
}
// JobProgress? getJob(int id) {
// return jobTable.firstWhere((element) => element.id == id);
// }
// return jobID
int add(Entry from, bool isRemoteToLocal) {
int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.transfer
..fileName = path.basename(from.path)
..jobName = from.path
..totalSize = from.size
@@ -746,6 +771,33 @@ class JobController {
return jobID;
}
int addDeleteFileJob(Entry file, bool isRemote) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.deleteFile
..fileName = path.basename(file.path)
..jobName = file.path
..totalSize = file.size
..state = JobState.none
..id = jobID
..isRemoteToLocal = isRemote);
return jobID;
}
int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.deleteDir
..fileName = path.basename(file.path)
..jobName = file.path
..fileCount = fileCount
..totalSize = file.size
..state = JobState.none
..id = jobID
..isRemoteToLocal = isRemote);
return jobID;
}
void tryUpdateJobProgress(Map<String, dynamic> evt) {
try {
int id = int.parse(evt['id']);
@@ -756,6 +808,7 @@ class JobController {
job.fileNum = int.parse(evt['file_num']);
job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']);
job.recvJobRes = true;
debugPrint("update job $id with $evt");
jobTable.refresh();
}
@@ -764,20 +817,48 @@ class JobController {
}
}
void jobDone(Map<String, dynamic> evt) async {
Future<bool> jobDone(Map<String, dynamic> evt) async {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
return;
// return;
}
int id = int.parse(evt['id']);
int id = -1;
int? fileNum = 0;
double? speed = 0;
try {
id = int.parse(evt['id']);
} catch (_) {}
final jobIndex = getJob(id);
if (jobIndex != -1) {
final job = jobTable[jobIndex];
job.finishedSize = job.totalSize;
if (jobIndex == -1) return true;
final job = jobTable[jobIndex];
job.recvJobRes = true;
if (job.type == JobType.deleteFile) {
job.state = JobState.done;
job.fileNum = int.parse(evt['file_num']);
jobTable.refresh();
} else if (job.type == JobType.deleteDir) {
try {
fileNum = int.tryParse(evt['file_num']);
} catch (_) {}
if (fileNum != null) {
if (fileNum < job.fileNum) return true; // file_num can be 0 at last
job.fileNum = fileNum;
if (fileNum >= job.fileCount - 1) {
job.state = JobState.done;
}
}
} else {
try {
fileNum = int.tryParse(evt['file_num']);
speed = double.tryParse(evt['speed']);
} catch (_) {}
if (fileNum != null) job.fileNum = fileNum;
if (speed != null) job.speed = speed;
job.state = JobState.done;
}
jobTable.refresh();
if (job.type == JobType.deleteDir) {
return job.state == JobState.done;
} else {
return true;
}
}
@@ -788,16 +869,61 @@ class JobController {
final job = jobTable[jobIndex];
job.state = JobState.error;
job.err = err;
job.fileNum = int.parse(evt['file_num']);
if (err == "skipped") {
job.state = JobState.done;
job.finishedSize = job.totalSize;
job.recvJobRes = true;
if (job.type == JobType.transfer) {
int? fileNum = int.tryParse(evt['file_num']);
if (fileNum != null) job.fileNum = fileNum;
if (err == "skipped") {
job.state = JobState.done;
job.finishedSize = job.totalSize;
}
} else if (job.type == JobType.deleteDir) {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
}
int? fileNum = int.tryParse(evt['file_num']);
if (fileNum != null) job.fileNum = fileNum;
} else if (job.type == JobType.deleteFile) {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
}
}
jobTable.refresh();
}
if (err == _kOneWayFileTransferError) {
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
final dm = alogManager;
if (dm != null) {
_lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm);
}
}
}
debugPrint("jobError $evt");
}
void updateJobStatus(int id,
{int? file_num, String? error, JobState? state}) {
final jobIndex = getJob(id);
if (jobIndex < 0) return;
final job = jobTable[jobIndex];
job.recvJobRes = true;
if (file_num != null) {
job.fileNum = file_num;
}
if (error != null) {
job.err = error;
job.state = JobState.error;
}
if (state != null) {
job.state = state;
}
if (job.type == JobType.deleteFile && error == null) {
job.state = JobState.done;
}
jobTable.refresh();
}
Future<void> cancelJob(int id) async {
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
}
@@ -814,6 +940,7 @@ class JobController {
final currJobId = JobController.jobID.next();
String fileName = path.basename(isRemote ? remote : to);
var jobProgress = JobProgress()
..type = JobType.transfer
..fileName = fileName
..jobName = isRemote ? remote : to
..id = currJobId
@@ -1088,8 +1215,12 @@ extension JobStateDisplay on JobState {
}
}
enum JobType { none, transfer, deleteFile, deleteDir }
class JobProgress {
JobType type = JobType.none;
JobState state = JobState.none;
var recvJobRes = false;
var id = 0;
var fileNum = 0;
var speed = 0.0;
@@ -1109,7 +1240,9 @@ class JobProgress {
int lastTransferredSize = 0;
clear() {
type = JobType.none;
state = JobState.none;
recvJobRes = false;
id = 0;
fileNum = 0;
speed = 0;
@@ -1123,11 +1256,81 @@ class JobProgress {
}
String display() {
if (state == JobState.done && err == "skipped") {
return translate("Skipped");
if (type == JobType.transfer) {
if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
} else if (type == JobType.deleteFile) {
if (err == "cancel") {
return translate("Cancel");
}
}
return state.display();
}
String getStatus() {
int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
if (handledFileCount >= fileCount) {
handledFileCount = fileCount;
}
if (state == JobState.done) {
handledFileCount = fileCount;
finishedSize = totalSize;
}
final filesStr = "$handledFileCount/$fileCount files";
final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
final sizePercentStr = totalSize > 0 && finishedSize > 0
? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
: "";
if (type == JobType.deleteFile) {
return display();
} else if (type == JobType.deleteDir) {
var res = '';
if (state == JobState.done || state == JobState.error) {
res = display();
}
if (filesStr.isNotEmpty) {
if (res.isNotEmpty) {
res += " ";
}
res += filesStr;
}
if (sizeStr.isNotEmpty) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizeStr;
}
return res;
} else if (type == JobType.transfer) {
var res = "";
if (state != JobState.inProgress && state != JobState.none) {
res += display();
}
if (filesStr.isNotEmpty) {
if (res.isNotEmpty) {
res += ", ";
}
res += filesStr;
}
if (sizeStr.isNotEmpty && state != JobState.inProgress) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizeStr;
}
if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizePercentStr;
}
return res;
}
return '';
}
}
class _PathStat {

View File

@@ -177,7 +177,7 @@ class PointerEventToRust {
}
}
class ToReleaseKeys {
class ToReleaseRawKeys {
RawKeyEvent? lastLShiftKeyEvent;
RawKeyEvent? lastRShiftKeyEvent;
RawKeyEvent? lastLCtrlKeyEvent;
@@ -282,6 +282,48 @@ class ToReleaseKeys {
}
}
class ToReleaseKeys {
KeyEvent? lastLShiftKeyEvent;
KeyEvent? lastRShiftKeyEvent;
KeyEvent? lastLCtrlKeyEvent;
KeyEvent? lastRCtrlKeyEvent;
KeyEvent? lastLAltKeyEvent;
KeyEvent? lastRAltKeyEvent;
KeyEvent? lastLCommandKeyEvent;
KeyEvent? lastRCommandKeyEvent;
KeyEvent? lastSuperKeyEvent;
reset() {
lastLShiftKeyEvent = null;
lastRShiftKeyEvent = null;
lastLCtrlKeyEvent = null;
lastRCtrlKeyEvent = null;
lastLAltKeyEvent = null;
lastRAltKeyEvent = null;
lastLCommandKeyEvent = null;
lastRCommandKeyEvent = null;
lastSuperKeyEvent = null;
}
release(KeyEventResult Function(KeyEvent e) handleKeyEvent) {
for (final key in [
lastLShiftKeyEvent,
lastRShiftKeyEvent,
lastLCtrlKeyEvent,
lastRCtrlKeyEvent,
lastLAltKeyEvent,
lastRAltKeyEvent,
lastLCommandKeyEvent,
lastRCommandKeyEvent,
lastSuperKeyEvent,
]) {
if (key != null) {
handleKeyEvent(key);
}
}
}
}
class InputModel {
final WeakReference<FFI> parent;
String keyboardMode = '';
@@ -292,6 +334,7 @@ class InputModel {
var alt = false;
var command = false;
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
// trackpad
@@ -339,6 +382,91 @@ class InputModel {
}
}
void handleKeyDownEventModifiers(KeyEvent e) {
KeyUpEvent upEvent(e) => KeyUpEvent(
physicalKey: e.physicalKey,
logicalKey: e.logicalKey,
timeStamp: e.timeStamp,
);
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
if (!alt) {
alt = true;
}
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
if (!alt) {
alt = true;
}
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
if (!ctrl) {
ctrl = true;
}
toReleaseKeys.lastLCtrlKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
if (!ctrl) {
ctrl = true;
}
toReleaseKeys.lastRCtrlKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
if (!shift) {
shift = true;
}
toReleaseKeys.lastLShiftKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
if (!shift) {
shift = true;
}
toReleaseKeys.lastRShiftKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
if (!command) {
command = true;
}
toReleaseKeys.lastLCommandKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
if (!command) {
command = true;
}
toReleaseKeys.lastRCommandKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
if (!command) {
command = true;
}
toReleaseKeys.lastSuperKeyEvent = upEvent(e);
}
}
void handleKeyUpEventModifiers(KeyEvent e) {
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
alt = false;
toReleaseKeys.lastLAltKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
alt = false;
toReleaseKeys.lastRAltKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
ctrl = false;
toReleaseKeys.lastLCtrlKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
ctrl = false;
toReleaseKeys.lastRCtrlKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
shift = false;
toReleaseKeys.lastLShiftKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
shift = false;
toReleaseKeys.lastRShiftKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
command = false;
toReleaseKeys.lastLCommandKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
command = false;
toReleaseKeys.lastRCommandKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
command = false;
toReleaseKeys.lastSuperKeyEvent = null;
}
}
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
@@ -358,7 +486,7 @@ class InputModel {
command = true;
}
}
toReleaseKeys.updateKeyDown(key, e);
toReleaseRawKeys.updateKeyDown(key, e);
}
if (e is RawKeyUpEvent) {
if (key == LogicalKeyboardKey.altLeft ||
@@ -376,12 +504,46 @@ class InputModel {
command = false;
}
toReleaseKeys.updateKeyUp(key, e);
toReleaseRawKeys.updateKeyUp(key, e);
}
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') {
mapKeyboardMode(e);
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardModeRaw(e);
} else {
legacyKeyboardModeRaw(e);
}
return KeyEventResult.handled;
}
KeyEventResult handleKeyEvent(KeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
return KeyEventResult.handled;
}
if (isWindows || isLinux) {
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
e.physicalKey == PhysicalKeyboardKey.metaRight) {
return KeyEventResult.handled;
}
}
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
handleKeyDownEventModifiers(e);
}
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
// FIXME: e.character is wrong for dead keys, eg: ^ in de
newKeyboardMode(
e.character ?? '',
e.physicalKey.usbHidUsage & 0xFFFF,
// Show repeat event be converted to "release+press" events?
e is KeyDownEvent || e is KeyRepeatEvent);
} else {
legacyKeyboardMode(e);
}
@@ -389,7 +551,33 @@ class InputModel {
return KeyEventResult.handled;
}
void mapKeyboardMode(RawKeyEvent e) {
/// Send Key Event
void newKeyboardMode(String character, int usbHid, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
bind.sessionHandleFlutterKeyEvent(
sessionId: sessionId,
character: character,
usbHid: usbHid,
lockModes: lockModes,
downOrUp: down);
}
void mapKeyboardModeRaw(RawKeyEvent e) {
int positionCode = -1;
int platformCode = -1;
bool down;
@@ -441,7 +629,7 @@ class InputModel {
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
bind.sessionHandleFlutterKeyEvent(
bind.sessionHandleFlutterRawKeyEvent(
sessionId: sessionId,
name: name,
platformCode: platformCode,
@@ -450,7 +638,7 @@ class InputModel {
downOrUp: down);
}
void legacyKeyboardMode(RawKeyEvent e) {
void legacyKeyboardModeRaw(RawKeyEvent e) {
if (e is RawKeyDownEvent) {
if (e.repeat) {
sendRawKey(e, press: true);
@@ -471,6 +659,24 @@ class InputModel {
inputKey(label, down: down, press: press ?? false);
}
void legacyKeyboardMode(KeyEvent e) {
if (e is KeyDownEvent) {
sendKey(e, down: true);
} else if (e is KeyRepeatEvent) {
sendKey(e, press: true);
} else if (e is KeyUpEvent) {
sendKey(e);
}
}
void sendKey(KeyEvent e, {bool? down, bool? press}) {
// for maximum compatibility
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
logicalKeyMap[e.logicalKey.keyId] ??
e.logicalKey.keyLabel;
inputKey(label, down: down, press: press ?? false);
}
/// Send key stroke event.
/// [down] indicates the key's state(down or up).
/// [press] indicates a click event(down and up).
@@ -566,7 +772,8 @@ class InputModel {
}
void enterOrLeave(bool enter) {
toReleaseKeys.release(handleRawKeyEvent);
toReleaseKeys.release(handleKeyEvent);
toReleaseRawKeys.release(handleRawKeyEvent);
_pointerMovedAfterEnter = false;
// Fix status
@@ -577,6 +784,9 @@ class InputModel {
if (!isInputSourceFlutter) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
}
if (!isWeb && enter) {
bind.setCurSessionId(sessionId: sessionId);
}
}
/// Send mouse movement event with distance in [x] and [y].
@@ -1164,15 +1374,15 @@ class InputModel {
// Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async {
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
await Future.delayed(Duration(milliseconds: 100));
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
}
Future<void> onMobileVolumeUp() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
Future<void> onMobileVolumeDown() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
Future<void> onMobilePower() async =>
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
}

View File

@@ -304,8 +304,13 @@ class FfiModel with ChangeNotifier {
} else if (name == 'job_progress') {
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
} else if (name == 'job_done') {
parent.target?.fileModel.jobController.jobDone(evt);
parent.target?.fileModel.refreshAll();
bool? refresh =
await parent.target?.fileModel.jobController.jobDone(evt);
if (refresh == true) {
// many job done for delete directory
// todo: refresh may not work when confirm delete local directory
parent.target?.fileModel.refreshAll();
}
} else if (name == 'job_error') {
parent.target?.fileModel.jobController.jobError(evt);
} else if (name == 'override_file_confirm') {
@@ -492,10 +497,12 @@ class FfiModel with ChangeNotifier {
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
newDisplay.originalWidth =
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
newDisplay.originalHeight =
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
newDisplay.originalWidth = int.tryParse(
evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
kInvalidResolutionValue;
newDisplay.originalHeight = int.tryParse(
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
kInvalidResolutionValue;
newDisplay._scale = _pi.scaleOfDisplay(display);
_pi.displays[display] = newDisplay;
@@ -788,7 +795,7 @@ class FfiModel with ChangeNotifier {
isRefreshing = false;
}
Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1;
_pi.features.privacyMode = features['privacy_mode'] == true;
if (!isCache) {
handleResolutions(peerId, evt["resolutions"]);
}
@@ -2178,6 +2185,7 @@ class CursorModel with ChangeNotifier {
debugPrint("deleting cursor with key $k");
deleteCustomCursor(k);
}
resetSystemCursor();
}
trySetRemoteWindowCoords() {
@@ -2224,8 +2232,10 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) {
try {
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
if ((evt['fps'] as String).isNotEmpty) {
if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
_data.speed = evt['speed'];
}
if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) {
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
final pi = parent.target?.ffiModel.pi;
if (pi != null) {
@@ -2246,14 +2256,18 @@ class QualityMonitorModel with ChangeNotifier {
_data.fps = null;
}
}
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
if ((evt['target_bitrate'] as String).isNotEmpty) {
if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
_data.delay = evt['delay'];
}
if (evt.containsKey('target_bitrate') &&
(evt['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt['target_bitrate'];
}
if ((evt['codec_format'] as String).isNotEmpty) {
if (evt.containsKey('codec_format') &&
(evt['codec_format'] as String).isNotEmpty) {
_data.codecFormat = evt['codec_format'];
}
if ((evt['chroma'] as String).isNotEmpty) {
if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
_data.chroma = evt['chroma'];
}
notifyListeners();
@@ -2497,6 +2511,7 @@ class FFI {
onEvent2UIRgba();
imageModel.onRgba(display, data);
});
this.id = id;
return;
}

View File

@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
}
void _updateOnlineState(Map<String, dynamic> evt) {
int changedCount = 0;
evt['onlines'].split(',').forEach((online) {
for (var i = 0; i < peers.length; i++) {
if (peers[i].id == online) {
peers[i].online = true;
if (!peers[i].online) {
changedCount += 1;
peers[i].online = true;
}
}
}
});
@@ -205,13 +209,18 @@ class Peers extends ChangeNotifier {
evt['offlines'].split(',').forEach((offline) {
for (var i = 0; i < peers.length; i++) {
if (peers[i].id == offline) {
peers[i].online = false;
if (peers[i].online) {
changedCount += 1;
peers[i].online = false;
}
}
}
});
event = UpdateEvent.online;
notifyListeners();
if (changedCount > 0) {
event = UpdateEvent.online;
notifyListeners();
}
}
void _updatePeers(Map<String, dynamic> evt) {

View File

@@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier {
notifyListeners();
}
// `notifyListeners()` will cause many rebuilds.
// So, we need to reduce the calls to "notifyListeners()" only when necessary.
// A better way is to use a new model.
setCurrentTabCachedPeers(List<Peer> peers) {
Future.delayed(Duration.zero, () {
final isPreEmpty = _currentTabCachedPeers.isEmpty;
_currentTabCachedPeers = peers;
notifyListeners();
final isNowEmpty = _currentTabCachedPeers.isEmpty;
if (isPreEmpty != isNowEmpty) {
notifyListeners();
}
});
}

View File

@@ -826,7 +826,7 @@ class Client {
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['is_start'] = authorized;
data['authorized'] = authorized;
data['is_file_transfer'] = isFileTransfer;
data['port_forward'] = portForward;
data['name'] = name;
@@ -840,6 +840,8 @@ class Client {
data['block_input'] = blockInput;
data['disconnected'] = disconnected;
data['from_switch'] = fromSwitch;
data['in_voice_call'] = inVoiceCall;
data['incoming_voice_call'] = incomingVoiceCall;
return data;
}

View File

@@ -20,6 +20,8 @@ class StateGlobal {
final svcStatus = SvcStatus.notReady.obs;
final RxBool isFocused = false.obs;
final isPortrait = false.obs;
String _inputSource = '';
// Use for desktop -> remote toolbar -> resolution

View File

@@ -11,3 +11,7 @@ final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => '';
final isWebOnWindows_ = false;
final isWebOnLinux_ = false;
final isWebOnMacOS_ = false;

View File

@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart';
deleteCustomCursor(String key) =>
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
resetSystemCursor() {}
MouseCursor buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) {

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import 'dart:html' as html;
import 'package:flutter_hbb/consts.dart';
@@ -23,6 +24,7 @@ sealed class EventToUI {
) = EventToUI_Rgba;
const factory EventToUI.texture(
int field0,
bool field1,
) = EventToUI_Texture;
}
@@ -33,15 +35,19 @@ class EventToUI_Event implements EventToUI {
}
class EventToUI_Rgba implements EventToUI {
const EventToUI_Rgba(final int field0) : this.field = field0;
const EventToUI_Rgba(final int field0) : field = field0;
final int field;
int get field0 => field;
}
class EventToUI_Texture implements EventToUI {
const EventToUI_Texture(final int field0) : this.field = field0;
final int field;
int get field0 => field;
const EventToUI_Texture(final int field0, final bool field1)
: f0 = field0,
f1 = field1;
final int f0;
final bool f1;
int get field0 => f0;
bool get field1 => f1;
}
class RustdeskImpl {
@@ -181,7 +187,7 @@ class RustdeskImpl {
Future<void> sessionToggleOption(
{required UuidValue sessionId, required String value, dynamic hint}) {
return Future(
() => js.context.callMethod('setByName', ['toggle_option', value]));
() => js.context.callMethod('setByName', ['option:toggle', value]));
}
Future<void> sessionTogglePrivacyMode(
@@ -190,8 +196,8 @@ class RustdeskImpl {
required bool on,
dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [
'toggle_option',
jsonEncode({implKey, on})
'toggle_privacy_mode',
jsonEncode({'impl_key': implKey, 'on': on})
]));
}
@@ -229,7 +235,7 @@ class RustdeskImpl {
}
String getLocalKbLayoutType({dynamic hint}) {
throw js.context.callMethod('getByName', ['option:local', 'kb_layout']);
return js.context.callMethod('getByName', ['option:local', 'kb_layout']);
}
Future<void> setLocalKbLayoutType(
@@ -346,7 +352,7 @@ class RustdeskImpl {
bool sessionIsKeyboardModeSupported(
{required UuidValue sessionId, required String mode, dynamic hint}) {
return mode == kKeyLegacyMode;
return [kKeyLegacyMode, kKeyMapMode].contains(mode);
}
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
@@ -385,14 +391,32 @@ class RustdeskImpl {
return Future(() => js.context.callMethod('setByName', [
'switch_display',
jsonEncode({
isDesktop: isDesktop,
sessionId: sessionId.toString(),
value: value
'isDesktop': isDesktop,
'sessionId': sessionId.toString(),
'value': value
})
]));
}
Future<void> sessionHandleFlutterKeyEvent(
{required UuidValue sessionId,
required String character,
required int usbHid,
required int lockModes,
required bool downOrUp,
dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [
'flutter_key_event',
jsonEncode({
'name': character,
'usb_hid': usbHid,
'lock_modes': lockModes,
if (downOrUp) 'down': 'true',
})
]));
}
Future<void> sessionHandleFlutterRawKeyEvent(
{required UuidValue sessionId,
required String name,
required int platformCode,
@@ -400,7 +424,6 @@ class RustdeskImpl {
required int lockModes,
required bool downOrUp,
dynamic hint}) {
// TODO: map mode
throw UnimplementedError();
}
@@ -633,7 +656,15 @@ class RustdeskImpl {
}
String mainGetLoginDeviceInfo({dynamic hint}) {
throw UnimplementedError();
String userAgent = html.window.navigator.userAgent;
String appName = html.window.navigator.appName;
String appVersion = html.window.navigator.appVersion;
String? platform = html.window.navigator.platform;
return jsonEncode({
'os': '$userAgent, $appName $appVersion ($platform)',
'type': 'Web client',
'name': js.context.callMethod('getByName', ['my_name']),
});
}
Future<void> mainChangeId({required String newId, dynamic hint}) {
@@ -702,11 +733,11 @@ class RustdeskImpl {
}
Future<String> mainGetAppName({dynamic hint}) {
throw UnimplementedError();
return Future.value(mainGetAppNameSync(hint: hint));
}
String mainGetAppNameSync({dynamic hint}) {
throw UnimplementedError();
return 'RustDesk';
}
String mainUriPrefixSync({dynamic hint}) {
@@ -714,7 +745,8 @@ class RustdeskImpl {
}
Future<String> mainGetLicense({dynamic hint}) {
throw UnimplementedError();
// TODO: implement
return Future(() => '');
}
Future<String> mainGetVersion({dynamic hint}) {
@@ -758,8 +790,9 @@ class RustdeskImpl {
}
Future<bool> mainIsUsingPublicServer({dynamic hint}) {
return Future(
() => js.context.callMethod('setByName', ["is_using_public_server"]));
return Future(() =>
js.context.callMethod('getByName', ["is_using_public_server"]) ==
'true');
}
Future<void> mainDiscover({dynamic hint}) {
@@ -826,11 +859,11 @@ class RustdeskImpl {
}
Future<String> mainGetMyId({dynamic hint}) {
throw UnimplementedError();
return Future(() => js.context.callMethod('getByName', ['my_id']));
}
Future<String> mainGetUuid({dynamic hint}) {
throw UnimplementedError();
return Future(() => js.context.callMethod('getByName', ['uuid']));
}
Future<String> mainGetPeerOption(
@@ -952,10 +985,11 @@ class RustdeskImpl {
Future<void> mainSetUserDefaultOption(
{required String key, required String value, dynamic hint}) {
return js.context.callMethod('getByName', [
js.context.callMethod('setByName', [
'option:user:default',
jsonEncode({'name': key, 'value': value})
]);
return Future.value();
}
String mainGetUserDefaultOption({required String key, dynamic hint}) {
@@ -1029,7 +1063,7 @@ class RustdeskImpl {
}
Future<String> mainGetLangs({dynamic hint}) {
throw UnimplementedError();
return Future(() => js.context.callMethod('getByName', ['langs']));
}
Future<String> mainGetTemporaryPassword({dynamic hint}) {
@@ -1041,7 +1075,7 @@ class RustdeskImpl {
}
Future<String> mainGetFingerprint({dynamic hint}) {
throw UnimplementedError();
return Future.value('');
}
Future<String> cmGetClientsState({dynamic hint}) {
@@ -1083,7 +1117,7 @@ class RustdeskImpl {
}
String mainSupportedHwdecodings({dynamic hint}) {
throw UnimplementedError();
return '{}';
}
Future<bool> mainIsRoot({dynamic hint}) {
@@ -1170,8 +1204,10 @@ class RustdeskImpl {
required int index,
required bool on,
dynamic hint}) {
// TODO
throw UnimplementedError();
return Future(() => js.context.callMethod('setByName', [
'toggle_virtual_display',
jsonEncode({'index': index, 'on': on})
]));
}
Future<void> mainSetHomeDir({required String home, dynamic hint}) {
@@ -1272,8 +1308,7 @@ class RustdeskImpl {
}
Future<String> mainGetBuildDate({dynamic hint}) {
// TODO
throw UnimplementedError();
return Future(() => js.context.callMethod('getByName', ['build_date']));
}
String translate(
@@ -1610,7 +1645,7 @@ class RustdeskImpl {
}
bool mainIsOptionFixed({required String key, dynamic hint}) {
throw UnimplementedError();
return false;
}
bool mainGetUseTextureRender({dynamic hint}) {
@@ -1650,5 +1685,40 @@ class RustdeskImpl {
throw UnimplementedError();
}
Future<String> getVoiceCallInputDevice({required bool isCm, dynamic hint}) {
throw UnimplementedError();
}
Future<void> setVoiceCallInputDevice(
{required bool isCm, required String device, dynamic hint}) {
throw UnimplementedError();
}
bool isPresetPasswordMobileOnly({dynamic hint}) {
throw UnimplementedError();
}
String mainGetBuildinOption({required String key, dynamic hint}) {
return '';
}
String installInstallOptions({dynamic hint}) {
throw UnimplementedError();
}
int mainMaxEncryptLen({dynamic hint}) {
throw UnimplementedError();
}
sessionRenameFile(
{required UuidValue sessionId,
required int actId,
required String path,
required String newName,
required bool isRemote,
dynamic hint}) {
throw UnimplementedError();
}
void dispose() {}
}

View File

@@ -1,4 +1,5 @@
import 'dart:js' as js;
import 'dart:html' as html;
final isAndroid_ = false;
final isIOS_ = false;
@@ -11,3 +12,9 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
final _userAgent = html.window.navigator.userAgent.toLowerCase();
final isWebOnWindows_ = _userAgent.contains('win');
final isWebOnLinux_ = _userAgent.contains('linux');
final isWebOnMacOS_ = _userAgent.contains('mac');

View File

@@ -58,6 +58,11 @@ class CursorManager {
]);
}
}
Future<void> resetSystemCursor() async {
latestKey = '';
js.context.callMethod('setByName', ['cursor', 'auto']);
}
}
class FlutterCustomMemoryImageCursor extends MouseCursor {
@@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
}
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
MouseCursor buildCursorOfCache(
model.CursorModel cursor, double scale, model.CursorData? cache) {

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/mobile/pages/scan_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:provider/provider.dart';
import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../models/model.dart';
class WebSettingsPage extends StatelessWidget {
const WebSettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isWebDesktop) {
return _buildDesktopButton(context);
} else {
return _buildMobileMenu(context);
}
}
Widget _buildDesktopButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
DesktopSettingPage(initialTabkey: SettingsTabKey.general),
),
);
},
);
}
Widget _buildMobileMenu(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
tooltip: "",
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
return (isIOS
? [
const PopupMenuItem(
value: "scan",
child: Icon(Icons.qr_code_scanner, color: Colors.black),
)
]
: <PopupMenuItem<String>>[]) +
[
PopupMenuItem(
value: "server",
child: Text(translate('ID/Relay Server')),
)
] +
[
PopupMenuItem(
value: "login",
child: Text(gFFI.userModel.userName.value.isEmpty
? translate("Login")
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
)
] +
[
PopupMenuItem(
value: "about",
child: Text(translate('About RustDesk')),
)
];
},
onSelected: (value) {
if (value == 'server') {
showServerSettings(gFFI.dialogManager);
}
if (value == 'about') {
showAbout(gFFI.dialogManager);
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
} else {
logOutConfirmDialog();
}
}
if (value == 'scan') {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ScanPage(),
),
);
}
});
}
}

View File

@@ -6,7 +6,7 @@ class TextureRgbaRenderer {
}
Future<bool> closeTexture(int key) {
throw UnimplementedError();
return Future(() => true);
}
Future<bool> onRgba(

View File

@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195

View File

@@ -380,6 +380,22 @@ packages:
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
source: git
version: "0.0.1+1"
extended_text:
dependency: "direct main"
description:
name: extended_text
sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
extended_text_library:
dependency: transitive
description:
name: extended_text_library
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
external_path:
dependency: "direct main"
description:
@@ -509,8 +525,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "38951317afe79d953ab25733667bd96e172a80d3"
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git
version: "0.0.1"
@@ -1613,5 +1629,5 @@ packages:
source: hosted
version: "0.2.1"
sdks:
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.3.0+46
version: 1.3.1+47
environment:
sdk: '^3.1.0'
@@ -93,7 +93,7 @@ dependencies:
flutter_gpu_texture_renderer:
git:
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 38951317afe79d953ab25733667bd96e172a80d3
ref: 2ded7f146437a761ffe6981e2f742038f85ca68d
uuid: ^3.0.7
auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0
@@ -104,6 +104,7 @@ dependencies:
pull_down_button: ^0.9.3
device_info_plus: ^9.1.0
qr_flutter: ^4.1.0
extended_text: 13.0.0
dev_dependencies:
icons_launcher: ^2.0.4

View File

@@ -5,7 +5,7 @@ use std::{
};
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
use hbb_common::{allow_err, log};
use hbb_common::{allow_err, bail};
use hbb_common::{
lazy_static,
tokio::sync::{
@@ -25,6 +25,8 @@ pub use context_send::*;
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
#[cfg(target_os = "windows")]
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
#[cfg(target_os = "windows")]
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
pub(crate) use platform::create_cliprdr_context;
@@ -130,7 +132,7 @@ impl ClipboardFile {
)
}
pub fn is_stopping_allowed_from_peer(&self) -> bool {
pub fn is_beginning_message(&self) -> bool {
matches!(
self,
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
@@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline]
fn send_data(conn_id: i32, data: ClipboardFile) {
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
#[cfg(target_os = "windows")]
return send_data_to_channel(conn_id, data);
#[cfg(not(target_os = "windows"))]
@@ -210,25 +212,28 @@ fn send_data(conn_id: i32, data: ClipboardFile) {
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline]
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
// no need to handle result here
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
if let Some(msg_channel) = VEC_MSG_CHANNEL
.read()
.unwrap()
.iter()
.find(|x| x.conn_id == conn_id)
{
allow_err!(msg_channel.sender.send(data));
msg_channel.sender.send(data)?;
Ok(())
} else {
bail!("conn_id not found");
}
}
#[cfg(feature = "unix-file-copy-paste")]
#[inline]
fn send_data_to_all(data: ClipboardFile) {
// no need to handle result here
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
// Need more tests to see if it's necessary to handle the error.
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
allow_err!(msg_channel.sender.send(data.clone()));
}
Ok(())
}
#[cfg(test)]

View File

@@ -7,7 +7,7 @@
use crate::{
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
};
use hbb_common::log;
use std::{
@@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE)
}
};
// no need to handle result here
send_data(conn_id as _, data);
allow_err!(send_data(conn_id as _, data));
0
}
@@ -1045,7 +1045,13 @@ extern "C" fn client_format_list(
.iter()
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
} else {
send_data(conn_id, data);
match send_data(conn_id, data) {
Ok(_) => {}
Err(e) => {
log::error!("failed to send format list: {:?}", e);
return ERR_CODE_SEND_MSG;
}
}
}
0
@@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response(
msg_flags
);
let data = ClipboardFile::FormatListResponse { msg_flags };
send_data(conn_id, data);
0
match send_data(conn_id, data) {
Ok(_) => 0,
Err(e) => {
log::error!("failed to send format list response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
}
extern "C" fn client_format_data_request(
@@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request(
conn_id,
requested_format_id
);
// no need to handle result here
send_data(conn_id, data);
0
match send_data(conn_id, data) {
Ok(_) => 0,
Err(e) => {
log::error!("failed to send format data request: {:?}", e);
ERR_CODE_SEND_MSG
}
}
}
extern "C" fn client_format_data_response(
@@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response(
msg_flags,
format_data,
};
send_data(conn_id, data);
0
match send_data(conn_id, data) {
Ok(_) => 0,
Err(e) => {
log::error!("failed to send format data response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
}
extern "C" fn client_file_contents_request(
@@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request(
clip_data_id,
};
log::debug!("client_file_contents_request called, data: {:?}", &data);
send_data(conn_id, data);
0
match send_data(conn_id, data) {
Ok(_) => 0,
Err(e) => {
log::error!("failed to send file contents request: {:?}", e);
ERR_CODE_SEND_MSG
}
}
}
extern "C" fn client_file_contents_response(
@@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response(
msg_flags,
stream_id
);
send_data(conn_id, data);
0
match send_data(conn_id, data) {
Ok(_) => 0,
Err(e) => {
log::error!("failed to send file contents response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
}

View File

@@ -220,7 +220,8 @@ struct wf_clipboard
HWND hwnd;
HANDLE hmem;
HANDLE thread;
HANDLE response_data_event;
HANDLE formatDataRespEvent;
BOOL formatDataRespReceived;
LPDATAOBJECT data_obj;
HANDLE data_obj_mutex;
@@ -228,6 +229,7 @@ struct wf_clipboard
ULONG req_fsize;
char *req_fdata;
HANDLE req_fevent;
BOOL req_f_received;
size_t nFiles;
size_t file_array_size;
@@ -287,6 +289,9 @@ static BOOL try_open_clipboard(HWND hwnd)
static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid,
void **ppvObject)
{
if (ppvObject == NULL)
return E_INVALIDARG;
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
{
IStream_AddRef(This);
@@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
}
*pcbRead = clipboard->req_fsize;
// Check overflow, can not be a real case
if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) {
// It's better to crash to release the explorer.exe
// This is a critical error, because the explorer is waiting for the data
// and the m_lOffset is wrong(overflowed)
return S_FALSE;
}
instance->m_lOffset.QuadPart += clipboard->req_fsize;
if (clipboard->req_fsize < cb)
@@ -517,11 +529,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp
static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc)
{
IStream *iStream;
IStream *iStream = NULL;
BOOL success = FALSE;
BOOL isDir = FALSE;
CliprdrStream *instance;
CliprdrStream *instance = NULL;
wfClipboard *clipboard = (wfClipboard *)pData;
if (!(pData && dsc))
{
return NULL;
}
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
if (instance)
@@ -874,14 +892,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This
static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count,
void *data)
{
CliprdrDataObject *instance;
IDataObject *iDataObject;
CliprdrDataObject *instance = NULL;
IDataObject *iDataObject = NULL;
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
if (!instance)
goto error;
instance->m_pFormatEtc = NULL;
instance->m_pStgMedium = NULL;
iDataObject = &instance->iDataObject;
iDataObject->lpVtbl = NULL;
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
if (!iDataObject->lpVtbl)
@@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc
return instance;
error:
CliprdrDataObject_Delete(instance);
if (iDataObject && iDataObject->lpVtbl)
{
free(iDataObject->lpVtbl);
}
if (instance)
{
if (instance->m_pFormatEtc)
{
free(instance->m_pFormatEtc);
}
if (instance->m_pStgMedium)
{
free(instance->m_pStgMedium);
}
CliprdrDataObject_Delete(instance);
}
return NULL;
}
@@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT
REFIID riid, void **ppvObject)
{
(void)This;
if (!ppvObject)
return E_INVALIDARG;
if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown))
{
@@ -1198,6 +1239,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
WCHAR *unicode_name;
#if !defined(UNICODE)
size_t size;
int towchar_count;
#endif
if (!clipboard || !format_name)
@@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
#if defined(UNICODE)
unicode_name = _wcsdup(format_name);
if (!unicode_name)
return 0;
#else
size = _tcslen(format_name);
unicode_name = calloc(size + 1, sizeof(WCHAR));
@@ -1212,11 +1256,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
if (!unicode_name)
return 0;
MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
#endif
if (!unicode_name)
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0);
if (towchar_count <= 0 || towchar_count > size)
return 0;
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
if (towchar_count <= 0)
return 0;
#endif
for (i = 0; i < clipboard->map_size; i++)
{
@@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
if (!clipboard)
return -1;
// to-do:
// Directly use the environment variable `TEMP` is not safe.
// But this function is not used for now.
if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) ==
0)
return -1;
@@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID)
return rc;
}
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data)
// Ensure the event is not signaled, and reset it if it is.
UINT try_reset_event(HANDLE event)
{
if (!event)
{
return ERROR_INTERNAL_ERROR;
}
DWORD result = WaitForSingleObject(event, 0);
if (result == WAIT_OBJECT_0)
{
if (!ResetEvent(event))
{
return GetLastError();
}
else
{
return ERROR_SUCCESS;
}
}
else if (result == WAIT_TIMEOUT)
{
return ERROR_SUCCESS;
}
else
{
return ERROR_INTERNAL_ERROR;
}
}
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data)
{
UINT rc = ERROR_SUCCESS;
clipboard->context->IsStopped = FALSE;
@@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE)
{
continue;
if ((*recvedFlag) == TRUE) {
// The data has been received, but the event is still not signaled.
// We just skip the rest of the waiting and reset the flag.
*recvedFlag = FALSE;
// Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data.
waitRes = WAIT_OBJECT_0;
} else {
// The data has not been received yet, we should continue to wait.
continue;
}
}
if (!ResetEvent(event))
{
// NOTE: critical error here, crash may be better
}
if (clipboard->context->IsStopped == TRUE)
@@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
return ERROR_INTERNAL_ERROR;
}
if (!ResetEvent(event))
{
// NOTE: critical error here, crash may be better
rc = ERROR_INTERNAL_ERROR;
}
if ((*data) == NULL)
{
rc = ERROR_INTERNAL_ERROR;
@@ -1519,6 +1606,13 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest)
return ERROR_INTERNAL_ERROR;
rc = try_reset_event(clipboard->formatDataRespEvent);
if (rc != ERROR_SUCCESS)
{
return rc;
}
clipboard->formatDataRespReceived = FALSE;
remoteFormatId = get_remote_format_id(clipboard, formatId);
formatDataRequest.connID = connID;
@@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
return rc;
}
wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem);
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
}
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
@@ -1543,7 +1637,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
return ERROR_INTERNAL_ERROR;
rc = try_reset_event(clipboard->req_fevent);
if (rc != ERROR_SUCCESS)
{
return rc;
}
clipboard->req_f_received = FALSE;
fileContentsRequest.connID = connID;
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
// But it is OK, because it is only used to check if the stream is the same in
// `wf_cliprdr_server_file_contents_request()` function.
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
fileContentsRequest.listIndex = index;
fileContentsRequest.dwFlags = flag;
@@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
return rc;
}
return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata);
return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata);
}
static UINT cliprdr_send_response_filecontents(
@@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
break;
case WM_DESTROYCLIPBOARD:
// to-do: clear clipboard data?
case WM_ASKCBFORMATNAME:
case WM_HSCROLLCLIPBOARD:
case WM_PAINTCLIPBOARD:
@@ -1904,7 +2009,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
LONG positionHigh, DWORD nRequested, DWORD *puSize)
{
BOOL res = FALSE;
HANDLE hFile;
HANDLE hFile = NULL;
DWORD nGet, rc;
if (!file_name || !buffer || !puSize)
@@ -1932,9 +2037,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
res = TRUE;
error:
if (!CloseHandle(hFile))
res = FALSE;
if (hFile)
{
if (!CloseHandle(hFile))
res = FALSE;
}
if (res)
*puSize = nGet;
@@ -1945,8 +2052,8 @@ error:
/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */
static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen)
{
HANDLE hFile;
FILEDESCRIPTORW *fd;
HANDLE hFile = NULL;
FILEDESCRIPTORW *fd = NULL;
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
if (!fd)
@@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
}
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen);
if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0]))
{
// The file name is too long, which is not a normal case.
// So we just return NULL.
CloseHandle(hFile);
free(fd);
return NULL;
}
wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1);
CloseHandle(hFile);
return fd;
@@ -2024,7 +2140,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
if (!clipboard->file_names[clipboard->nFiles])
return FALSE;
wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name);
// `MAX_PATH` is long enough for the file name.
// So we just return FALSE if the file name is too long, which is not a normal case.
if ((wcslen(full_file_name) + 1) > MAX_PATH)
return FALSE;
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
/* add to descriptor array */
clipboard->fileDescriptor[clipboard->nFiles] =
wf_cliprdr_get_file_descriptor(full_file_name, pathLen);
@@ -2048,8 +2169,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
if (!clipboard || !Dir)
return FALSE;
// StringCchCopy(DirSpec, MAX_PATH, Dir);
// StringCchCat(DirSpec, MAX_PATH, TEXT("\\*"));
if (wcslen(Dir) + 3 > MAX_PATH)
return FALSE;
StringCchCopyW(DirSpec, MAX_PATH, Dir);
StringCchCatW(DirSpec, MAX_PATH, L"\\*");
@@ -2078,9 +2199,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
WCHAR DirAdd[MAX_PATH];
// StringCchCopy(DirAdd, MAX_PATH, Dir);
// StringCchCat(DirAdd, MAX_PATH, _T("\\"));
// StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
return FALSE;
StringCchCopyW(DirAdd, MAX_PATH, Dir);
StringCchCatW(DirAdd, MAX_PATH, L"\\");
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
@@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
else
{
WCHAR fileName[MAX_PATH];
// StringCchCopy(fileName, MAX_PATH, Dir);
// StringCchCat(fileName, MAX_PATH, _T("\\"));
// StringCchCat(fileName, MAX_PATH, FindFileData.cFileName);
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
return FALSE;
StringCchCopyW(fileName, MAX_PATH, Dir);
StringCchCatW(fileName, MAX_PATH, L"\\");
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
@@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
if (context->EnableFiles)
{
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
*p_conn_id = formatList->connID;
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
rc = CHANNEL_RC_OK;
if (p_conn_id) {
*p_conn_id = formatList->connID;
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
rc = CHANNEL_RC_OK;
}
}
else
{
@@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
format_ids->connID = formatList->connID;
format_ids->size = (UINT32)clipboard->map_size;
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
for (i = 0; i < format_ids->size; ++i)
if (format_ids)
{
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
}
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
{
rc = CHANNEL_RC_OK;
format_ids->connID = formatList->connID;
format_ids->size = (UINT32)clipboard->map_size;
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
if (format_ids->formats)
{
for (i = 0; i < format_ids->size; ++i)
{
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
}
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
{
rc = CHANNEL_RC_OK;
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
}
else
{
@@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
p += len + 1, clipboard->nFiles++)
{
int cchWideChar;
WCHAR *wFileName;
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
if (wFileName)
{
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
free(wFileName);
}
else
{
rc = ERROR_INTERNAL_ERROR;
GlobalUnlock(stg_medium.hGlobal);
ReleaseStgMedium(&stg_medium);
goto exit;
}
}
}
GlobalUnlock(stg_medium.hGlobal);
ReleaseStgMedium(&stg_medium);
resp:
// size will not overflow, because size type is size_t (unsigned __int64)
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
@@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
globlemem = (char *)GlobalLock(hClipdata);
size = (int)GlobalSize(hClipdata);
buff = malloc(size);
CopyMemory(buff, globlemem, size);
if (buff)
{
CopyMemory(buff, globlemem, size);
rc = ERROR_SUCCESS;
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
GlobalUnlock(hClipdata);
CloseClipboard();
rc = ERROR_SUCCESS;
}
}
else
@@ -2545,7 +2697,7 @@ exit:
response.requestedFormatData = (BYTE *)buff;
if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response))
{
// CAUTION: if failed to send, server will wait a long time
// CAUTION: if failed to send, server will wait a long time, default 30 seconds.
}
if (buff)
@@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
rc = CHANNEL_RC_OK;
} while (0);
if (!SetEvent(clipboard->response_data_event))
if (!SetEvent(clipboard->formatDataRespEvent))
{
// CAUTION: critical error here, process will hang up until wait timeout default 3min.
// If failed to set event, set flag to indicate the event is received.
DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError());
clipboard->formatDataRespReceived = TRUE;
rc = ERROR_INTERNAL_ERROR;
}
return rc;
@@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
if (!SetEvent(clipboard->req_fevent))
{
// CAUTION: critical error here, process will hang up until wait timeout default 3min.
// If failed to set event, set flag to indicate the event is received.
DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError());
clipboard->req_f_received = TRUE;
}
return rc;
}
@@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
goto error;
if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL)))
if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
goto error;
clipboard->formatDataRespReceived = FALSE;
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
goto error;
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
goto error;
clipboard->req_f_received = FALSE;
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
goto error;
@@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
clipboard->data_obj = NULL;
}
if (clipboard->response_data_event)
CloseHandle(clipboard->response_data_event);
if (clipboard->formatDataRespEvent)
CloseHandle(clipboard->formatDataRespEvent);
if (clipboard->data_obj_mutex)
CloseHandle(clipboard->data_obj_mutex);

View File

@@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3;
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
const BUF_LEN: usize = 4;
const MOUSE_EVENT_BUTTON_NUMBER_BACK: i64 = 3;
const MOUSE_EVENT_BUTTON_NUMBER_FORWARD: i64 = 4;
/// The event source user data value of cgevent.
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
@@ -226,14 +229,24 @@ impl MouseControllable for Enigo {
}
self.last_click_time = Some(now);
let (current_x, current_y) = Self::mouse_location();
let (button, event_type) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown),
let (button, event_type, btn_value) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, None),
MouseButton::Back => (
CGMouseButton::Left,
CGEventType::OtherMouseDown,
Some(MOUSE_EVENT_BUTTON_NUMBER_BACK),
),
MouseButton::Forward => (
CGMouseButton::Left,
CGEventType::OtherMouseDown,
Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD),
),
_ => {
log::info!("Unsupported button {:?}", button);
return Ok(());
},
}
};
let dest = CGPoint::new(current_x as f64, current_y as f64);
if let Some(src) = self.event_source.as_ref() {
@@ -244,6 +257,9 @@ impl MouseControllable for Enigo {
self.multiple_click,
);
}
if let Some(v) = btn_value {
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
}
self.post(event);
}
}
@@ -252,14 +268,24 @@ impl MouseControllable for Enigo {
fn mouse_up(&mut self, button: MouseButton) {
let (current_x, current_y) = Self::mouse_location();
let (button, event_type) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp),
let (button, event_type, btn_value) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp, None),
MouseButton::Back => (
CGMouseButton::Left,
CGEventType::OtherMouseUp,
Some(MOUSE_EVENT_BUTTON_NUMBER_BACK),
),
MouseButton::Forward => (
CGMouseButton::Left,
CGEventType::OtherMouseUp,
Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD),
),
_ => {
log::info!("Unsupported button {:?}", button);
return;
},
}
};
let dest = CGPoint::new(current_x as f64, current_y as f64);
if let Some(src) = self.event_source.as_ref() {
@@ -270,6 +296,9 @@ impl MouseControllable for Enigo {
self.multiple_click,
);
}
if let Some(v) = btn_value {
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
}
self.post(event);
}
}
@@ -345,7 +374,7 @@ impl KeyboardControllable for Enigo {
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
self
}
fn key_sequence(&mut self, sequence: &str) {
// NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68
// TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time
@@ -382,12 +411,10 @@ impl KeyboardControllable for Enigo {
fn key_down(&mut self, key: Key) -> crate::ResultType {
let code = self.key_to_keycode(key);
if code == u16::MAX {
return Err("".into());
return Err("".into());
}
if let Some(src) = self.event_source.as_ref() {
if let Ok(event) =
CGEvent::new_keyboard_event(src.clone(), code, true)
{
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) {
self.post(event);
}
}

View File

@@ -326,6 +326,7 @@ enum ClipboardFormat {
ImageRgba = 21;
ImagePng = 22;
ImageSvg = 23;
Special = 31;
}
message Clipboard {
@@ -334,6 +335,8 @@ message Clipboard {
int32 width = 3;
int32 height = 4;
ClipboardFormat format = 5;
// Special format name, only used when format is Special.
string special_name = 6;
}
message MultiClipboards { repeated Clipboard clipboards = 1; }

View File

@@ -39,7 +39,7 @@ pub const REG_INTERVAL: i64 = 15_000;
pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3;
const PASSWORD_ENC_VERSION: &str = "00";
const ENCRYPT_MAX_LEN: usize = 128;
pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all
#[cfg(target_os = "macos")]
lazy_static::lazy_static! {
@@ -296,6 +296,8 @@ pub struct PeerConfig {
pub keyboard_mode: String,
#[serde(flatten)]
pub view_only: ViewOnly,
#[serde(flatten)]
pub sync_init_clipboard: SyncInitClipboard,
// Mouse wheel or touchpad scroll mode
#[serde(
default = "PeerConfig::default_reverse_mouse_wheel",
@@ -373,6 +375,7 @@ impl Default for PeerConfig {
ui_flutter: Default::default(),
info: Default::default(),
transfer: Default::default(),
sync_init_clipboard: Default::default(),
}
}
}
@@ -1462,6 +1465,13 @@ serde_field_bool!(
"ViewOnly::default_view_only"
);
serde_field_bool!(
SyncInitClipboard,
"sync-init-clipboard",
default_sync_init_clipboard,
"SyncInitClipboard::default_sync_init_clipboard"
);
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct LocalConfig {
#[serde(default, deserialize_with = "deserialize_string")]
@@ -2156,6 +2166,7 @@ pub mod keys {
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference";
pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard";
pub const OPTION_THEME: &str = "theme";
pub const OPTION_LANGUAGE: &str = "lang";
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
@@ -2218,6 +2229,9 @@ pub mod keys {
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
pub const OPTION_HIDE_TRAY: &str = "hide-tray";
pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection";
pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password";
pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer";
// flutter local options
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
@@ -2276,6 +2290,7 @@ pub mod keys {
OPTION_CUSTOM_IMAGE_QUALITY,
OPTION_CUSTOM_FPS,
OPTION_CODEC_PREFERENCE,
OPTION_SYNC_INIT_CLIPBOARD,
];
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
@@ -2362,6 +2377,9 @@ pub mod keys {
OPTION_HIDE_HELP_CARDS,
OPTION_DEFAULT_CONNECT_PASSWORD,
OPTION_HIDE_TRAY,
OPTION_ONE_WAY_CLIPBOARD_REDIRECTION,
OPTION_ALLOW_LOGON_SCREEN_PASSWORD,
OPTION_ONE_WAY_FILE_TRANSFER,
];
}

View File

@@ -89,11 +89,11 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String
log::error!("Duplicate encryption!");
return s.to_owned();
}
if s.bytes().len() > max_len {
if s.chars().count() > max_len {
return String::default();
}
if version == "00" {
if let Ok(s) = encrypt(s.as_bytes(), max_len) {
if let Ok(s) = encrypt(s.as_bytes()) {
return version.to_owned() + &s;
}
}
@@ -130,7 +130,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u
return vec![];
}
if version == "00" {
if let Ok(s) = encrypt(v, max_len) {
if let Ok(s) = encrypt(v) {
let mut version = version.to_owned().into_bytes();
version.append(&mut s.into_bytes());
return version;
@@ -155,8 +155,8 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, boo
(v.to_owned(), false, !v.is_empty())
}
fn encrypt(v: &[u8], max_len: usize) -> Result<String, ()> {
if !v.is_empty() && v.len() <= max_len {
fn encrypt(v: &[u8]) -> Result<String, ()> {
if !v.is_empty() {
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
} else {
Err(())

View File

@@ -7,6 +7,9 @@ lazy_static::lazy_static! {
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
pub const DISPLAY_SERVER_X11: &str = "x11";
pub const DISPLAY_DESKTOP_KDE: &str = "KDE";
pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
pub struct Distro {
pub name: String,
@@ -29,6 +32,15 @@ impl Distro {
}
}
#[inline]
pub fn is_kde() -> bool {
if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) {
env == DISPLAY_DESKTOP_KDE
} else {
false
}
}
#[inline]
pub fn is_gdm_user(username: &str) -> bool {
username == "gdm"

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.3.0"
version = "1.3.1"
edition = "2021"
description = "RustDesk Remote Desktop"

View File

@@ -498,6 +498,15 @@ pub struct HwCodecConfig {
pub vram_decode: Vec<hwcodec::vram::DecodeContext>,
}
// HwCodecConfig2 is used to store the config in json format,
// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version.
// struct T { a: Vec<A>, b: Vec<String>} will fail if b is empty, but struct T { a: Vec<String>, b: Vec<String>} is ok.
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
struct HwCodecConfig2 {
#[serde(default)]
pub config: String,
}
// ipc server process start check process once, other process get from ipc server once
// install: --server start check process, check process send to --server, ui get from --server
// portable: ui start check process, check process send to ui
@@ -509,7 +518,12 @@ impl HwCodecConfig {
log::info!("set hwcodec config");
log::debug!("{config:?}");
#[cfg(any(windows, target_os = "macos"))]
hbb_common::config::common_store(&config, "_hwcodec");
hbb_common::config::common_store(
&HwCodecConfig2 {
config: serde_json::to_string_pretty(&config).unwrap_or_default(),
},
"_hwcodec",
);
*CONFIG.lock().unwrap() = Some(config);
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
}
@@ -587,7 +601,8 @@ impl HwCodecConfig {
Some(c) => c,
None => {
log::info!("try load cached hwcodec config");
let c = hbb_common::config::common_load::<HwCodecConfig>("_hwcodec");
let c = hbb_common::config::common_load::<HwCodecConfig2>("_hwcodec");
let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default();
let new_signature = hwcodec::common::get_gpu_signature();
if c.signature == new_signature {
log::debug!("load cached hwcodec config: {c:?}");

View File

@@ -316,7 +316,7 @@ impl ToString for CodecFormat {
CodecFormat::AV1 => "AV1".into(),
CodecFormat::H264 => "H264".into(),
CodecFormat::H265 => "H265".into(),
CodecFormat::Unknown => "Unknow".into(),
CodecFormat::Unknown => "Unknown".into(),
}
}
}

View File

@@ -27,39 +27,40 @@ use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_porta
use lazy_static::lazy_static;
lazy_static! {
pub static ref RDP_RESPONSE: Mutex<Option<RdpResponse>> = Mutex::new(None);
pub static ref RDP_SESSION_INFO: Mutex<Option<RdpSessionInfo>> = Mutex::new(None);
}
#[inline]
pub fn close_session() {
let _ = RDP_RESPONSE.lock().unwrap().take();
let _ = RDP_SESSION_INFO.lock().unwrap().take();
}
#[inline]
pub fn is_rdp_session_hold() -> bool {
RDP_RESPONSE.lock().unwrap().is_some()
RDP_SESSION_INFO.lock().unwrap().is_some()
}
pub fn try_close_session() {
let mut rdp_res = RDP_RESPONSE.lock().unwrap();
let mut rdp_info = RDP_SESSION_INFO.lock().unwrap();
let mut close = false;
if let Some(rdp_res) = &*rdp_res {
if let Some(rdp_info) = &*rdp_info {
// If is server running and restore token is supported, there's no need to keep the session.
if is_server_running() && rdp_res.is_support_restore_token {
if is_server_running() && rdp_info.is_support_restore_token {
close = true;
}
}
if close {
*rdp_res = None;
*rdp_info = None;
}
}
pub struct RdpResponse {
pub struct RdpSessionInfo {
pub conn: Arc<SyncConnection>,
pub streams: Vec<PwStreamInfo>,
pub fd: OwnedFd,
pub session: dbus::Path<'static>,
pub is_support_restore_token: bool,
pub resolution: Arc<Mutex<Option<(usize, usize)>>>,
}
#[derive(Debug, Clone, Copy)]
pub struct PwStreamInfo {
@@ -69,6 +70,12 @@ pub struct PwStreamInfo {
size: (usize, usize),
}
impl PwStreamInfo {
pub fn get_size(&self) -> (usize, usize) {
self.size
}
}
#[derive(Debug)]
pub struct DBusError(String);
@@ -105,24 +112,31 @@ pub struct PipeWireCapturable {
}
impl PipeWireCapturable {
fn new(conn: Arc<SyncConnection>, fd: OwnedFd, stream: PwStreamInfo) -> Self {
fn new(
conn: Arc<SyncConnection>,
fd: OwnedFd,
resolution: Arc<Mutex<Option<(usize, usize)>>>,
stream: PwStreamInfo,
) -> Self {
// alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling
// https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244
let res = get_res(Self {
let size = get_res(Self {
dbus_conn: conn.clone(),
fd: fd.clone(),
path: stream.path,
source_type: stream.source_type,
position: stream.position,
size: stream.size,
});
})
.unwrap_or(stream.size);
*resolution.lock().unwrap() = Some(size);
Self {
dbus_conn: conn,
fd,
path: stream.path,
source_type: stream.source_type,
position: stream.position,
size: res.unwrap_or(stream.size),
size,
}
}
}
@@ -813,7 +827,7 @@ fn on_start_response(
}
pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
let mut rdp_connection = match RDP_RESPONSE.lock() {
let mut rdp_connection = match RDP_SESSION_INFO.lock() {
Ok(conn) => conn,
Err(err) => return Err(Box::new(err)),
};
@@ -822,28 +836,36 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
let conn = Arc::new(conn);
let rdp_res = RdpResponse {
let rdp_info = RdpSessionInfo {
conn,
streams,
fd,
session,
is_support_restore_token,
resolution: Arc::new(Mutex::new(None)),
};
*rdp_connection = Some(rdp_res);
*rdp_connection = Some(rdp_info);
}
let rdp_res = match rdp_connection.as_ref() {
let rdp_info = match rdp_connection.as_ref() {
Some(res) => res,
None => {
return Err(Box::new(DBusError("RDP response is None.".into())));
}
};
Ok(rdp_res
Ok(rdp_info
.streams
.clone()
.into_iter()
.map(|s| PipeWireCapturable::new(rdp_res.conn.clone(), rdp_res.fd.clone(), s))
.map(|s| {
PipeWireCapturable::new(
rdp_info.conn.clone(),
rdp_info.fd.clone(),
rdp_info.resolution.clone(),
s,
)
})
.collect())
}

View File

@@ -1,3 +1,4 @@
use hbb_common::libc;
use std::ptr;
use std::rc::Rc;
@@ -99,11 +100,16 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error>
if reply.is_null() {
// TODO: Should seperate SHM disabled from SHM not supported?
return Err(Error::UnsupportedExtension);
} else if e.is_null() {
return Ok(());
} else {
// TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here?
return Err(Error::Generic);
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
libc::free(reply as *mut _);
if e.is_null() {
return Ok(());
} else {
libc::free(e as *mut _);
// TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here?
return Err(Error::Generic);
}
}
}

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk
pkgver=1.3.0
pkgver=1.3.1
pkgrel=0
epoch=
pkgdesc=""

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>RustDesk</vendor>
<vendor_url>https://rustdesk.com/</vendor_url>
<icon_name>rustdesk</icon_name>
<action id="com.rustdesk.RustDesk.options">
<description>Change RustDesk options</description>
<message>Authentication is required to change RustDesk options</message>
<message xml:lang="zh_CN">要更改RustDesk选项, 需要您先通过身份验证</message>
<message xml:lang="zh_TW">要變更RustDesk選項, 需要您先通過身份驗證</message>
<message xml:lang="de">Authentifizierung zum Ändern der RustDesk-Optionen</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/share/rustdesk/files/polkit</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>

View File

@@ -1,9 +1,10 @@
Name: rustdesk
Version: 1.3.0
Version: 1.3.1
Release: 0
Summary: RPM package
License: GPL-3.0
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
%description

View File

@@ -1,9 +1,10 @@
Name: rustdesk
Version: 1.3.0
Version: 1.3.1
Release: 0
Summary: RPM package
License: GPL-3.0
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
%description

View File

@@ -3,7 +3,8 @@ Version: 1.1.9
Release: 0
Summary: RPM package
License: GPL-3.0
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1
%description
The best open-source remote desktop client software, written in Rust.

View File

@@ -1,9 +1,10 @@
Name: rustdesk
Version: 1.3.0
Version: 1.3.1
Release: 0
Summary: RPM package
License: GPL-3.0
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3
%description
The best open-source remote desktop client software, written in Rust.

View File

@@ -1,95 +0,0 @@
From afe89a70f6bc7ebd0a6a0a31101801b88cbd60ee Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com>
Date: Sun, 5 May 2024 12:45:23 +0800
Subject: [PATCH] use release/7.0's update_bitrate
Signed-off-by: 21pages <pages21@163.com>
---
libavcodec/qsvenc.c | 39 +++++++++++++++++++++++++++++++++++++++
libavcodec/qsvenc.h | 6 ++++++
2 files changed, 45 insertions(+)
diff --git a/libavcodec/qsvenc.c b/libavcodec/qsvenc.c
index 2382c2f5f7..9b34f37eb3 100644
--- a/libavcodec/qsvenc.c
+++ b/libavcodec/qsvenc.c
@@ -714,6 +714,11 @@ static int init_video_param(AVCodecContext *avctx, QSVEncContext *q)
brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes),
initial_delay_in_kilobytes) + 0x10000) / 0x10000;
+ q->old_rc_buffer_size = avctx->rc_buffer_size;
+ q->old_rc_initial_buffer_occupancy = avctx->rc_initial_buffer_occupancy;
+ q->old_bit_rate = avctx->bit_rate;
+ q->old_rc_max_rate = avctx->rc_max_rate;
+
switch (q->param.mfx.RateControlMethod) {
case MFX_RATECONTROL_CBR:
case MFX_RATECONTROL_VBR:
@@ -1657,6 +1662,39 @@ static int update_qp(AVCodecContext *avctx, QSVEncContext *q,
return updated;
}
+static int update_bitrate(AVCodecContext *avctx, QSVEncContext *q)
+{
+ int updated = 0;
+ int target_bitrate_kbps, max_bitrate_kbps, brc_param_multiplier;
+ int buffer_size_in_kilobytes, initial_delay_in_kilobytes;
+
+ UPDATE_PARAM(q->old_rc_buffer_size, avctx->rc_buffer_size);
+ UPDATE_PARAM(q->old_rc_initial_buffer_occupancy, avctx->rc_initial_buffer_occupancy);
+ UPDATE_PARAM(q->old_bit_rate, avctx->bit_rate);
+ UPDATE_PARAM(q->old_rc_max_rate, avctx->rc_max_rate);
+ if (!updated)
+ return 0;
+
+ buffer_size_in_kilobytes = avctx->rc_buffer_size / 8000;
+ initial_delay_in_kilobytes = avctx->rc_initial_buffer_occupancy / 8000;
+ target_bitrate_kbps = avctx->bit_rate / 1000;
+ max_bitrate_kbps = avctx->rc_max_rate / 1000;
+ brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes),
+ initial_delay_in_kilobytes) + 0x10000) / 0x10000;
+
+ q->param.mfx.BufferSizeInKB = buffer_size_in_kilobytes / brc_param_multiplier;
+ q->param.mfx.InitialDelayInKB = initial_delay_in_kilobytes / brc_param_multiplier;
+ q->param.mfx.TargetKbps = target_bitrate_kbps / brc_param_multiplier;
+ q->param.mfx.MaxKbps = max_bitrate_kbps / brc_param_multiplier;
+ q->param.mfx.BRCParamMultiplier = brc_param_multiplier;
+ av_log(avctx, AV_LOG_VERBOSE,
+ "Reset BufferSizeInKB: %d; InitialDelayInKB: %d; "
+ "TargetKbps: %d; MaxKbps: %d; BRCParamMultiplier: %d\n",
+ q->param.mfx.BufferSizeInKB, q->param.mfx.InitialDelayInKB,
+ q->param.mfx.TargetKbps, q->param.mfx.MaxKbps, q->param.mfx.BRCParamMultiplier);
+ return updated;
+}
+
static int update_parameters(AVCodecContext *avctx, QSVEncContext *q,
const AVFrame *frame)
{
@@ -1666,6 +1704,7 @@ static int update_parameters(AVCodecContext *avctx, QSVEncContext *q,
return 0;
needReset = update_qp(avctx, q, frame);
+ needReset |= update_bitrate(avctx, q);
if (!needReset)
return 0;
diff --git a/libavcodec/qsvenc.h b/libavcodec/qsvenc.h
index b754ac4b56..5745533165 100644
--- a/libavcodec/qsvenc.h
+++ b/libavcodec/qsvenc.h
@@ -224,6 +224,12 @@ typedef struct QSVEncContext {
int min_qp_p;
int max_qp_b;
int min_qp_b;
+
+ // These are used for bitrate control reset
+ int old_bit_rate;
+ int old_rc_buffer_size;
+ int old_rc_initial_buffer_occupancy;
+ int old_rc_max_rate;
} QSVEncContext;
int ff_qsv_enc_init(AVCodecContext *avctx, QSVEncContext *q);
--
2.43.0.windows.1

View File

@@ -1,40 +0,0 @@
From be3d9d8092720bbe4239212648d2e9c4ffd7f40c Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com>
Date: Wed, 22 May 2024 17:09:28 +0800
Subject: [PATCH] android mediacodec encode align 64
Signed-off-by: 21pages <pages21@163.com>
---
libavcodec/mediacodecenc.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c
index 984014f1b1..8dcd3dcd64 100644
--- a/libavcodec/mediacodecenc.c
+++ b/libavcodec/mediacodecenc.c
@@ -200,16 +200,17 @@ static av_cold int mediacodec_init(AVCodecContext *avctx)
ff_AMediaFormat_setString(format, "mime", codec_mime);
// Workaround the alignment requirement of mediacodec. We can't do it
// silently for AV_PIX_FMT_MEDIACODEC.
+ const int align = 64;
if (avctx->pix_fmt != AV_PIX_FMT_MEDIACODEC) {
- s->width = FFALIGN(avctx->width, 16);
- s->height = FFALIGN(avctx->height, 16);
+ s->width = FFALIGN(avctx->width, align);
+ s->height = FFALIGN(avctx->height, align);
} else {
s->width = avctx->width;
s->height = avctx->height;
- if (s->width % 16 || s->height % 16)
+ if (s->width % align || s->height % align)
av_log(avctx, AV_LOG_WARNING,
- "Video size %dx%d isn't align to 16, it may have device compatibility issue\n",
- s->width, s->height);
+ "Video size %dx%d isn't align to %d, it may have device compatibility issue\n",
+ s->width, s->height, align);
}
ff_AMediaFormat_setInt32(format, "width", s->width);
ff_AMediaFormat_setInt32(format, "height", s->height);
--
2.34.1

View File

@@ -1,9 +1,9 @@
From f0b694749b38b2cfd94df4eed10e667342c234e5 Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com>
Date: Sat, 24 Feb 2024 15:33:24 +0800
Subject: [PATCH 1/2] avcodec/amfenc: add query_timeout option for h264/hevc
From f6988e5424e041ff6f6e241f4d8fa69a04c05e64 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Thu, 5 Sep 2024 16:26:20 +0800
Subject: [PATCH 1/3] avcodec/amfenc: add query_timeout option for h264/hevc
Signed-off-by: 21pages <pages21@163.com>
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/amfenc.h | 1 +
libavcodec/amfenc_h264.c | 4 ++++
@@ -11,10 +11,10 @@ Signed-off-by: 21pages <pages21@163.com>
3 files changed, 9 insertions(+)
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index 1ab98d2f78..e92120ea39 100644
index 2dbd378ef8..d636673a9d 100644
--- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h
@@ -87,6 +87,7 @@ typedef struct AmfContext {
@@ -89,6 +89,7 @@ typedef struct AmfContext {
int quality;
int b_frame_delta_qp;
int ref_b_frame_delta_qp;
@@ -23,40 +23,40 @@ index 1ab98d2f78..e92120ea39 100644
// Dynamic options, can be set after Init() call
diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c
index efb04589f6..f55dbc80f0 100644
index c1d5f4054e..415828f005 100644
--- a/libavcodec/amfenc_h264.c
+++ b/libavcodec/amfenc_h264.c
@@ -121,6 +121,7 @@ static const AVOption options[] = {
@@ -135,6 +135,7 @@ static const AVOption options[] = {
{ "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE },
{ "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE },
+ { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE },
{ NULL }
};
@@ -155,6 +156,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx)
//Pre Analysis options
{ "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE },
@@ -222,6 +223,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate);
+ if (ctx->query_timeout >= 0)
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout);
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout);
+
switch (avctx->profile) {
case FF_PROFILE_H264_BASELINE:
case AV_PROFILE_H264_BASELINE:
profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE;
diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c
index 8ab9330730..7a40bcad31 100644
index 33a167aa52..65259d7153 100644
--- a/libavcodec/amfenc_hevc.c
+++ b/libavcodec/amfenc_hevc.c
@@ -89,6 +89,7 @@ static const AVOption options[] = {
@@ -98,6 +98,7 @@ static const AVOption options[] = {
{ "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE },
{ "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE },
+ { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE },
{ NULL }
};
@@ -122,6 +123,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx)
//Pre Analysis options
{ "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE },
@@ -183,6 +184,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate);
@@ -64,7 +64,7 @@ index 8ab9330730..7a40bcad31 100644
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout);
+
switch (avctx->profile) {
case FF_PROFILE_HEVC_MAIN:
case AV_PROFILE_HEVC_MAIN:
profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN;
--
2.43.0.windows.1

View File

@@ -1,16 +1,16 @@
From 4d0d20d96ad458cfec0444b9be0182ca6085ee0c Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com>
Date: Sat, 24 Feb 2024 16:02:44 +0800
Subject: [PATCH 2/2] libavcodec/amfenc: reconfig when bitrate change
From 6e76c57cf2c0e790228f19c88089eef110fd74aa Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Thu, 5 Sep 2024 16:32:16 +0800
Subject: [PATCH 2/3] libavcodec/amfenc: reconfig when bitrate change
Signed-off-by: 21pages <pages21@163.com>
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/amfenc.c | 20 ++++++++++++++++++++
libavcodec/amfenc.h | 1 +
2 files changed, 21 insertions(+)
diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c
index a033e1220e..3eab01a903 100644
index 061859f85c..97587fe66b 100644
--- a/libavcodec/amfenc.c
+++ b/libavcodec/amfenc.c
@@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx)
@@ -21,7 +21,7 @@ index a033e1220e..3eab01a903 100644
// configure AMF logger
// the return of these functions indicates old state and do not affect behaviour
@@ -575,6 +576,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe
@@ -583,6 +584,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe
frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer);
}
@@ -45,9 +45,9 @@ index a033e1220e..3eab01a903 100644
int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
{
AmfContext *ctx = avctx->priv_data;
@@ -586,6 +604,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
AVFrame *frame = ctx->delayed_frame;
int block_and_wait;
@@ -596,6 +614,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
int query_output_data_flag = 0;
AMF_RESULT res_resubmit;
+ reconfig_encoder(avctx);
+
@@ -55,13 +55,13 @@ index a033e1220e..3eab01a903 100644
return AVERROR(EINVAL);
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index e92120ea39..31172645f2 100644
index d636673a9d..09506ee2e0 100644
--- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h
@@ -107,6 +107,7 @@ typedef struct AmfContext {
int me_half_pel;
int me_quarter_pel;
int aud;
@@ -113,6 +113,7 @@ typedef struct AmfContext {
int max_b_frames;
int qvbr_quality_level;
int hw_high_motion_quality_boost;
+ int64_t av_bitrate;
// HEVC - specific options

View File

@@ -1,32 +1,32 @@
From 8fd62e4ecd058b09abf8847be5fbbf0eef44a90f Mon Sep 17 00:00:00 2001
From 14b77216106eaaff9cf701528039ae4264eaf420 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Tue, 16 Jul 2024 14:58:33 +0800
Subject: [PATCH] amf colorspace
Date: Thu, 5 Sep 2024 16:41:59 +0800
Subject: [PATCH 3/3] amf colorspace
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/amfenc.h | 1 +
libavcodec/amfenc_h264.c | 39 +++++++++++++++++++++++++++++++++
libavcodec/amfenc_h264.c | 40 ++++++++++++++++++++++++++++++++++
libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+)
3 files changed, 88 insertions(+)
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index 31172645f2..493e01603d 100644
index 09506ee2e0..7f458b14f7 100644
--- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h
@@ -23,6 +23,7 @@
@@ -24,6 +24,7 @@
#include <AMF/components/VideoEncoderVCE.h>
#include <AMF/components/VideoEncoderHEVC.h>
#include <AMF/components/VideoEncoderAV1.h>
+#include <AMF/components/ColorSpace.h>
#include "libavutil/fifo.h"
diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c
index f55dbc80f0..5a6b6e164f 100644
index 415828f005..7da5a96c71 100644
--- a/libavcodec/amfenc_h264.c
+++ b/libavcodec/amfenc_h264.c
@@ -139,6 +139,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx)
@@ -200,6 +200,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx)
AMFRate framerate;
AMFSize framesize = AMFConstructSize(avctx->width, avctx->height);
int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0;
@@ -36,7 +36,7 @@ index f55dbc80f0..5a6b6e164f 100644
if (avctx->framerate.num > 0 && avctx->framerate.den > 0) {
framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den);
@@ -199,11 +202,47 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx)
@@ -266,10 +269,47 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_ASPECT_RATIO, ratio);
}
@@ -70,25 +70,25 @@ index f55dbc80f0..5a6b6e164f 100644
+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;
+ break;
+ }
+ }
}
+ pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt;
+ color_depth = AMF_COLOR_BIT_DEPTH_8;
+ if (pix_fmt == AV_PIX_FMT_P010) {
+ color_depth = AMF_COLOR_BIT_DEPTH_10;
}
+ }
+
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, color_depth);
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, color_profile);
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc);
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries);
// autodetect rate control method
if (ctx->rate_control_mode == AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_UNKNOWN) {
if (ctx->qp_i != -1 || ctx->qp_p != -1 || ctx->qp_b != -1) {
diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c
index 7a40bcad31..0260f43c81 100644
index 65259d7153..7c930d3ccc 100644
--- a/libavcodec/amfenc_hevc.c
+++ b/libavcodec/amfenc_hevc.c
@@ -106,6 +106,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx)
@@ -161,6 +161,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx)
AMFRate framerate;
AMFSize framesize = AMFConstructSize(avctx->width, avctx->height);
int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0;
@@ -98,17 +98,17 @@ index 7a40bcad31..0260f43c81 100644
if (avctx->framerate.num > 0 && avctx->framerate.den > 0) {
framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den);
@@ -130,6 +133,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx)
case FF_PROFILE_HEVC_MAIN:
@@ -191,6 +194,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
case AV_PROFILE_HEVC_MAIN:
profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN;
break;
+ case FF_PROFILE_HEVC_MAIN_10:
+ case AV_PROFILE_HEVC_MAIN_10:
+ profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10;
+ break;
default:
break;
}
@@ -158,6 +164,47 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx)
@@ -219,6 +225,47 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_ASPECT_RATIO, ratio);
}

View File

@@ -1,16 +1,8 @@
if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX)
set(FF_VERSION "n5.1.5")
set(FF_SHA512 "a933f18e53207ccc277b42c9a68db00f31cefec555e6d5d7c57db3409023b2c38fd93ebe2ccfcd17ba2397adb912e93f2388241ca970b7d8bd005ccfe86d5679")
else()
set(FF_VERSION "n7.0.1")
set(FF_SHA512 "1212ebcb78fdaa103b0304373d374e41bf1fe680e1fa4ce0f60624857491c26b4dda004c490c3ef32d4a0e10f42ae6b54546f9f318e2dcfbaa116117f687bc88")
endif()
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO ffmpeg/ffmpeg
REF "${FF_VERSION}"
SHA512 "${FF_SHA512}"
REF "n${VERSION}"
SHA512 3ba02e8b979c80bf61d55f414bdac2c756578bb36498ed7486151755c6ccf8bd8ff2b8c7afa3c5d1acd862ce48314886a86a105613c05e36601984c334f8f6bf
HEAD_REF master
PATCHES
0002-fix-msvc-link.patch # upstreamed in future version
@@ -18,25 +10,11 @@ vcpkg_from_github(
0005-fix-nasm.patch # upstreamed in future version
0012-Fix-ssl-110-detection.patch
0013-define-WINVER.patch
patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch
patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch
patch/0003-amf-colorspace.patch
)
if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX)
vcpkg_apply_patches(
SOURCE_PATH ${SOURCE_PATH}
PATCHES
${CMAKE_CURRENT_LIST_DIR}/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch
${CMAKE_CURRENT_LIST_DIR}/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch
${CMAKE_CURRENT_LIST_DIR}/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch
${CMAKE_CURRENT_LIST_DIR}/5.1/0004-amf-colorspace.patch
)
elseif(VCPKG_TARGET_IS_ANDROID)
vcpkg_apply_patches(
SOURCE_PATH ${SOURCE_PATH}
PATCHES
${CMAKE_CURRENT_LIST_DIR}/7.0/0001-android-mediacodec-encode-align-64.patch
)
endif()
if(SOURCE_PATH MATCHES " ")
message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces")
endif()
@@ -130,6 +108,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
string(APPEND OPTIONS "\
--target-os=win32 \
--toolchain=msvc \
--cc=cl \
--enable-gpl \
--enable-d3d11va \
--enable-cuda \
@@ -210,6 +189,10 @@ endif()
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"")
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"")
if(VCPKG_TARGET_IS_WINDOWS)
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"")
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"")
endif()
# # Setup vcpkg toolchain
set(prog_env "")
@@ -219,8 +202,9 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER)
get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME)
set(ENV{CC} "${CC_filename}")
string(APPEND OPTIONS " --cc=${CC_filename}")
# string(APPEND OPTIONS " --host_cc=${CC_filename}") ffmpeg not yet setup for cross builds?
if(VCPKG_HOST_IS_WINDOWS)
string(APPEND OPTIONS " --host_cc=${CC_filename}")
endif()
list(APPEND prog_env "${CC_path}")
endif()
@@ -291,6 +275,13 @@ if(VCPKG_DETECTED_CMAKE_STRIP)
list(APPEND prog_env "${STRIP_path}")
endif()
if(VCPKG_HOST_IS_WINDOWS)
vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16)
set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe")
list(APPEND prog_env "${MSYS_ROOT}/usr/bin" "${MSYS_ROOT}/usr/share/automake-1.16")
else()
# find_program(SHELL bash)
endif()
list(REMOVE_DUPLICATES prog_env)
vcpkg_add_to_path(PREPEND ${prog_env})

View File

@@ -1,6 +1,6 @@
{
"name": "ffmpeg",
"version": "7.0.1",
"version": "7.0.2",
"port-version": 0,
"description": [
"a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.",

View File

@@ -84,7 +84,7 @@ pub mod io_loop;
pub const MILLI1: Duration = Duration::from_millis(1);
pub const SEC30: Duration = Duration::from_secs(30);
pub const VIDEO_QUEUE_SIZE: usize = 120;
const MAX_DECODE_FAIL_COUNTER: usize = 10; // Currently, failed decode cause refresh_video, so make it small
const MAX_DECODE_FAIL_COUNTER: usize = 3;
#[cfg(target_os = "linux")]
pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited";
@@ -1151,6 +1151,7 @@ pub struct VideoHandler {
record: bool,
_display: usize, // useful for debug
fail_counter: usize,
first_frame: bool,
}
impl VideoHandler {
@@ -1176,6 +1177,7 @@ impl VideoHandler {
record: false,
_display,
fail_counter: 0,
first_frame: true,
}
}
@@ -1204,9 +1206,19 @@ impl VideoHandler {
self.fail_counter = 0;
} else {
if self.fail_counter < usize::MAX {
self.fail_counter += 1
if self.first_frame && self.fail_counter < MAX_DECODE_FAIL_COUNTER {
log::error!("decode first frame failed");
self.fail_counter = MAX_DECODE_FAIL_COUNTER;
} else {
self.fail_counter += 1;
}
log::error!(
"Failed to handle video frame, fail counter: {}",
self.fail_counter
);
}
}
self.first_frame = false;
if self.record {
self.recorder
.lock()
@@ -1222,12 +1234,17 @@ impl VideoHandler {
/// Reset the decoder, change format if it is Some
pub fn reset(&mut self, format: Option<CodecFormat>) {
log::info!(
"reset video handler for display #{}, format: {format:?}",
self._display
);
#[cfg(target_os = "macos")]
self.rgb.set_align(crate::get_dst_align_rgba());
let luid = Self::get_adapter_luid();
let format = format.unwrap_or(self.decoder.format());
self.decoder = Decoder::new(format, luid);
self.fail_counter = 0;
self.first_frame = true;
}
/// Start or stop screen record.
@@ -1783,28 +1800,6 @@ impl LoginConfigHandler {
)
}
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
if self.conn_type.eq(&ConnType::FILE_TRANSFER)
|| self.conn_type.eq(&ConnType::PORT_FORWARD)
|| self.conn_type.eq(&ConnType::RDP)
{
return None;
}
let mut n = 0;
let mut msg = OptionMessage::new();
if self.version < hbb_common::get_version_number("1.2.4") {
if self.get_toggle_option("privacy-mode") {
msg.privacy_mode = BoolOption::Yes.into();
n += 1;
}
}
if n > 0 {
Some(msg)
} else {
None
}
}
/// Parse the image quality option.
/// Return [`ImageQuality`] if the option is valid, otherwise return `None`.
///
@@ -3407,3 +3402,135 @@ async fn hc_connection_(
}
Ok(())
}
pub mod peer_online {
use hbb_common::{
anyhow::bail,
config::{Config, CONNECT_TIMEOUT, READ_TIMEOUT},
log,
rendezvous_proto::*,
sleep,
socket_client::connect_tcp,
tcp::FramedStream,
ResultType,
};
pub async fn query_online_states<F: FnOnce(Vec<String>, Vec<String>)>(ids: Vec<String>, f: F) {
let test = false;
if test {
sleep(1.5).await;
let mut onlines = ids;
let offlines = onlines.drain((onlines.len() / 2)..).collect();
f(onlines, offlines)
} else {
let query_timeout = std::time::Duration::from_millis(3_000);
match query_online_states_(&ids, query_timeout).await {
Ok((onlines, offlines)) => {
f(onlines, offlines);
}
Err(e) => {
log::debug!("query onlines, {}", &e);
}
}
}
}
async fn create_online_stream() -> ResultType<FramedStream> {
let (rendezvous_server, _servers, _contained) =
crate::get_rendezvous_server(READ_TIMEOUT).await;
let tmp: Vec<&str> = rendezvous_server.split(":").collect();
if tmp.len() != 2 {
bail!("Invalid server address: {}", rendezvous_server);
}
let port: u16 = tmp[1].parse()?;
if port == 0 {
bail!("Invalid server address: {}", rendezvous_server);
}
let online_server = format!("{}:{}", tmp[0], port - 1);
connect_tcp(online_server, CONNECT_TIMEOUT).await
}
async fn query_online_states_(
ids: &Vec<String>,
timeout: std::time::Duration,
) -> ResultType<(Vec<String>, Vec<String>)> {
let mut msg_out = RendezvousMessage::new();
msg_out.set_online_request(OnlineRequest {
id: Config::get_id(),
peers: ids.clone(),
..Default::default()
});
let mut socket = match create_online_stream().await {
Ok(s) => s,
Err(e) => {
log::debug!("Failed to create peers online stream, {e}");
return Ok((vec![], ids.clone()));
}
};
// TODO: Use long connections to avoid socket creation
// If we use a Arc<Mutex<Option<FramedStream>>> to hold and reuse the previous socket,
// we may face the following error:
// An established connection was aborted by the software in your host machine. (os error 10053)
if let Err(e) = socket.send(&msg_out).await {
log::debug!("Failed to send peers online states query, {e}");
return Ok((vec![], ids.clone()));
}
// Retry for 2 times to get the online response
for _ in 0..2 {
if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg(
&mut socket,
Some(timeout.as_millis() as _),
)
.await
{
match msg_in.union {
Some(rendezvous_message::Union::OnlineResponse(online_response)) => {
let states = online_response.states;
let mut onlines = Vec::new();
let mut offlines = Vec::new();
for i in 0..ids.len() {
// bytes index from left to right
let bit_value = 0x01 << (7 - i % 8);
if (states[i / 8] & bit_value) == bit_value {
onlines.push(ids[i].clone());
} else {
offlines.push(ids[i].clone());
}
}
return Ok((onlines, offlines));
}
_ => {
// ignore
}
}
} else {
// TODO: Make sure socket closed?
bail!("Online stream receives None");
}
}
bail!("Failed to query online states, no online response");
}
#[cfg(test)]
mod tests {
use hbb_common::tokio;
#[tokio::test]
async fn test_query_onlines() {
super::query_online_states(
vec![
"152183996".to_owned(),
"165782066".to_owned(),
"155323351".to_owned(),
"460952777".to_owned(),
],
|onlines: Vec<String>, offlines: Vec<String>| {
println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines);
},
)
.await;
}
}
}

View File

@@ -26,7 +26,7 @@ use crossbeam_queue::ArrayQueue;
use hbb_common::tokio::sync::mpsc::error::TryRecvError;
use hbb_common::{
allow_err,
config::{PeerConfig, TransferSerde},
config::{self, PeerConfig, TransferSerde},
fs::{
self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm,
DigestCheckResult, RemoveJobMeta,
@@ -353,6 +353,7 @@ impl<T: InvokeUiSession> Remote<T> {
} else {
if let Err(e) = ContextSend::make_sure_enabled() {
log::error!("failed to restart clipboard context: {}", e);
// to-do: Show msgbox with "Don't show again" option
};
log::debug!("Send system clipboard message to remote");
let msg = crate::clipboard_file::clip_2_msg(clip);
@@ -957,22 +958,6 @@ impl<T: InvokeUiSession> Remote<T> {
true
}
async fn send_opts_after_login(&self, peer: &mut Stream) {
if let Some(opts) = self
.handler
.lc
.read()
.unwrap()
.get_option_message_after_login()
{
let mut misc = Misc::new();
misc.set_option(opts);
let mut msg_out = Message::new();
msg_out.set_misc(misc);
allow_err!(peer.send(&msg_out).await);
}
}
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
if !self.peer_info.is_support_virtual_display() {
return;
@@ -1134,7 +1119,6 @@ impl<T: InvokeUiSession> Remote<T> {
self.first_frame = true;
self.handler.close_success();
self.handler.adapt_size();
self.send_opts_after_login(peer).await;
self.send_toggle_virtual_display_msg(peer).await;
self.send_toggle_privacy_mode_msg(peer).await;
}
@@ -1212,18 +1196,20 @@ impl<T: InvokeUiSession> Remote<T> {
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg(
&peer_version,
&peer_platform,
crate::clipboard::ClipboardSide::Client,
) {
let sender = self.sender.clone();
let permission_config = self.handler.get_permission_config();
tokio::spawn(async move {
if permission_config.is_text_clipboard_required() {
sender.send(Data::Message(msg_out)).ok();
}
});
if self.handler.lc.read().unwrap().sync_init_clipboard.v {
if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg(
&peer_version,
&peer_platform,
crate::clipboard::ClipboardSide::Client,
) {
let sender = self.sender.clone();
let permission_config = self.handler.get_permission_config();
tokio::spawn(async move {
if permission_config.is_text_clipboard_required() {
sender.send(Data::Message(msg_out)).ok();
}
});
}
}
// on connection established client
@@ -1634,7 +1620,7 @@ impl<T: InvokeUiSession> Remote<T> {
},
Some(message::Union::MessageBox(msgbox)) => {
let mut link = msgbox.link;
if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) {
if let Some(v) = config::HELPER_URL.get(&link as &str) {
link = v.to_string();
} else {
log::warn!("Message box ignore link {} for security", &link);
@@ -1906,7 +1892,7 @@ impl<T: InvokeUiSession> Remote<T> {
return;
};
let is_stopping_allowed = clip.is_stopping_allowed_from_peer();
let is_stopping_allowed = clip.is_beginning_message();
let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v;
let stop = is_stopping_allowed && !file_transfer_enabled;
log::debug!(

View File

@@ -1,9 +1,10 @@
use arboard::{ClipboardData, ClipboardFormat};
use clipboard_master::{ClipboardHandler, Master, Shutdown};
use hbb_common::{log, message_proto::*, ResultType};
use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{
sync::{mpsc::Sender, Arc, Mutex},
thread::JoinHandle,
time::Duration,
};
pub const CLIPBOARD_NAME: &'static str = "clipboard";
@@ -12,6 +13,9 @@ pub const CLIPBOARD_INTERVAL: u64 = 333;
// This format is used to store the flag in the clipboard.
const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner";
// Add special format for Excel XML Spreadsheet
const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet";
lazy_static::lazy_static! {
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
// cache the clipboard msg
@@ -23,6 +27,9 @@ lazy_static::lazy_static! {
static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = Arc::new(Mutex::new(None));
}
const CLIPBOARD_GET_MAX_RETRY: usize = 3;
const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33);
const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
ClipboardFormat::Text,
ClipboardFormat::Html,
@@ -30,6 +37,7 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
ClipboardFormat::ImageRgba,
ClipboardFormat::ImagePng,
ClipboardFormat::ImageSvg,
ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET),
ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT),
];
@@ -147,14 +155,18 @@ pub fn check_clipboard(
*ctx = ClipboardContext::new().ok();
}
let ctx2 = ctx.as_mut()?;
let content = ctx2.get(side, force);
if let Ok(content) = content {
if !content.is_empty() {
let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone());
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
return Some(msg);
match ctx2.get(side, force) {
Ok(content) => {
if !content.is_empty() {
let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone());
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
return Some(msg);
}
}
Err(e) => {
log::error!("Failed to get clipboard content. {}", e);
}
}
None
@@ -259,16 +271,49 @@ impl ClipboardContext {
Ok(ClipboardContext { inner: board })
}
fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType<Vec<ClipboardData>> {
// If there're multiple threads or processes trying to access the clipboard at the same time,
// the previous clipboard owner will fail to access the clipboard.
// `GetLastError()` will return `ERROR_CLIPBOARD_NOT_OPEN` (OSError(1418): Thread does not have a clipboard open) at this time.
// See https://github.com/rustdesk-org/arboard/blob/747ab2d9b40a5c9c5102051cf3b0bb38b4845e60/src/platform/windows.rs#L34
//
// This is a common case on Windows, so we retry here.
// Related issues:
// https://github.com/rustdesk/rustdesk/issues/9263
// https://github.com/rustdesk/rustdesk/issues/9222#issuecomment-2329233175
for i in 0..CLIPBOARD_GET_MAX_RETRY {
match self.inner.get_formats(SUPPORTED_FORMATS) {
Ok(data) => {
return Ok(data
.into_iter()
.filter(|c| !matches!(c, arboard::ClipboardData::None))
.collect())
}
Err(e) => match e {
arboard::Error::ClipboardOccupied => {
log::debug!("Failed to get clipboard formats, clipboard is occupied, retrying... {}", i + 1);
std::thread::sleep(CLIPBOARD_GET_RETRY_INTERVAL_DUR);
}
_ => {
log::error!("Failed to get clipboard formats, {}", e);
return Err(e.into());
}
},
}
}
bail!("Failed to get clipboard formats, clipboard is occupied, {CLIPBOARD_GET_MAX_RETRY} retries failed");
}
pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType<Vec<ClipboardData>> {
let _lock = ARBOARD_MTX.lock().unwrap();
let data = self.inner.get_formats(SUPPORTED_FORMATS)?;
let data = self.get_formats(SUPPORTED_FORMATS)?;
if data.is_empty() {
return Ok(data);
}
if !force {
for c in data.iter() {
if let ClipboardData::Special((_, d)) = c {
if side.is_owner(d) {
if let ClipboardData::Special((s, d)) = c {
if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) {
return Ok(vec![]);
}
}
@@ -276,7 +321,10 @@ impl ClipboardContext {
}
Ok(data
.into_iter()
.filter(|c| !matches!(c, ClipboardData::Special(_)))
.filter(|c| match c {
ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT,
_ => true,
})
.collect())
}
@@ -454,12 +502,30 @@ mod proto {
}
}
fn special_to_proto(d: Vec<u8>, s: String) -> Clipboard {
let compressed = compress_func(&d);
let compress = compressed.len() < d.len();
let content = if compress {
compressed
} else {
s.bytes().collect::<Vec<u8>>()
};
Clipboard {
compress,
content: content.into(),
format: ClipboardFormat::Special.into(),
special_name: s,
..Default::default()
}
}
fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> {
let d = match data {
ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text),
ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf),
ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html),
ClipboardData::Image(a) => image_to_proto(a),
ClipboardData::Special((s, d)) => special_to_proto(d, s),
_ => return None,
};
Some(d)
@@ -496,6 +562,9 @@ mod proto {
Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg(
std::str::from_utf8(&data).unwrap_or_default(),
))),
Ok(ClipboardFormat::Special) => {
Some(ClipboardData::Special((clipboard.special_name, data)))
}
_ => None,
}
}

View File

@@ -84,6 +84,7 @@ lazy_static::lazy_static! {
// Is server logic running. The server code can invoked to run by the main process if --server is not running.
static ref SERVER_RUNNING: Arc<RwLock<bool>> = Default::default();
static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--"));
static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned()) || std::env::args().nth(1) == Some("--cm-no-ui".to_owned());
}
pub struct SimpleCallOnReturn {
@@ -137,6 +138,11 @@ pub fn is_main() -> bool {
*IS_MAIN
}
#[inline]
pub fn is_cm() -> bool {
*IS_CM
}
// Is server logic running.
#[inline]
pub fn is_server_running() -> bool {
@@ -1644,3 +1650,13 @@ mod tests {
);
}
}
#[inline]
pub fn get_builtin_option(key: &str) -> String {
config::BUILTIN_SETTINGS
.read()
.unwrap()
.get(key)
.cloned()
.unwrap_or_default()
}

View File

@@ -39,6 +39,7 @@ pub fn core_main() -> Option<Vec<String>> {
let mut _is_run_as_system = false;
let mut _is_quick_support = false;
let mut _is_flutter_invoke_new_connection = false;
let mut no_server = false;
let mut arg_exe = Default::default();
for arg in std::env::args() {
if i == 0 {
@@ -62,6 +63,8 @@ pub fn core_main() -> Option<Vec<String>> {
_is_run_as_system = true;
} else if arg == "--quick_support" {
_is_quick_support = true;
} else if arg == "--no-server" {
no_server = true;
} else {
args.push(arg);
}
@@ -134,6 +137,7 @@ pub fn core_main() -> Option<Vec<String>> {
}
}
hbb_common::init_log(false, &log_name);
log::info!("main start args: {:?}, env: {:?}", args, std::env::args());
// linux uni (url) go here.
#[cfg(all(target_os = "linux", feature = "flutter"))]
@@ -161,9 +165,8 @@ pub fn core_main() -> Option<Vec<String>> {
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
init_plugins(&args);
log::info!("main start args:{:?}", args);
if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) {
std::thread::spawn(move || crate::start_server(false));
std::thread::spawn(move || crate::start_server(false, no_server));
} else {
#[cfg(windows)]
{
@@ -279,11 +282,11 @@ pub fn core_main() -> Option<Vec<String>> {
crate::privacy_mode::restore_reg_connectivity(true);
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
crate::start_server(true);
crate::start_server(true, false);
}
#[cfg(target_os = "macos")]
{
let handler = std::thread::spawn(move || crate::start_server(true));
let handler = std::thread::spawn(move || crate::start_server(true, false));
crate::tray::start_tray();
// prevent server exit when encountering errors from tray
hbb_common::allow_err!(handler.join());
@@ -473,8 +476,18 @@ pub fn core_main() -> Option<Vec<String>> {
crate::ui_interface::start_option_status_sync();
} else if args[0] == "--cm-no-ui" {
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))]
crate::flutter::connection_manager::start_cm_no_ui();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
crate::ui_interface::start_option_status_sync();
crate::flutter::connection_manager::start_cm_no_ui();
}
return None;
} else if args[0] == "-gtk-sudo" {
// rustdesk service kill `rustdesk --` processes
#[cfg(target_os = "linux")]
if args.len() > 2 {
crate::platform::gtk_sudo::exec();
}
return None;
} else {
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]

View File

@@ -802,13 +802,13 @@ impl InvokeUiSession for FlutterHandler {
fn set_peer_info(&self, pi: &PeerInfo) {
let displays = Self::make_displays_msg(&pi.displays);
let mut features: HashMap<&str, i32> = Default::default();
let mut features: HashMap<&str, bool> = Default::default();
for ref f in pi.features.iter() {
features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 });
features.insert("privacy_mode", f.privacy_mode);
}
// compatible with 1.1.9
if get_version_number(&pi.version) < get_version_number("1.2.0") {
features.insert("privacy_mode", 0);
features.insert("privacy_mode", false);
}
let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned());
let resolutions = serialize_resolutions(&pi.resolutions.resolutions);
@@ -2057,18 +2057,18 @@ pub mod sessions {
pub(super) mod async_tasks {
use hbb_common::{
bail,
tokio::{
self, select,
sync::mpsc::{unbounded_channel, UnboundedSender},
},
tokio::{self, select},
ResultType,
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
sync::{
mpsc::{sync_channel, SyncSender},
Arc, Mutex,
},
};
type TxQueryOnlines = UnboundedSender<Vec<String>>;
type TxQueryOnlines = SyncSender<Vec<String>>;
lazy_static::lazy_static! {
static ref TX_QUERY_ONLINES: Arc<Mutex<Option<TxQueryOnlines>>> = Default::default();
}
@@ -2085,21 +2085,18 @@ pub(super) mod async_tasks {
#[tokio::main(flavor = "current_thread")]
async fn start_flutter_async_runner_() {
let (tx_onlines, mut rx_onlines) = unbounded_channel::<Vec<String>>();
// Only one task is allowed to run at the same time.
let (tx_onlines, rx_onlines) = sync_channel::<Vec<String>>(1);
TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines);
loop {
select! {
ids = rx_onlines.recv() => {
match ids {
Some(_ids) => {
#[cfg(not(any(target_os = "ios")))]
crate::rendezvous_mediator::query_online_states(_ids, handle_query_onlines).await
}
None => {
break;
}
}
match rx_onlines.recv() {
Ok(ids) => {
crate::client::peer_online::query_online_states(ids, handle_query_onlines).await
}
_ => {
// unreachable!
break;
}
}
}
@@ -2107,7 +2104,8 @@ pub(super) mod async_tasks {
pub fn query_onlines(ids: Vec<String>) -> ResultType<()> {
if let Some(tx) = TX_QUERY_ONLINES.lock().unwrap().as_ref() {
let _ = tx.send(ids)?;
// Ignore if the channel is full.
let _ = tx.try_send(ids)?;
} else {
bail!("No tx_query_onlines");
}

View File

@@ -1,6 +1,6 @@
use crate::{
client::file_trait::FileManager,
common::{is_keyboard_mode_supported, make_fd_to_json},
common::make_fd_to_json,
flutter::{
self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option,
},
@@ -19,13 +19,11 @@ use hbb_common::allow_err;
use hbb_common::{
config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
fs, lazy_static, log,
message_proto::KeyboardMode,
rendezvous_proto::ConnType,
ResultType,
};
use std::{
collections::HashMap,
str::FromStr,
sync::{
atomic::{AtomicI32, Ordering},
Arc,
@@ -447,15 +445,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32
pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn<bool> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
if let Ok(mode) = KeyboardMode::from_str(&mode[..]) {
SyncReturn(is_keyboard_mode_supported(
&mode,
session.get_peer_version(),
&session.peer_platform(),
))
} else {
SyncReturn(false)
}
SyncReturn(session.is_keyboard_mode_supported(mode))
} else {
SyncReturn(false)
}
@@ -490,6 +480,25 @@ pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Ve
}
pub fn session_handle_flutter_key_event(
session_id: SessionID,
character: String,
usb_hid: i32,
lock_modes: i32,
down_or_up: bool,
) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_key_event(
&keyboard_mode,
&character,
usb_hid,
lock_modes,
down_or_up,
);
}
}
pub fn session_handle_flutter_raw_key_event(
session_id: SessionID,
name: String,
platform_code: i32,
@@ -499,7 +508,7 @@ pub fn session_handle_flutter_key_event(
) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_key_event(
session.handle_flutter_raw_key_event(
&keyboard_mode,
&name,
platform_code,
@@ -2273,6 +2282,10 @@ pub fn main_clear_trusted_devices() {
clear_trusted_devices()
}
pub fn main_max_encrypt_len() -> SyncReturn<usize> {
SyncReturn(max_encrypt_len())
}
pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.request_init_msgs(display);

View File

@@ -403,7 +403,8 @@ async fn handle(data: Data, stream: &mut Connection) {
{
hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0)
.await;
crate::run_me::<&str>(vec![]).ok();
// https://github.com/rustdesk/rustdesk/discussions/9254
crate::run_me::<&str>(vec!["--no-server"]).ok();
}
#[cfg(target_os = "macos")]
{
@@ -928,16 +929,23 @@ pub fn set_permanent_password(v: String) -> ResultType<()> {
pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> {
let v = v.trim().to_owned();
let min_len = 4;
if !v.is_empty() && v.len() < min_len {
let err = if translate {
crate::lang::translate(
"Requires at least {".to_string() + &format!("{min_len}") + "} characters",
)
} else {
// Sometimes, translated can't show normally in command line
format!("Requires at least {} characters", min_len)
};
bail!(err);
let max_len = crate::ui_interface::max_encrypt_len();
let len = v.chars().count();
if !v.is_empty() {
if len < min_len {
let err = if translate {
crate::lang::translate(
"Requires at least {".to_string() + &format!("{min_len}") + "} characters",
)
} else {
// Sometimes, translated can't show normally in command line
format!("Requires at least {} characters", min_len)
};
bail!(err);
}
if len > max_len {
bail!("No more than {max_len} characters");
}
}
Config::set_unlock_pin(&v);
set_config("unlock-pin", v)

View File

@@ -34,6 +34,7 @@ const OS_LOWER_ANDROID: &str = "android";
#[cfg(any(target_os = "windows", target_os = "macos"))]
static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false);
@@ -71,6 +72,7 @@ pub mod client {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn change_grab_status(state: GrabState, keyboard_mode: &str) {
#[cfg(feature = "flutter")]
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
return;
}

View File

@@ -1,4 +1,3 @@
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::config::Config;
use hbb_common::{
allow_err,
@@ -22,7 +21,7 @@ use std::{
type Message = RendezvousMessage;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
pub(super) fn start_listening() -> ResultType<()> {
let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port()));
let socket = std::net::UdpSocket::bind(addr)?;
@@ -40,13 +39,22 @@ pub(super) fn start_listening() -> ResultType<()> {
&Config::get_option("enable-lan-discovery"),
)
{
let id = Config::get_id();
if p.id == id {
continue;
}
if let Some(self_addr) = get_ipaddr_by_peer(&addr) {
let mut msg_out = Message::new();
let mut hostname = whoami::hostname();
// The default hostname is "localhost" which is a bit confusing
if hostname == "localhost" {
hostname = "unknown".to_owned();
}
let peer = PeerDiscovery {
cmd: "pong".to_owned(),
mac: get_mac(&self_addr),
id: Config::get_id(),
hostname: whoami::hostname(),
id,
hostname,
username: crate::platform::get_active_username(),
platform: whoami::platform().to_string(),
..Default::default()
@@ -100,17 +108,17 @@ fn get_broadcast_port() -> u16 {
}
fn get_mac(_ip: &IpAddr) -> String {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
if let Ok(mac) = get_mac_by_ip(_ip) {
mac.to_string()
} else {
"".to_owned()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
#[cfg(target_os = "ios")]
"".to_owned()
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(target_os = "ios"))]
fn get_mac_by_ip(ip: &IpAddr) -> ResultType<String> {
for interface in default_net::get_interfaces() {
match ip {
@@ -153,6 +161,10 @@ fn get_ipaddr_by_peer<A: ToSocketAddrs>(peer: A) -> Option<IpAddr> {
fn create_broadcast_sockets() -> Vec<UdpSocket> {
let mut ipv4s = Vec::new();
// TODO: maybe we should use a better way to get ipv4 addresses.
// But currently, it's ok to use `[Ipv4Addr::UNSPECIFIED]` for discovery.
// `default_net::get_interfaces()` causes undefined symbols error when `flutter build` on iOS simulator x86_64
#[cfg(not(any(target_os = "ios")))]
for interface in default_net::get_interfaces() {
for ipv4 in &interface.ipv4 {
ipv4s.push(ipv4.addr.clone());
@@ -178,8 +190,20 @@ fn send_query() -> ResultType<Vec<UdpSocket>> {
}
let mut msg_out = Message::new();
// We may not be able to get the mac address on mobile platforms.
// So we need to use the id to avoid discovering ourselves.
#[cfg(any(target_os = "android", target_os = "ios"))]
let id = crate::ui_interface::get_id();
// `crate::ui_interface::get_id()` will cause error:
// `get_id()` uses async code with `current_thread`, which is not allowed in this context.
//
// No need to get id for desktop platforms.
// We can use the mac address to identify the device.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let id = "".to_owned();
let peer = PeerDiscovery {
cmd: "ping".to_owned(),
id,
..Default::default()
};
msg_out.set_peer_discovery(peer);

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -1,19 +1,19 @@
lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Статус"),
("Your Desktop", "Твоят Работен Плот"),
("desk_tip", "Вашият работен плот може да бъде достъпен с този идентификационен код и парола."),
("Status", "Положение"),
("Your Desktop", "Вашата работна среда"),
("desk_tip", "Вашата работна среда не може да бъде достъпена с този потребителски код и парола."),
("Password", "Парола"),
("Ready", "Готово"),
("Established", "Установен"),
("connecting_status", "Свързване с RustDesk мрежата..."),
("Enable service", "Пусни услуга"),
("Enable service", "Разреши услуга"),
("Start service", "Стартирай услуга"),
("Service is running", "Услугата работи"),
("Service is not running", "Услугата не работи"),
("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"),
("Control Remote Desktop", "Контролирайте отдалечения работен плот"),
("Control Remote Desktop", "Отдалечено управление на работна среда"),
("Transfer file", "Прехвърляне на файл"),
("Connect", "Свързване"),
("Recent sessions", "Последни сесии"),
@@ -23,27 +23,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("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/Релейн сървър"),
("Import server config", "Експортиране конфигурацията на сървъра"),
("Export Server Config", "Експортиране на конфигурация на сървъра"),
("Import server configuration successfully", "Импортирането конфигурацията на сървъра успешно"),
("Export server configuration successfully", "Експортирането конфигурацията на сървъра успешно"),
("Invalid server configuration", "Невалидна конфигурация на сървъра"),
("Enable keyboard/mouse", "Позволяване на клавиатура/мишка"),
("Enable clipboard", "Позволяване достъп до клипборда"),
("Enable file transfer", "Позволяване прехвърляне на файлове"),
("Enable TCP tunneling", "Позволяване на TCP тунели"),
("IP Whitelisting", "Определяне на позволени IP по списък"),
("ID/Relay Server", "ID/Препредаващ сървър"),
("Import server config", "Внасяне сървър настройки за "),
("Export Server Config", "Изнасяне настройки на сървър"),
("Import server configuration successfully", "Успешно внасяне на сървърни настройки"),
("Export server configuration successfully", "Успешно изнасяне на сървърни настройки"),
("Invalid server configuration", "Недопустими сървърни настройки"),
("Clipboard is empty", "Клипбордът е празен"),
("Stop service", "Спрете услугата"),
("Change ID", "Промяна на ID"),
("Your new ID", "Вашето ново ID"),
("Stop service", "Спираане на услуга"),
("Change ID", "Промяна определител (ID)"),
("Your new ID", "Вашият нов определител (ID)"),
("length %min% to %max%", "дължина %min% до %max%"),
("starts with a letter", "започва с буква"),
("allowed characters", "разрешени знаци"),
("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) символи са позволени. Първата буква трябва да е a-z, A-Z. С дължина мержу 6 и 16."),
("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) са сред позволени. Първа буква следва да е a-z, A-Z. С дължина мержу 6 и 16."),
("Website", "Уебсайт"),
("About", "Относно програмата"),
("About", "Относно"),
("Slogan_tip", "Направено от сърце в този хаотичен свят!"),
("Privacy Statement", "Декларация за поверителност"),
("Mute", "Без звук"),
@@ -53,23 +53,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input", "Аудио вход"),
("Enhancements", "Подобрения"),
("Hardware Codec", "Хардуерен кодек"),
("Adaptive bitrate", "Адаптивен битрейт"),
("Adaptive bitrate", "Приспособяваще се скорост на предаване наданни"),
("ID Server", "ID сървър"),
("Relay Server", "Релейн сървър"),
("Relay Server", "Препращащ сървър"),
("API Server", "API сървър"),
("invalid_http", "трябва да започва с http:// или https://"),
("Invalid IP", "Невалиден IP"),
("Invalid format", "Невалиден формат"),
("Invalid IP", "Недопустим IP"),
("Invalid format", "Недопустим формат"),
("server_not_support", "Все още не се поддържа от сървъра"),
("Not available", "Не е наличен"),
("Too frequent", "Твърде често"),
("Cancel", "Отказ"),
("Skip", "Пропускане"),
("Close", "Затвори"),
("Retry", "Опитайте отново"),
("Close", "Затваряне"),
("Retry", "Преповтори"),
("OK", "Добре"),
("Password Required", "Изисква се парола"),
("Please enter your password", "Моля въведете паролата си"),
("Please enter your password", "Моля въведете парола"),
("Remember password", "Запомни паролата"),
("Wrong Password", "Грешна парола"),
("Do you want to enter again?", "Искате ли да въведете отново?"),
@@ -99,73 +99,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Properties", "Свойства"),
("Multi Select", "Множествен избор"),
("Select All", "Избери всички"),
("Unselect All", "Деселектирай всички"),
("Empty Directory", "Празна директория"),
("Not an empty directory", "Не е празна директория"),
("Unselect All", "Избери никой"),
("Empty Directory", "Празна папка"),
("Not an empty directory", "Не е празна папка"),
("Are you sure you want to delete this file?", "Сигурни ли сте, че искате да изтриете този файл?"),
("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна директория?"),
("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази директория?"),
("Do this for all conflicts", "Направете това за всички конфликти"),
("This is irreversible!", ""),
("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна папка?"),
("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази папка?"),
("Do this for all conflicts", "Разреши така всички конфликти"),
("This is irreversible!", "Това е необратимо!"),
("Deleting", "Изтриване"),
("files", "файлове"),
("Waiting", ""),
("Finished", "Готово"),
("Waiting", "Изчакване"),
("Finished", "Завършено"),
("Speed", "Скорост"),
("Custom Image Quality", "Персонализирано качество на изображението"),
("Custom Image Quality", "Качество на изображението по свой избор"),
("Privacy mode", "Режим на поверителност"),
("Block user input", "Блокиране на потребителско въвеждане"),
("Unblock user input", "Отблокиране на потребителско въвеждане"),
("Adjust Window", "Регулирай прозореца"),
("Block user input", "Забрана за потребителски вход"),
("Unblock user input", "Разрешаване на потребителски въвеждане"),
("Adjust Window", "Нагласи прозореца"),
("Original", "Оригинално"),
("Shrink", "Свиване"),
("Stretch", "Разтегнат"),
("Scrollbar", "Плъзгач"),
("ScrollAuto", "Автоматичен плъзгач"),
("ScrollAuto", "Автоматичено приплъзване"),
("Good image quality", "Добро качество на изображението"),
("Balanced", "Балансиран"),
("Optimize reaction time", "Оптимизирайте времето за реакция"),
("Custom", "Персонализиран"),
("Show remote cursor", "Показване на дистанционния курсор"),
("Show quality monitor", "Показване на прозорец за качество"),
("Disable clipboard", "Деактивиране на клипборда"),
("Lock after session end", "Заключване след края на сесията"),
("Balanced", "Уравновесен"),
("Optimize reaction time", "С оглед времето на реакция"),
("Custom", "По собствено желание"),
("Show remote cursor", "Показвай отдалечения курсор"),
("Show quality monitor", "Показвай прозорец за качество"),
("Disable clipboard", "Забрана за достъп до клипборд"),
("Lock after session end", "Заключване след край на ползване"),
("Insert", "Поставяне"),
("Insert Lock", "Заявка за заключване"),
("Refresh", "Обнови"),
("ID does not exist", "ID-то не съществува"),
("Failed to connect to rendezvous server", "Неуспешно свързване със сървъра за рандеву"),
("Refresh", "Обновяване"),
("ID does not exist", "Несъществуващ определител (ID)"),
("Failed to connect to rendezvous server", "Неуспешно свързване към сървъра за среща (rendezvous)"),
("Please try later", "Моля опитайте по-късно"),
("Remote desktop is offline", "Отдалеченият работен плот е офлайн"),
("Remote desktop is offline", "Отдалечената работна среда не е налична"),
("Key mismatch", "Ключово несъответствие"),
("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", "Задайте парола"),
("Timeout", "Изтичане на времето"),
("Failed to connect to relay server", "Провал при свързване към препредаващ сървър"),
("Failed to connect via rendezvous server", "Провал при свързване към сървър за срещи (rendezvous)"),
("Failed to connect via relay server", "Провал при свързване чрез препредаващ сървър"),
("Failed to make direct connection to remote desktop", "Провал при установяване на пряка връзка с отдалечена работна среда"),
("Set Password", "Задаване на парола"),
("OS Password", "Парола на Операционната система"),
("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно като отдалечена достъп. За да заобиколите UAC, моля, щракнете върху бутона по-долу, за да инсталирате RustDesk в системата."),
("Click to upgrade", "Кликнете, за да надстроите"),
("Click to download", "Кликнете, за да изтеглите"),
("Click to update", "Кликнете, за да актуализирате"),
("Configure", "Конфигуриране"),
("config_acc", "За да управлявате вашия работен плот дистанционно, трябва да предоставите на RustDesk разрешения \"Достъпност\"."),
("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."),
("Installing ...", "Инсталиране..."),
("Install", "Инсталирай"),
("Installation", "Инсталация"),
("Installation Path", "Инсталационен път"),
("Create start menu shortcuts", "Създайте преки пътища в менюто 'Старт'."),
("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно за отдалечена достъп. За да заобиколите UAC, моля, натиснете копчето по-долу, за да поставите RustDesk като системна услуга."),
("Click to upgrade", "Натиснете, за да надстроите"),
("Click to download", "Натиснете, за да изтеглите"),
("Click to update", "Натиснете, за да обновите"),
("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 ...", "Генериране..."),
("agreement_tip", "Започвайки поставянето, вие приемате лицензионното споразумение."),
("Accept and Install", "Приемете и поставяте"),
("End-user license agreement", "Споразумение с потребителя"),
("Generating ...", "Пораждане..."),
("Your installation is lower version.", "Вашата инсталация е по-ниска версия."),
("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"),
("Listening ...", "Слушане..."),
("Remote Host", "Отдалечен хост"),
("Remote Host", "Отдалечен сървър"),
("Remote Port", "Отдалечен порт"),
("Action", "Действие"),
("Add", "Добави"),
@@ -173,154 +173,154 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Local Address", "Локален адрес"),
("Change Local Port", "Промяна на локалният порт"),
("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"),
("Too short, at least 6 characters.", ""),
("The confirmation is not identical.", ""),
("Too short, at least 6 characters.", "Прекалено кратко, поне 6 знака"),
("The confirmation is not identical.", "Потвърждението не съвпада"),
("Permissions", "Разрешения"),
("Accept", "Приеми"),
("Dismiss", "Отхвърляне"),
("Disconnect", "Прекъснете връзката"),
("Enable file copy and paste", ""),
("Disconnect", "Прекъсване"),
("Enable file copy and paste", "Разрешаване прехвърляне на файлове"),
("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 сесия"),
("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 достъп"),
("Enable direct IP access", "Разрешаване пряк IP достъп"),
("Rename", "Преименуване"),
("Space", "Пространство"),
("Create desktop shortcut", "Създайте пряк път на работния плот"),
("Change Path", "Промяна на пътя"),
("Create Folder", "Създай папка"),
("Please enter the folder name", "Моля, въведете името на папката"),
("Please enter the folder name", "Моля, въведете име на папката"),
("Fix it", "Оправи го"),
("Warning", "Внимание"),
("Login screen using Wayland is not supported", "Екранът за влизане с помощта на Wayland не се поддържа"),
("Reboot required", "Изисква се рестартиране"),
("Unsupported display server", "Неподдържан сървър за дисплея"),
("x11 expected", ""),
("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", "Затворено ръчно от партньора"),
("Invalid port", "Недопустим порт"),
("Closed manually by the peer", "Затворено ръчно от другата страна"),
("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"),
("Run without install", "Стартирайте без инсталиране"),
("Connect via relay", "Свържете чрез реле"),
("Always connect via relay", "Винаги свързвайте чрез реле"),
("Connect via relay", "Свързване чрез препращане"),
("Always connect via relay", "Винаги чрез препращане"),
("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"),
("Login", "Влизане"),
("Verify", "Потвърди"),
("Remember me", "Запомни ме"),
("Trust this device", "Доверете се на това устройство"),
("Trust this device", "Доверяване на това устройство"),
("Verification code", "Код за потвърждение"),
("verification_tip", "На регистрирания имейл адрес е изпратен код за потвърждение, въведете кода за потвърждение, за да продължите да влизате."),
("Logout", "Излез от профила си"),
("Tags", "Етикети"),
("Search ID", "Търсене на ID"),
("whitelist_sep", "Разделени със запетая, точка и запетая, интервали или нов ред"),
("verification_tip", "На посочения имейл е изпратен код за потвърждение. Моля въведете го, за да продължите с влизането."),
("Logout", "Отписване (Изход)"),
("Tags", "Белези"),
("Search ID", "Търси ID"),
("whitelist_sep", "Разделени със запетая, точка и запетая, празни символи или нов ред"),
("Add ID", "Добави ID"),
("Add Tag", "Добави етикет"),
("Unselect all tags", "Премахнете избора на всички етикети"),
("Unselect all tags", "Премахнете избора на всички белези (tags)"),
("Network error", "Мрежова грешка"),
("Username missed", "Пропуснато потребителско име"),
("Password missed", "Пропусната парола"),
("Wrong credentials", "Wrong username or password"),
("The verification code is incorrect or has expired", ""),
("Edit Tag", "Edit tag"),
("Username missed", "Липсващо потребителско име"),
("Password missed", "Липсваща парола"),
("Wrong credentials", "Грешни пълномощия"),
("The verification code is incorrect or has expired", "Кодът за проверка е неправилен или с изтекла давност."),
("Edit Tag", "Промени белег"),
("Forget Password", "Забравена парола"),
("Favorites", ""),
("Favorites", "Любими"),
("Add to Favorites", "Добави към любими"),
("Remove from Favorites", "Премахване от любими"),
("Empty", "Празно"),
("Invalid folder name", ""),
("Socks5 Proxy", "Socks5 прокси"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"),
("Discovered", ""),
("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."),
("Remote ID", "Дистанционно ID"),
("Invalid folder name", "Непозволено име на папка"),
("Socks5 Proxy", "Socks5 посредник"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) посредник"),
("Discovered", "Открит"),
("install_daemon_tip", "За зареждане при стартиране на ОС следва да поставите RustDesk като системна услуга."),
("Remote ID", "Отдалечено ID"),
("Paste", "Постави"),
("Paste here?", "Постави тук?"),
("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"),
("Download new version", ""),
("Touch mode", "Режим тъч (сензорен)"),
("Download new version", "Изтегляне на нова версия"),
("Touch mode", "Режим сензорен (touch)"),
("Mouse mode", "Режим мишка"),
("One-Finger Tap", "Докосване с един пръст"),
("One-Finger Tap", "Допир с един пръст"),
("Left Mouse", "Ляв бутон на мишката"),
("One-Long Tap", "Едно дълго докосване"),
("Two-Finger Tap", "Докосване с два пръста"),
("One-Long Tap", "Дълъг допир"),
("Two-Finger Tap", "Допир с два пръста"),
("Right Mouse", "Десен бутон на мишката"),
("One-Finger Move", "Преместване с един пръст"),
("Double Tap & Move", "Докоснете два пъти и преместете"),
("Mouse Drag", "Плъзгане с мишката"),
("Double Tap & Move", "Двоен допир и преместване"),
("Mouse Drag", "Провличане с мишката"),
("Three-Finger vertically", "Три пръста вертикално"),
("Mouse Wheel", "Колело на мишката"),
("Two-Finger Move", "Движение с два пръста"),
("Canvas Move", "Преместване на платното"),
("Pinch to Zoom", "Щипнете, за да увеличите"),
("Canvas Zoom", "Увеличение на платното"),
("Reset canvas", ""),
("No permission of file transfer", ""),
("Note", ""),
("Connection", ""),
("Reset canvas", "Нулиране на платното"),
("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"),
("Note", "Бележка"),
("Connection", "Връзка"),
("Share Screen", "Сподели екран"),
("Chat", "Чат"),
("Total", "Обшо"),
("items", "елементи"),
("Chat", "Говор"),
("Total", "Общо"),
("items", "неща"),
("Selected", "Избрано"),
("Screen Capture", "Заснемане на екрана"),
("Input Control", "Контрол на въвеждане"),
("Audio Capture", "Аудио записване"),
("Screen Capture", "Снемане на екрана"),
("Input Control", "Управление на вход"),
("Audio Capture", "Аудиозапис"),
("File Connection", "Файлова връзка"),
("Screen Connection", "Свързване на екрана"),
("Screen Connection", "Екранна връзка"),
("Do you accept?", "Приемате ли?"),
("Open System Setting", "Отворете системната настройка"),
("How to get Android input permission?", ""),
("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или докосване, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."),
("Open System Setting", "Отворете системните настройки"),
("How to get Android input permission?", "Как да получим право за въвеждане под Андрид?"),
("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или допир, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."),
("android_input_permission_tip2", "Моля, отидете на следващата страница с системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."),
("android_new_connection_tip", "Получена е нова заявка за контрол, която иска да контролира вашето текущо устройство."),
("android_service_will_start_tip", "Включването на \"Заснемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."),
("android_new_connection_tip", "Получена е нова заявка за отдалечено управление на вашето текущо устройство."),
("android_service_will_start_tip", "Включването на \"Снемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."),
("android_stop_service_tip", "Затварянето на услугата автоматично ще затвори всички установени връзки."),
("android_version_audio_tip", "Текущата версия на Android не поддържа аудио заснемане, моля, актуализирайте устройството с Android 10 или по-нова версия."),
("android_start_service_tip", "Докоснете [Start service] или активирайте разрешение [Screen Capture], за да стартирате услугата за споделяне на екрана."),
("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, докато не се свържете отново."),
("Account", "Акаунт"),
("android_version_audio_tip", "Текущата версия на Android не поддържа аудиозапис. Моля, актуализирайте устройството с Android 10 или по-нов."),
("android_start_service_tip", "Докоснете [Start service] или позволете [Screen Capture], за да започне услугата по споделяне на екрана."),
("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, а ще изискват да се свържете отново."),
("Account", "Сметка"),
("Overwrite", "Презаписване"),
("This file exists, skip or overwrite this file?", ""),
("Quit", "Излез"),
("This file exists, skip or overwrite this file?", "Този файл съществува вече. Пропускане или презаписване?"),
("Quit", "Изход"),
("Help", "Помощ"),
("Failed", "Неуспешно"),
("Succeeded", "Успешно"),
("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, излезте"),
("Unsupported", "Не се поддържа"),
("Peer denied", ""),
("Please install plugins", ""),
("Peer exit", ""),
("Failed to turn off", ""),
("Turned off", ""),
("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, изход"),
("Unsupported", "Неподдържан"),
("Peer denied", "Отказ от другата страна"),
("Please install plugins", "Моля поставете приставки"),
("Peer exit", "Изход от другата страна"),
("Failed to turn off", "Провал при опит за изключване"),
("Turned off", "Изкключен"),
("Language", "Език"),
("Keep RustDesk background service", ""),
("Keep RustDesk background service", "Запази работеща фонова услуга с RustDesk"),
("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"),
("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"),
("Start on boot", "Стартирайте при зареждане"),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", ""),
("Legacy mode", ""),
("Map mode", ""),
("Translate mode", "Режим на превод"),
("Use permanent password", "Използвайте постоянна парола"),
("Use both passwords", "Използвайте и двете пароли"),
("Set permanent password", "Задайте постоянна парола"),
("Enable remote restart", "Разрешете отдалечено рестартиране"),
("Restart remote device", "Рестартирайте отдалеченото устройство"),
("Connection not allowed", "Връзката непозволена"),
("Legacy mode", "По остарял начин"),
("Map mode", "По начин със съответствие (map)"),
("Translate mode", "По нчаин с превод"),
("Use permanent password", "Използване на постоянна парола"),
("Use both passwords", "Използване и на двете пароли"),
("Set permanent password", "Задаване постоянна парола"),
("Enable remote restart", "Разрешаване на отдалечен рестарт"),
("Restart remote device", "Рестартиране на отдалечено устройство"),
("Are you sure you want to restart", "Сигурни ли сте, че искате да рестартирате"),
("Restarting remote device", "Рестартира се отдалечено устройство"),
("Restarting remote device", "Рестартиране на отдалечено устройство"),
("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"),
("Copied", "Копирано"),
("Copied", "Преписано"),
("Exit Fullscreen", "Изход от цял екран"),
("Fullscreen", "Цял екран"),
("Mobile Actions", "Мобилни действия"),
@@ -334,10 +334,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Hide Toolbar", "Скриване на лентата с инструменти"),
("Direct Connection", "Директна връзка"),
("Relay Connection", "Релейна връзка"),
("Secure Connection", "Защитена връзка"),
("Insecure Connection", "Незащитена връзка"),
("Secure Connection", "Сигурна връзка"),
("Insecure Connection", "Несигурна връзка"),
("Scale original", "Оригинален мащаб"),
("Scale adaptive", "Адаптивно мащабиране"),
("Scale adaptive", "Приспособимо мащабиране"),
("General", "Основен"),
("Security", "Сигурност"),
("Theme", "Тема"),
@@ -345,128 +345,128 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Light Theme", "Светла тема"),
("Dark", "Тъмна"),
("Light", "Светла"),
("Follow System", "Следвай системата"),
("Enable hardware codec", "Активиране на хардуерен кодек"),
("Follow System", "Следвай система"),
("Enable hardware codec", "Позволяване хардуерен кодек"),
("Unlock Security Settings", "Отключи настройките за сигурност"),
("Enable audio", "Разрешете аудиото"),
("Unlock Network Settings", "Отключи мрежовите настройки"),
("Server", "Сървър"),
("Direct IP Access", "Директен IP достъп"),
("Proxy", "Прокси"),
("Apply", "Приложи"),
("Disconnect all devices?", ""),
("Clear", "Изчисти"),
("Direct IP Access", "Пряк IP достъп"),
("Proxy", "Посредник (Proxy)"),
("Apply", "Прилагане"),
("Disconnect all devices?", "Разкачване на всички устройства"),
("Clear", "Изчистване"),
("Audio Input Device", "Аудио входно устройство"),
("Use IP Whitelisting", "Използвайте бял списък с IP адреси"),
("Use IP Whitelisting", "Използване бял списък с IP адреси"),
("Network", "Мрежа"),
("Pin Toolbar", "Фиксиране на лентата с инструменти"),
("Unpin Toolbar", "Откачване на лентата с инструменти"),
("Pin Toolbar", "Закачане лента с инструменти"),
("Unpin Toolbar", "Откачюане лента с инструменти"),
("Recording", "Записване"),
("Directory", "Директория"),
("Automatically record incoming sessions", ""),
("Change", "Промени"),
("Start session recording", ""),
("Stop session recording", ""),
("Enable recording session", ""),
("Enable LAN discovery", "Активирайте откриване в LAN"),
("Deny LAN discovery", "Забранете откриване в LAN"),
("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"),
("Change", "Промяна"),
("Start session recording", "Започванена запис"),
("Stop session recording", "Край на запис"),
("Enable recording session", "Позволяване запис"),
("Enable LAN discovery", "Позволяване откриване във вътрешна мрежа"),
("Deny LAN discovery", "Забрана за откриване във вътрешна мрежа"),
("Write a message", "Напишете съобщение"),
("Prompt", "Подкана"),
("Please wait for confirmation of UAC...", ""),
("Please wait for confirmation of UAC...", "Моля изчакайте за потвърждение от UAC..."),
("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."),
("Disconnected", "Прекъсната връзка"),
("Other", "Други"),
("Confirm before closing multiple tabs", ""),
("Confirm before closing multiple tabs", "Потвърждение преди затваряне на няколко раздела"),
("Keyboard Settings", "Настройки на клавиатурата"),
("Full Access", "Пълен достъп"),
("Screen Share", "Споделяне на екрана"),
("Wayland requires Ubuntu 21.04 or higher version.", ""),
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""),
("JumpLink", "Преглед"),
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (Работете от страна на партньора)."),
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"),
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
("JumpLink", "Препратка"),
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
("Show RustDesk", "Покажи RustDesk"),
("This PC", "Този компютър"),
("or", "или"),
("Continue with", "Продължи с"),
("Elevate", "Повишаване"),
("Zoom cursor", "Мащабиране на Курсор"),
("Accept sessions via password", "Приемайте сесии чрез парола"),
("Accept sessions via click", "Приемане на сесии чрез щракване"),
("Accept sessions via both", "Приемайте сесии и през двете"),
("Please wait for the remote side to accept your session request...", ""),
("Zoom cursor", "Уголемяване курсор"),
("Accept sessions via password", "Приемане сесии чрез парола"),
("Accept sessions via click", "Приемане сесии чрез цъкване"),
("Accept sessions via both", "Приемане сесии и по двата начина"),
("Please wait for the remote side to accept your session request...", "Моля, изчакайте докато другата страна приеме заявката за отдалечен достъп..."),
("One-time Password", "Еднократна парола"),
("Use one-time password", ""),
("One-time password length", ""),
("Request access to your device", ""),
("Hide connection management window", ""),
("hide_cm_tip", "Разрешете скриването само ако приемате сесии чрез парола и използвате постоянна парола"),
("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."),
("Right click to select tabs", ""),
("Use one-time password", "Ползване на еднократна парола"),
("One-time password length", "Дължина на еднократна парола"),
("Request access to your device", "Искане за достъп до ваше устройство"),
("Hide connection management window", "Скриване на прозореца за управление на свързване"),
("hide_cm_tip", "Разрешаване скриване само ако се приемат сесии чрез постоянна парола"),
("wayland_experiment_tip", "Поддръжката на Wayland е в експериментален стадий, моля, използвайте X11, ако се нуждаете от безконтролен достъп.."),
("Right click to select tabs", "Десен бутон за избор на раздел"),
("Skipped", "Пропуснато"),
("Add to address book", ""),
("Add to address book", "Добавяне към познати адреси"),
("Group", "Група"),
("Search", "Търсене"),
("Closed manually by web console", ""),
("Local keyboard type", ""),
("Select local keyboard type", ""),
("Closed manually by web console", "Затворен ръчно от уеб конзола"),
("Local keyboard type", "Тип на тукашната клавиатура"),
("Select local keyboard type", "Избор на тип на тукашната клавиатура"),
("software_render_tip", "Ако използвате графична карта Nvidia под Linux и отдалеченият прозорец се затваря веднага след свързване, превключването към драйвера Nouveau с отворен код и изборът да използвате софтуерно изобразяване може да помогне. Изисква се рестартиране на софтуера."),
("Always use software rendering", ""),
("config_input", "За да контролирате отдалечен работен плот с клавиатура, трябва да предоставите на RustDesk разрешения \"Input Monitoring\"."),
("config_microphone", "За да говорите дистанционно, трябва да предоставите на RustDesk разрешения \"Запис на звук\"."),
("request_elevation_tip", "Можете също така да поискате повишаване на привилегии, ако има някой от отдалечената страна."),
("Wait", "Изчакайте"),
("Elevation Error", "Грешка при повишаване на привилегии"),
("Ask the remote user for authentication", ""),
("Choose this if the remote account is administrator", ""),
("Transmit the username and password of administrator", ""),
("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиранят RustDesk."),
("Request Elevation", "Поискайте повишаване на привилегии"),
("Always use software rendering", "Винаги ползвай софтуерно изграждане на картината"),
("config_input", "За да управлявате отдалечена среда с клавиатура, трябва да предоставите на RustDesk право за \"Input Monitoring\"."),
("config_microphone", "За да говорите отдалечено, трябва да предоставите на RustDesk право за \"Запис на звук\"."),
("request_elevation_tip", "Можете също така да поискате разширени права, ако има някой от отдалечената страна."),
("Wait", "Изчакване"),
("Elevation Error", "Грешка при добвиане на разширени права"),
("Ask the remote user for authentication", "Попитайте отдалечения потребител за удостоверяване"),
("Choose this if the remote account is administrator", "Изберете това, ако отдалеченият потребител е администратор."),
("Transmit the username and password of administrator", "Предаване на потребителското име и паролата на администратора"),
("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиран RustDesk."),
("Request Elevation", "Поискайте разширени права"),
("wait_accept_uac_tip", "Моля, изчакайте отдалеченият потребител да приеме диалоговия прозорец на UAC."),
("Elevate successfully", ""),
("uppercase", ""),
("lowercase", ""),
("digit", ""),
("special character", ""),
("length>=8", ""),
("Weak", ""),
("Medium", ""),
("Strong", ""),
("Switch Sides", "Сменете страните"),
("Please confirm if you want to share your desktop?", ""),
("Display", ""),
("Elevate successfully", "Успешно получаване на разширени права"),
("uppercase", "големи букви"),
("lowercase", "малки букви"),
("digit", "цифра"),
("special character", "специален знак"),
("length>=8", "дължина>=8"),
("Weak", "Слаба"),
("Medium", "Средна"),
("Strong", "Силна"),
("Switch Sides", "Размяна на страните"),
("Please confirm if you want to share your desktop?", "Моля, потвърдете дали искате да споделите работното си пространство"),
("Display", "Екран"),
("Default View Style", "Стил на изглед по подразбиране"),
("Default Scroll Style", "Стил на превъртане по подразбиране"),
("Default Image Quality", "Качество на изображението по подразбиране"),
("Default Codec", "Кодек по подразбиране"),
("Bitrate", "Битрейт"),
("Bitrate", "Скорост на предаване на данни (bitrate)"),
("FPS", "Кадри в секунда"),
("Auto", "Автоматично"),
("Other Default Options", "Други опции по подразбиране"),
("Voice call", ""),
("Text chat", ""),
("Stop voice call", ""),
("Voice call", "Гласови обаждания"),
("Text chat", "Текстов разговор"),
("Stop voice call", "Прекратяване гласово обаждане"),
("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез реле. Освен това, ако искате да използвате реле при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез реле\" в картата на последните сесии, ако съществува."),
("Reconnect", "Свържете се отново"),
("Reconnect", "Повторно свързане"),
("Codec", "Кодек"),
("Resolution", "Резолюция"),
("No transfers in progress", "Не се извършват трансфери"),
("Set one-time password length", ""),
("Resolution", "Разделителна способност"),
("No transfers in progress", "Няма текущи прехвърляния"),
("Set one-time password length", "Задаване дължаина на еднократна парола"),
("RDP Settings", "RDP настройки"),
("Sort by", "Сортирай по"),
("Sort by", "Подредба по"),
("New Connection", "Ново свързване"),
("Restore", ""),
("Minimize", ""),
("Maximize", ""),
("Restore", "Възстановяване"),
("Minimize", "Смаляване"),
("Maximize", "Уголемяване"),
("Your Device", "Вашето устройство"),
("empty_recent_tip", "Ами сега, няма скорошни сесии!\nВреме е да планирате нова."),
("empty_favorite_tip", "Все още нямате любими връстници?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"),
("empty_lan_tip", "О, не, изглежда, че все още не сме открили връстници."),
("empty_address_book_tip", "Изглежда, че в момента няма изброени връстници във вашата адресна книга."),
("eg: admin", ""),
("eg: admin", "напр. admin"),
("Empty Username", "Празно потребителско име"),
("Empty Password", "Празна парола"),
("Me", "Аз"),
("identical_file_tip", "Този файл е идентичен с този на партньора."),
("Me", "Мен"),
("identical_file_tip", "Файлът съвпада с този от другата страна."),
("show_monitors_tip", "Показване на мониторите в лентата с инструменти"),
("View Mode", "Режим на преглед"),
("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"),
@@ -482,47 +482,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"),
("No need to elevate", ""),
("System Sound", "Системен звук"),
("Default", ""),
("New RDP", ""),
("Fingerprint", ""),
("Default", "По подразбиране"),
("New RDP", "Нов RDP"),
("Fingerprint", "Пръстов отпечатък"),
("Copy Fingerprint", "Копиране на пръстов отпечатък"),
("no fingerprints", "Няма пръстови отпечатъци"),
("Select a peer", ""),
("Select peers", ""),
("Plugins", ""),
("Uninstall", ""),
("Update", ""),
("Enable", ""),
("Disable", ""),
("Select a peer", "Избери отдалечена страна"),
("Select peers", "Избери отдалечени страни"),
("Plugins", "Приставки"),
("Uninstall", "Премахни"),
("Update", "Обновяване"),
("Enable", "Позволяване"),
("Disable", "Забрана"),
("Options", "Настроики"),
("resolution_original_tip", "Оригинална резолюция"),
("resolution_fit_local_tip", "Напасване към локална разделителна способност"),
("resolution_custom_tip", "Персонализирана разделителна способност"),
("resolution_original_tip", "Оригинална разделителна способност"),
("resolution_fit_local_tip", "Приспособяване към тукашната разделителна способност"),
("resolution_custom_tip", "Разделителна способност по свой избор"),
("Collapse toolbar", "Свиване на лентата с инструменти"),
("Accept and Elevate", "Приемете и повишаване на привилегии"),
("accept_and_elevate_btn_tooltip", "Приемете връзката и повишете UAC разрешенията."),
("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за копиране изтече."),
("Incoming connection", ""),
("Outgoing connection", ""),
("Exit", "Излез"),
("Open", "Отвори"),
("Accept and Elevate", "Приемане и предоставяне на допълнителни права"),
("accept_and_elevate_btn_tooltip", "Приемане на връзката предоставяне на UAC разрешения."),
("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за препис изтече."),
("Incoming connection", "Входяща връзка"),
("Outgoing connection", "Изходяща връзка"),
("Exit", "Изход"),
("Open", "Отваряне"),
("logout_tip", "Сигурни ли сте, че искате да излезете?"),
("Service", "Услуга"),
("Start", "Стартиране"),
("Stop", "Спиране"),
("exceed_max_devices", "Достигнахте максималния брой управлявани устройства."),
("Sync with recent sessions", ""),
("Sort tags", ""),
("Open connection in new tab", ""),
("Move tab to new window", ""),
("Can not be empty", ""),
("Already exists", ""),
("Change Password", "Промяна на паролата"),
("Refresh Password", "Обнови паролата"),
("ID", ""),
("Sync with recent sessions", "Синхронизиране с последните сесии"),
("Sort tags", "Подреди белези"),
("Open connection in new tab", "Разкриване на връзка в нов раздел"),
("Move tab to new window", "Отделяне на раздела в нов прозорец"),
("Can not be empty", "Не може да е празно"),
("Already exists", "Вече съществува"),
("Change Password", "Промяна на парола"),
("Refresh Password", "Обновяване парола"),
("ID", "Определител (ID)"),
("Grid View", "Мрежов изглед"),
("List View", "Списъчен изглед"),
("Select", ""),
("Select", "Избиране"),
("Toggle Tags", "Превключване на етикети"),
("pull_ab_failed_tip", "Неуспешно опресняване на адресната книга"),
("push_ab_failed_tip", "Неуспешно синхронизиране на адресната книга със сървъра"),
@@ -530,119 +530,122 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Change Color", "Промяна на цвета"),
("Primary Color", "Основен цвят"),
("HSV Color", "HSV цвят"),
("Installation Successful!", "Успешна инсталация!"),
("Installation failed!", ""),
("Reverse mouse wheel", ""),
("{} sessions", ""),
("Installation Successful!", "Успешно поставяне!"),
("Installation failed!", "Провал при поставяне"),
("Reverse mouse wheel", "Обърнато колелото на мишката"),
("{} sessions", "{} сесии"),
("scam_title", "Възможно е да сте ИЗМАМЕНИ!"),
("scam_text1", "Ако разговаряте по телефона с някой, когото НЕ ПОЗНАВАТЕ и НЯМАТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да стартирате услугата, не продължавайте и затворете незабавно."),
("scam_text2", "Те вероятно са измамник, който се опитва да открадне вашите пари или друга лична информация."),
("Don't show again", "Не показвай отново"),
("I Agree", ""),
("Decline", ""),
("Timeout in minutes", ""),
("I Agree", "Съгласен"),
("Decline", "Отказвам"),
("Timeout in minutes", "Време за отговор в минути"),
("auto_disconnect_option_tip", "Автоматично затваряне на входящите сесии при неактивност на потребителя"),
("Connection failed due to inactivity", "Автоматично прекъсване на връзката поради неактивност"),
("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", "Моля обновете RustDesk Server Pro на версия {} или по-нова!"),
("pull_group_failed_tip", "Неуспешно опресняване на групата"),
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("Test", ""),
("Filter by intersection", "Отсяване по пресичане"),
("Remove wallpaper during incoming sessions", "Спри фоновото изображение по време на входящи сесии"),
("Test", "Проверка"),
("display_is_plugged_out_msg", "Дисплеят е изключен, превключете на първия монитор."),
("No displays", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
("Use all my displays for the remote session", ""),
("No displays", "Няма екрани"),
("Open in new window", "Отваряне в нов прозорец"),
("Show displays as individual windows", "Показване на екраните в отделни прозорци"),
("Use all my displays for the remote session", "Използване на всички тукашни екрани за отдалечена работа"),
("selinux_tip", "SELinux е активиран на вашето устройство, което може да попречи на RustDesk да работи правилно като контролирана страна."),
("Change view", ""),
("Big tiles", ""),
("Small tiles", ""),
("List", ""),
("Virtual display", ""),
("Plug out all", ""),
("Change view", "Промяна изглед"),
("Big tiles", "Големи заглавия"),
("Small tiles", "Малки заглавия"),
("List", "Списък"),
("Virtual display", "Виртуален екран"),
("Plug out all", "Изтръгване на всички"),
("True color (4:4:4)", ""),
("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"),
("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (<domain>:<port>).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (<id>@<server_address >?key=<key_value>), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"<id>@public\" , ключът не е необходим за публичен сървър"),
("privacy_mode_impl_mag_tip", "Режим 1"),
("privacy_mode_impl_virtual_display_tip", "Режим 2"),
("Enter privacy mode", ""),
("Exit privacy mode", ""),
("Enter privacy mode", "Влизане в поверителен режим"),
("Exit privacy mode", "Изход от поверителен режим"),
("idd_not_support_under_win10_2004_tip", "Индиректен драйвер за дисплей не се поддържа. Изисква се Windows 10, версия 2004 или по-нова."),
("input_source_1_tip", "Входен източник 1"),
("input_source_2_tip", "Входен източник 2"),
("Swap control-command key", ""),
("swap-left-right-mouse", "Разменете левия и десния бутон на мишката"),
("2FA code", "Код за Двуфакторна удостоверяване"),
("swap-left-right-mouse", "Размяна на копчетата на мишката"),
("2FA code", "Код за Двуфакторно удостоверяване"),
("More", "Повече"),
("enable-2fa-title", "Активиране на двуфакторно удостоверяване"),
("enable-2fa-title", "Позволяване на двуфакторно удостоверяване"),
("enable-2fa-desc", "Моля, настройте вашия удостоверител сега. Можете да използвате приложение за удостоверяване като Authy, Microsoft или Google Authenticator на вашия телефон или настолен компютър.\n\nСканирайте QR кода с вашето приложение и въведете кода, който приложението ви показва, за да активирате двуфакторно удостоверяване."),
("wrong-2fa-code", "е може да се потвърди кодът. Проверете дали настройките за код и локалното време са правилни"),
("enter-2fa-title", "Двуфакторно удостоверяване"),
("Email verification code must be 6 characters.", ""),
("2FA code must be 6 digits.", ""),
("Multiple Windows sessions found", ""),
("Please select the session you want to connect to", ""),
("Email verification code must be 6 characters.", "Кодът за проверка следва да е с дължина 6 знака."),
("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"),
("Multiple Windows sessions found", "Установени са няколко Windwos сесии"),
("Please select the session you want to connect to", "Моля определете сесия към която искате да се свърженете"),
("powered_by_me", ""),
("outgoing_only_desk_tip", ""),
("preset_password_warning", ""),
("Security Alert", ""),
("My address book", ""),
("Personal", ""),
("Owner", ""),
("Set shared password", ""),
("Exist in", ""),
("Read-only", ""),
("Read/Write", ""),
("Full Control", ""),
("Security Alert", "Предупреждение за сигурност"),
("My address book", "Моята адресна книга"),
("Personal", "Личен"),
("Owner", "Собственик"),
("Set shared password", "Определяне споделена парола"),
("Exist in", "Съществува в"),
("Read-only", "Само четене"),
("Read/Write", "Писане/четене"),
("Full Control", "Пълен контрол"),
("share_warning_tip", ""),
("Everyone", ""),
("Everyone", "Всички"),
("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
("Follow remote cursor", "Следвай отдалечения курсор"),
("Follow remote window focus", "Следвай фокуса на отдалечените прозорци"),
("default_proxy_tip", ""),
("no_audio_input_device_tip", ""),
("Incoming", ""),
("Outgoing", ""),
("Clear Wayland screen selection", ""),
("Incoming", "Входящ"),
("Outgoing", "Изходящ"),
("Clear Wayland screen selection", "Изчистване избор на Wayland екран"),
("clear_Wayland_screen_selection_tip", ""),
("confirm_clear_Wayland_screen_selection_tip", ""),
("android_new_voice_call_tip", ""),
("texture_render_tip", ""),
("Use texture rendering", ""),
("Floating window", ""),
("Use texture rendering", "Използвай текстово изграждане"),
("Floating window", "Плаващ прозорец"),
("floating_window_tip", ""),
("Keep screen on", ""),
("Never", ""),
("During controlled", ""),
("During service is on", ""),
("Capture screen using DirectX", ""),
("Keep screen on", "Запази екранът включен"),
("Never", "Никога"),
("During controlled", "Докато е обект на управление"),
("During service is on", "Докато услугата е включена"),
("Capture screen using DirectX", "Снемай екрана ползвайки DirectX"),
("Back", "Назад"),
("Apps", ""),
("Volume up", ""),
("Volume down", ""),
("Power", ""),
("Telegram bot", ""),
("Apps", "Приложения"),
("Volume up", "Усилване звук"),
("Volume down", "Намаляне звук"),
("Power", "Мощност"),
("Telegram bot", "Телеграм бот"),
("enable-bot-tip", ""),
("enable-bot-desc", ""),
("cancel-2fa-confirm-tip", ""),
("cancel-bot-confirm-tip", ""),
("About RustDesk", ""),
("About RustDesk", "Относно RustDesk"),
("Send clipboard keystrokes", ""),
("network_error_tip", ""),
("Unlock with PIN", ""),
("Unlock with PIN", "Отключване с PIN"),
("Requires at least {} characters", ""),
("Wrong PIN", ""),
("Set PIN", ""),
("Enable trusted devices", ""),
("Manage trusted devices", ""),
("Platform", ""),
("Days remaining", ""),
("Wrong PIN", "Грешен PIN"),
("Set PIN", "Избор PIN"),
("Enable trusted devices", "Позволяване доверени устройства"),
("Manage trusted devices", "Управление доверени устройства"),
("Platform", "Платформа"),
("Days remaining", "Оставащи дни"),
("enable-trusted-devices-tip", ""),
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("Resume", "Възобновяване"),
("Invalid file name", "Невалидно име за файл"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "父目录"),
("Resume", "继续"),
("Invalid file name", "无效文件名"),
("one-way-file-transfer-tip", "被控端启用了单项文件传输"),
("Authentication Required", "需要身份验证"),
("Authenticate", "认证"),
].iter().cloned().collect();
}

View File

@@ -641,8 +641,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Platform", "Platforma"),
("Days remaining", "Zbývajících dnů"),
("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"),
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("Parent directory", "Rodičovský adresář"),
("Resume", "Pokračovat"),
("Invalid file name", "Nesprávný název souboru"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Status"),
("Your Desktop", "Dit skrivebord"),
("desk_tip", "Du kan adgang til dit skrivebord med dette ID og adgangskode."),
("desk_tip", "Du kan give adgang til dit skrivebord med dette ID og denne adgangskode."),
("Password", "Adgangskode"),
("Ready", "Klar"),
("Established", "Etableret"),
@@ -38,18 +38,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop service", "Sluk for forbindelsesserveren"),
("Change ID", "Ændr ID"),
("Your new ID", "Dit nye ID"),
("length %min% to %max%", ""),
("starts with a letter", ""),
("allowed characters", ""),
("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."),
("length %min% to %max%", "længde %min% til %max%"),
("starts with a letter", "starter med ét bogstav"),
("allowed characters", "tilladte tegn"),
("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Antal tegn skal være mellem 6 og 16."),
("Website", "Hjemmeside"),
("About", "Om"),
("Slogan_tip", ""),
("Privacy Statement", ""),
("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"),
("Privacy Statement", "Privatlivspolitik"),
("Mute", "Sluk for mikrofonen"),
("Build Date", ""),
("Version", ""),
("Home", ""),
("Build Date", "Build dato"),
("Version", "Version"),
("Home", "Hjem"),
("Audio Input", "Lydinput"),
("Enhancements", "Forbedringer"),
("Hardware Codec", "Hardware-codec"),
@@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Original", "Original"),
("Shrink", "Krymp"),
("Stretch", "Stræk ud"),
("Scrollbar", "Rullebar"),
("ScrollAuto", "Auto-rul"),
("Scrollbar", "Scrollbar"),
("ScrollAuto", "Auto-scroll"),
("Good image quality", "God billedkvalitet"),
("Balanced", "Afbalanceret"),
("Optimize reaction time", "Optimeret responstid"),
@@ -139,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remote desktop is offline", "Fjernskrivebord er offline"),
("Key mismatch", "Nøgle uoverensstemmelse"),
("Timeout", "Timeout"),
("Failed to connect to relay server", "Forbindelse til relæ-serveren mislykkedes"),
("Failed to connect to relay server", "Forbindelse til relay-serveren mislykkedes"),
("Failed to connect via rendezvous server", "Forbindelse via Rendezvous-server mislykkedes"),
("Failed to connect via relay server", "Forbindelse via relæ-serveren mislykkedes"),
("Failed to connect via relay server", "Forbindelse via relay-serveren mislykkedes"),
("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"),
("Set Password", "Indstil adgangskode"),
("OS Password", "Operativsystemadgangskode"),
@@ -218,7 +218,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remember me", "Husk mig"),
("Trust this device", "Husk denne enhed"),
("Verification code", "Verifikationskode"),
("verification_tip", ""),
("verification_tip", "En bekræftelseskode er blevet sendt til den registrerede e-mail adresse. Indtast bekræftelseskoden for at logge på."),
("Logout", "Logger af"),
("Tags", "Nøgleord"),
("Search ID", "Søg efter ID"),
@@ -230,7 +230,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Username missed", "Glemt brugernavn"),
("Password missed", "Glemt kodeord"),
("Wrong credentials", "Forkerte registreringsdata"),
("The verification code is incorrect or has expired", ""),
("The verification code is incorrect or has expired", "Bekræftelsesnøglen er forkert eller er udløbet"),
("Edit Tag", "Rediger nøgleord"),
("Forget Password", "Glem adgangskoden"),
("Favorites", "Favoritter"),
@@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"),
("Download new version", "Download ny version"),
("Touch mode", "Touch-tilstand"),
("Mouse mode", "Musse-tilstand"),
("Mouse mode", "Muse-tilstand"),
("One-Finger Tap", "En-finger-tryk"),
("Left Mouse", "Venstre mus"),
("One-Long Tap", "Tryk og hold med en finger"),
@@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_service_will_start_tip", "Ved at tænde for skærmoptagelsen startes tjenesten automatisk, så andre enheder kan anmode om en forbindelse fra denne enhed."),
("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."),
("android_version_audio_tip", "Den aktuelle Android-version understøtter ikke lydoptagelse. Android 10 eller højere er påkrævet."),
("android_start_service_tip", ""),
("android_permission_may_not_change_tip", ""),
("android_start_service_tip", "Tryk [Start tjeneste] eller aktivér [Skærmoptagelse] tilladelse for at dele skærmen."),
("android_permission_may_not_change_tip", "Rettigheder til oprettede forbindelser ændres ikke med det samme før der forbindelsen genoprettes."),
("Account", "Konto"),
("Overwrite", "Overskriv"),
("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"),
@@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Language", "Sprog"),
("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"),
("Ignore Battery Optimizations", "Ignorér betteri optimeringer"),
("android_open_battery_optimizations_tip", ""),
("android_open_battery_optimizations_tip", "Hvis du ønsker at slukke for denne funktion, åbn RustDesk appens indstillinger, tryk på [Batteri], og fjern flueben ved [Uden begrænsninger]"),
("Start on boot", "Start under opstart"),
("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"),
("Connection not allowed", "Forbindelse ikke tilladt"),
@@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Map mode", "Kortmodus"),
("Translate mode", "Oversættelsesmodus"),
("Use permanent password", "Brug permanent adgangskode"),
("Use both passwords", "Brug begge adgangskoder"),
("Use both passwords", "Brug begge typer adgangskoder"),
("Set permanent password", "Sæt permanent adgangskode"),
("Enable remote restart", "Aktivér fjerngenstart"),
("Restart remote device", "Genstart fjernenhed"),
@@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Ratio", "Forhold"),
("Image Quality", "Billedkvalitet"),
("Scroll Style", "Rullestil"),
("Show Toolbar", ""),
("Hide Toolbar", ""),
("Show Toolbar", "Vis værktøjslinje"),
("Hide Toolbar", "Skjul værktøjslinje"),
("Direct Connection", "Direkte forbindelse"),
("Relay Connection", "Viderestillingsforbindelse"),
("Secure Connection", "Sikker forbindelse"),
@@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input Device", "Lydindgangsenhed"),
("Use IP Whitelisting", "Brug IP Whitelisting"),
("Network", "Netværk"),
("Pin Toolbar", ""),
("Unpin Toolbar", ""),
("Pin Toolbar", "Fastgør værktøjslinjen"),
("Unpin Toolbar", "Frigiv værktøjslinjen"),
("Recording", "Optager"),
("Directory", "Mappe"),
("Automatically record incoming sessions", "Optag automatisk indgående sessioner"),
@@ -368,15 +368,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Start session recording", "Start sessionsoptagelse"),
("Stop session recording", "Stop sessionsoptagelse"),
("Enable recording session", "Aktivér optagelsessession"),
("Enable LAN discovery", "Aktivér LAN Discovery"),
("Deny LAN discovery", "Afvis LAN Discovery"),
("Enable LAN discovery", "Aktivér opdagelse via det lokale netværk"),
("Deny LAN discovery", "Afvis opdagelse via det lokale netværk"),
("Write a message", "Skriv en besked"),
("Prompt", "Prompt"),
("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."),
("elevated_foreground_window_tip", ""),
("elevated_foreground_window_tip", "Det nuværende vindue på fjernskrivebordet kræver højere rettigheder for at køre, så det er midlertidigt ikke muligt at bruge musen og tastaturet. Du kan bede fjernbrugeren om at minimere vinduet, eller trykke på elevér knappen i forbindelsesvinduet. For at undgå dette problem, er det anbefalet at installere RustDesk på fjernenheden."),
("Disconnected", "Afbrudt"),
("Other", "Andre"),
("Confirm before closing multiple tabs", "Bekræft før du lukker flere faner"),
("Confirm before closing multiple tabs", "Bekræft nedlukning hvis der er flere faner"),
("Keyboard Settings", "Tastaturindstillinger"),
("Full Access", "Fuld adgang"),
("Screen Share", "Skærmdeling"),
@@ -399,8 +399,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("One-time password length", "Engangskode længde"),
("Request access to your device", "Efterspørg adgang til din enhed"),
("Hide connection management window", "Skjul forbindelseshåndteringsvindue"),
("hide_cm_tip", ""),
("wayland_experiment_tip", ""),
("hide_cm_tip", "Tillad at skjule, hvis der kun forbindes ved brug af midlertidige og permanente adgangskoder"),
("wayland_experiment_tip", "Wayland understøttelse er stadigvæk under udvikling. Hvis du har brug for ubemandet adgang, bedes du bruge X11."),
("Right click to select tabs", "Højreklik for at vælge faner"),
("Skipped", "Sprunget over"),
("Add to address book", "Tilføj til adressebog"),
@@ -409,19 +409,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by web console", "Lukket ned manuelt af webkonsollen"),
("Local keyboard type", "Lokal tastatur type"),
("Select local keyboard type", "Vælg lokal tastatur type"),
("software_render_tip", ""),
("software_render_tip", "Hvis du bruger et Nvidia grafikkort på Linux, og fjernskrivebordsvinduet lukker ned med det samme efter forbindelsen er oprettet, kan det hjælpe at skifte til Nouveau open-source driveren, og aktivere software rendering. Et genstart af RustDesk er nødvendigt."),
("Always use software rendering", "Brug altid software rendering"),
("config_input", ""),
("config_microphone", ""),
("request_elevation_tip", ""),
("config_input", "For at styre fjernskrivebordet med tastaturet, skal du give Rustdesk rettigheder til at optage tastetryk"),
("config_microphone", "For at tale sammen over fjernstyring, skal du give RustDesk rettigheder til at optage lyd"),
("request_elevation_tip", "Du kan også spørge om elevationsrettigheder, hvis der er nogen i nærheden af fjernenheden."),
("Wait", "Vent"),
("Elevation Error", "Elevationsfejl"),
("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"),
("Choose this if the remote account is administrator", "Vælg dette hvis fjernbrugeren er en administrator"),
("Transmit the username and password of administrator", "Send brugernavnet og adgangskoden på administratoren"),
("still_click_uac_tip", ""),
("still_click_uac_tip", "Kræver stadigvæk at fjernbrugeren skal trykke OK på UAC vinduet ved kørsel af RustDesk."),
("Request Elevation", "Efterspørger elevation"),
("wait_accept_uac_tip", ""),
("wait_accept_uac_tip", "Vent venligst på at fjernbrugeren accepterer UAC dialog forespørgslen."),
("Elevate successfully", "Elevation lykkedes"),
("uppercase", "store bogstaver"),
("lowercase", "små bogstaver"),
@@ -435,7 +435,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"),
("Display", "Visning"),
("Default View Style", "Standard visningsstil"),
("Default Scroll Style", "Standard rulle stil"),
("Default Scroll Style", "Standard scrollestil"),
("Default Image Quality", "Standard billedkvalitet"),
("Default Codec", "Standard codec"),
("Bitrate", "Bitrate"),
@@ -445,7 +445,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Stemmeopkald"),
("Text chat", "Tekstchat"),
("Stop voice call", "Stop stemmeopkald"),
("relay_hint_tip", ""),
("relay_hint_tip", "Det kan ske, at det ikke er muligt at forbinde direkte; du kan forsøge at forbinde via en relay-server. Derudover, hvis du ønsker at bruge en relay-server på dit første forsøg, kan du tilføje \"/r\" efter ID'et, eller bruge valgmuligheden \"Forbind altid via relay-server\" i fanen for seneste sessioner, hvis den findes."),
("Reconnect", "Genopret"),
("Codec", "Codec"),
("Resolution", "Opløsning"),
@@ -458,191 +458,194 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Minimize", "Minimér"),
("Maximize", "Maksimér"),
("Your Device", "Din enhed"),
("empty_recent_tip", ""),
("empty_favorite_tip", ""),
("empty_lan_tip", ""),
("empty_address_book_tip", ""),
("empty_recent_tip", "Ups, ingen seneste sessioner!\nTid til at oprette en ny."),
("empty_favorite_tip", "Ingen yndlings modparter endnu?\nLad os finde én at forbinde til, og tilføje den til dine favoritter!"),
("empty_lan_tip", "Åh nej, det ser ud til, at vi ikke kunne finde nogen modparter endnu."),
("empty_address_book_tip", "Åh nej, det ser ud til at der ikke er nogle modparter der er tilføjet til din adressebog."),
("eg: admin", "fx: admin"),
("Empty Username", "Tom brugernavn"),
("Empty Password", "Tom adgangskode"),
("Me", "Mig"),
("identical_file_tip", ""),
("show_monitors_tip", ""),
("View Mode", ""),
("login_linux_tip", ""),
("verify_rustdesk_password_tip", ""),
("remember_account_tip", ""),
("os_account_desk_tip", ""),
("OS Account", ""),
("another_user_login_title_tip", ""),
("another_user_login_text_tip", ""),
("xorg_not_found_title_tip", ""),
("xorg_not_found_text_tip", ""),
("no_desktop_title_tip", ""),
("no_desktop_text_tip", ""),
("No need to elevate", ""),
("System Sound", ""),
("Default", ""),
("New RDP", ""),
("Fingerprint", ""),
("Copy Fingerprint", ""),
("no fingerprints", ""),
("Select a peer", ""),
("Select peers", ""),
("Plugins", ""),
("Uninstall", ""),
("Update", ""),
("Enable", ""),
("Disable", ""),
("Options", ""),
("resolution_original_tip", ""),
("resolution_fit_local_tip", ""),
("resolution_custom_tip", ""),
("Collapse toolbar", ""),
("Accept and Elevate", ""),
("accept_and_elevate_btn_tooltip", ""),
("clipboard_wait_response_timeout_tip", ""),
("Incoming connection", ""),
("Outgoing connection", ""),
("Exit", ""),
("Open", ""),
("logout_tip", ""),
("Service", ""),
("Start", ""),
("Stop", ""),
("exceed_max_devices", ""),
("Sync with recent sessions", ""),
("Sort tags", ""),
("Open connection in new tab", ""),
("Move tab to new window", ""),
("Can not be empty", ""),
("Already exists", ""),
("Change Password", ""),
("Refresh Password", ""),
("ID", ""),
("Grid View", ""),
("List View", ""),
("Select", ""),
("Toggle Tags", ""),
("pull_ab_failed_tip", ""),
("push_ab_failed_tip", ""),
("synced_peer_readded_tip", ""),
("Change Color", ""),
("Primary Color", ""),
("HSV Color", ""),
("Installation Successful!", ""),
("Installation failed!", ""),
("Reverse mouse wheel", ""),
("{} sessions", ""),
("scam_title", ""),
("scam_text1", ""),
("scam_text2", ""),
("Don't show again", ""),
("I Agree", ""),
("Decline", ""),
("Timeout in minutes", ""),
("auto_disconnect_option_tip", ""),
("Connection failed due to inactivity", ""),
("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
("pull_group_failed_tip", ""),
("Filter by intersection", ""),
("Remove wallpaper during incoming sessions", ""),
("Test", ""),
("display_is_plugged_out_msg", ""),
("No displays", ""),
("Open in new window", ""),
("Show displays as individual windows", ""),
("Use all my displays for the remote session", ""),
("selinux_tip", ""),
("Change view", ""),
("Big tiles", ""),
("Small tiles", ""),
("List", ""),
("Virtual display", ""),
("Plug out all", ""),
("True color (4:4:4)", ""),
("Enable blocking user input", ""),
("id_input_tip", ""),
("privacy_mode_impl_mag_tip", ""),
("privacy_mode_impl_virtual_display_tip", ""),
("Enter privacy mode", ""),
("Exit privacy mode", ""),
("idd_not_support_under_win10_2004_tip", ""),
("input_source_1_tip", ""),
("input_source_2_tip", ""),
("Swap control-command key", ""),
("swap-left-right-mouse", ""),
("2FA code", ""),
("More", ""),
("enable-2fa-title", ""),
("enable-2fa-desc", ""),
("wrong-2fa-code", ""),
("enter-2fa-title", ""),
("Email verification code must be 6 characters.", ""),
("2FA code must be 6 digits.", ""),
("Multiple Windows sessions found", ""),
("Please select the session you want to connect to", ""),
("powered_by_me", ""),
("outgoing_only_desk_tip", ""),
("preset_password_warning", ""),
("Security Alert", ""),
("My address book", ""),
("Personal", ""),
("Owner", ""),
("Set shared password", ""),
("Exist in", ""),
("Read-only", ""),
("Read/Write", ""),
("Full Control", ""),
("share_warning_tip", ""),
("Everyone", ""),
("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
("default_proxy_tip", ""),
("no_audio_input_device_tip", ""),
("Incoming", ""),
("Outgoing", ""),
("Clear Wayland screen selection", ""),
("clear_Wayland_screen_selection_tip", ""),
("confirm_clear_Wayland_screen_selection_tip", ""),
("android_new_voice_call_tip", ""),
("texture_render_tip", ""),
("Use texture rendering", ""),
("Floating window", ""),
("floating_window_tip", ""),
("Keep screen on", ""),
("Never", ""),
("During controlled", ""),
("During service is on", ""),
("Capture screen using DirectX", ""),
("Back", ""),
("Apps", ""),
("Volume up", ""),
("Volume down", ""),
("Power", ""),
("Telegram bot", ""),
("enable-bot-tip", ""),
("enable-bot-desc", ""),
("cancel-2fa-confirm-tip", ""),
("cancel-bot-confirm-tip", ""),
("About RustDesk", ""),
("Send clipboard keystrokes", ""),
("network_error_tip", ""),
("Unlock with PIN", ""),
("Requires at least {} characters", ""),
("Wrong PIN", ""),
("Set PIN", ""),
("Enable trusted devices", ""),
("Manage trusted devices", ""),
("Platform", ""),
("Days remaining", ""),
("enable-trusted-devices-tip", ""),
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("identical_file_tip", "Denne fil er identisk med modpartens."),
("show_monitors_tip", "Vis skærme i værktøjsbjælken"),
("View Mode", "Visningstilstand"),
("login_linux_tip", "Du skal logge på en fjernstyret Linux konto for at aktivere en X skrivebordssession"),
("verify_rustdesk_password_tip", "Bekræft RustDesk adgangskode"),
("remember_account_tip", "Husk denne konto"),
("os_account_desk_tip", "Denne konto benyttes til at logge på fjernsystemet, og aktivere skrivebordssessionen i hovedløs tilstand"),
("OS Account", "Styresystem konto"),
("another_user_login_title_tip", "En anden bruger er allerede logget ind"),
("another_user_login_text_tip", "Frakobl"),
("xorg_not_found_title_tip", "Xorg ikke fundet"),
("xorg_not_found_text_tip", "Installér venlist Xorg"),
("no_desktop_title_tip", "Intet skrivebordsmiljø er tilgængeligt"),
("no_desktop_text_tip", "Installér venligst GNOME skrivebordet"),
("No need to elevate", "Ingen grund til at elevere"),
("System Sound", "Systemlyd"),
("Default", "Standard"),
("New RDP", "Ny RDP"),
("Fingerprint", "Fingeraftryk"),
("Copy Fingerprint", "Kopiér fingeraftryk"),
("no fingerprints", "Ingen fingeraftryk"),
("Select a peer", "Vælg en peer"),
("Select peers", "Vælg peers"),
("Plugins", "Plugins"),
("Uninstall", "Afinstallér"),
("Update", "Opdatér"),
("Enable", "Aktivér"),
("Disable", "Deaktivér"),
("Options", "Valgmuligheder"),
("resolution_original_tip", "Original skærmopløsning"),
("resolution_fit_local_tip", "Tilpas lokal skærmopløsning"),
("resolution_custom_tip", "Bruger-tilpasset skærmopløsning"),
("Collapse toolbar", "Skjul værktøjsbjælke"),
("Accept and Elevate", "Acceptér og elevér"),
("accept_and_elevate_btn_tooltip", "Acceptér forbindelsen og elevér UAC tilladelser"),
("clipboard_wait_response_timeout_tip", "Tiden for at vente på en kopieringsforespørgsel udløb"),
("Incoming connection", "Indgående forbindelse"),
("Outgoing connection", "Udgående forbindelse"),
("Exit", "Afslut"),
("Open", "Åben"),
("logout_tip", "Er du sikker på at du vil logge af?"),
("Service", "Tjeneste"),
("Start", "Start"),
("Stop", "Stop"),
("exceed_max_devices", "Du har nået det maksimale antal håndtérbare enheder."),
("Sync with recent sessions", "Synkronisér med tidligere sessioner"),
("Sort tags", "Sortér nøgleord"),
("Open connection in new tab", "Åbn forbindelse i en ny fane"),
("Move tab to new window", "Flyt fane i et nyt vindue"),
("Can not be empty", "Kan ikke være tom"),
("Already exists", "Findes allerede"),
("Change Password", "Skift adgangskode"),
("Refresh Password", "Genopfrisk adgangskode"),
("ID", "ID"),
("Grid View", "Gittervisning"),
("List View", "Listevisning"),
("Select", "Vælg"),
("Toggle Tags", "Slå nøgleord til/fra"),
("pull_ab_failed_tip", "Opdatering af adressebog mislykkedes"),
("push_ab_failed_tip", "Synkronisering af adressebog til serveren mislykkedes"),
("synced_peer_readded_tip", "Enhederne, som var til stede i de seneste sessioner, vil blive synkroniseret tilbage til adressebogen."),
("Change Color", "Skift farve"),
("Primary Color", "Primær farve"),
("HSV Color", "HSV farve"),
("Installation Successful!", "Installation fuldført!"),
("Installation failed!", "Installation mislykkedes!"),
("Reverse mouse wheel", "Invertér musehjul"),
("{} sessions", "{} sessioner"),
("scam_title", "ADVARSEL: Du kan blive SVINDLET!"),
("scam_text1", "Hvis du taler telefon med en person du IKKE kender, og IKKE stoler på, som har bedt dig om at bruge RustDesk til at forbinde til din PC, stop med det samme, og læg på omgående."),
("scam_text2", "Det er højest sandsynligvis en svinder som forsøger at stjæle dine penge eller andre personlige oplysninger."),
("Don't show again", "Vis ikke igen"),
("I Agree", "Jeg accepterer"),
("Decline", "Afvis"),
("Timeout in minutes", "Udløbstid i minutter"),
("auto_disconnect_option_tip", "Luk automatisk indkommende sessioner ved inaktivitet"),
("Connection failed due to inactivity", "Forbindelsen blev afbrudt grundet inaktivitet"),
("Check for software update on startup", "Søg efter opdateringer ved opstart"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Opgradér venligst RustDesk Server Pro til version {} eller nyere!"),
("pull_group_failed_tip", "Genindlæsning af gruppe mislykkedes"),
("Filter by intersection", "Filtrér efter intersection"),
("Remove wallpaper during incoming sessions", "Skjul baggrundsskærm ved indgående forbindelser"),
("Test", "Test"),
("display_is_plugged_out_msg", "Skærmen er slukket, skift til den første skærm."),
("No displays", "Ingen skærme"),
("Open in new window", "Åbn i et nyt vindue"),
("Show displays as individual windows", "Vis skærme som selvstændige vinduer"),
("Use all my displays for the remote session", "Brug alle mine skærme til fjernforbindelsen"),
("selinux_tip", "SELinux er aktiveret på din enhed, som kan forhindre RustDesk i at køre normalt."),
("Change view", "Skift visning"),
("Big tiles", "Store fliser"),
("Small tiles", "Små fliser"),
("List", "Liste"),
("Virtual display", "Virtuel skærm"),
("Plug out all", "Frakobl alt"),
("True color (4:4:4)", "True color (4:4:4)"),
("Enable blocking user input", "Aktivér blokering af brugerstyring"),
("id_input_tip", "Du kan indtaste ét ID, en direkte IP adresse, eller et domæne med en port (<domæne>:<port>).\nHvis du ønsker at forbinde til en enhed på en anden server, tilføj da server adressen (<id>@<server_adresse>?key=<nøgle>), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker adgang til en enhed på en offentlig server, indtast venligst \"<id>@offentlig server\", nøglen er ikke nødvendig for offentlige servere.\n\nHvis du gerne vil tvinge brugen af en relay-forbindelse på den første forbindelse, tilføj \"/r\" efter ID'et, fx, \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Tilstand 1"),
("privacy_mode_impl_virtual_display_tip", "Tilstand 2"),
("Enter privacy mode", "Start privatlivstilstand"),
("Exit privacy mode", "Afslut privatlivstilstand"),
("idd_not_support_under_win10_2004_tip", "Indirekte grafik drivere er ikke understøttet. Windows 10 version 2004 eller nyere er påkrævet."),
("input_source_1_tip", "Input kilde 1"),
("input_source_2_tip", "Input kilde 2"),
("Swap control-command key", "Byt rundt på Control & Command tasterne"),
("swap-left-right-mouse", "Byt rundt på venstre og højre musetaster"),
("2FA code", "To-faktor kode"),
("More", "Mere"),
("enable-2fa-title", "Tænd for to-faktor godkendelse"),
("enable-2fa-desc", "Åbn din godkendelsesapp nu. Du kan bruge en godkendelsesapp så som Authy, Microsoft eller Google Authenticator på din telefon eller din PC.\n\nScan QR koden med din app og indtast koden som din app fremviser, for at aktivere for to-faktor godkendelse."),
("wrong-2fa-code", "Kan ikke verificere koden. Forsikr at koden og tidsindstillingerne på enheden er korrekte"),
("enter-2fa-title", "To-faktor godkendelse"),
("Email verification code must be 6 characters.", "E-mail bekræftelseskode skal være mindst 6 tegn"),
("2FA code must be 6 digits.", "To-faktor kode skal være mindst 6 cifre"),
("Multiple Windows sessions found", "Flere Windows sessioner fundet"),
("Please select the session you want to connect to", "Vælg venligst sessionen du ønsker at forbinde til"),
("powered_by_me", "Drives af RustDesk"),
("outgoing_only_desk_tip", "Dette er en brugertilpasset udgave.\nDu kan forbinde til andre enheder, men andre enheder kan ikke forbinde til din enhed."),
("preset_password_warning", "Denne brugertilpassede udgave har en forudbestemt adgangskode. Alle der kender til denne adgangskode, kan få fuld adgang til din enhed. Hvis du ikke forventede dette, bør du afinstallere denne udgave af RustDesk med det samme."),
("Security Alert", "Sikkerhedsalarm"),
("My address book", "Min adressebog"),
("Personal", "Personlig"),
("Owner", "Ejer"),
("Set shared password", "Sæt delt adgangskode"),
("Exist in", "Findes i"),
("Read-only", "Skrivebeskyttet"),
("Read/Write", "Læse/Skrive"),
("Full Control", "Fuld kontrol"),
("share_warning_tip", "Felterne for oven er delt og synlige for andre."),
("Everyone", "Alle"),
("ab_web_console_tip", "Mere på web konsollen"),
("allow-only-conn-window-open-tip", "Tillad kun fjernforbindelser hvis RustDesk vinduet er synligt"),
("no_need_privacy_mode_no_physical_displays_tip", "Ingen fysiske skærme, ingen nødvendighed for at bruge privatlivstilstanden."),
("Follow remote cursor", "Følg musemarkør på fjernforbindelse"),
("Follow remote window focus", "Følg vinduefokus på fjernforbindelse"),
("default_proxy_tip", "Protokollen og porten som anvendes som standard er Socks5 og 1080"),
("no_audio_input_device_tip", "Ingen lydinput enhed fundet"),
("Incoming", "Indgående"),
("Outgoing", "Udgående"),
("Clear Wayland screen selection", "Ryd Wayland skærmvalg"),
("clear_Wayland_screen_selection_tip", "Efter at fravælge den valgte skærm, kan du genvælge skærmen som skal deles."),
("confirm_clear_Wayland_screen_selection_tip", "Er du sikker på at du vil fjerne Wayland skærmvalget?"),
("android_new_voice_call_tip", "Du har modtaget en ny stemmeopkaldsforespørgsel. Hvis du accepterer, vil lyden skifte til stemmekommunikation."),
("texture_render_tip", "Brug tekstur-rendering for at gøre billedkvaliteten blødere. Du kan også prøve at deaktivere denne funktion, hvis du oplever problemer."),
("Use texture rendering", "Anvend tekstur-rendering"),
("Floating window", "Svævende vindue"),
("floating_window_tip", "Det hjælper på at RustDesk baggrundstjenesten kører"),
("Keep screen on", "Hold skærmen tændt"),
("Never", "Aldrig"),
("During controlled", "Imens under kontrol"),
("During service is on", "Imens tjenesten kører"),
("Capture screen using DirectX", "Optag skærm med DirectX"),
("Back", "Tilbage"),
("Apps", "Apps"),
("Volume up", "Skru op for lyd"),
("Volume down", "Skru ned for lyd"),
("Power", "Tænd/Sluk"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Hvis du aktiverer denne funktion, kan du modtage to-faktor godkendelseskoden fra din robot. Den kan også fungere som en notifikation for forbindelsesanmodninger."),
("enable-bot-desc", "1. Åbn en chat med @BotFather.\n2. Send kommandoen \"/newbot\". Du vil modtage en nøgle efter at have gennemført dette trin.\n3. Start en chat med din nyoprettede bot. Send en besked som begynder med skråstreg \"/\", som fx \"/hello\", for at aktivere den.\n"),
("cancel-2fa-confirm-tip", "Er du sikker på at du vil afbryde to-faktor godkendelse?"),
("cancel-bot-confirm-tip", "Er du sikker på at du vil afbryde Telegram robotten?"),
("About RustDesk", "Om RustDesk"),
("Send clipboard keystrokes", "Send udklipsholder tastetryk"),
("network_error_tip", "Tjek venligst din internetforbindelse, og forsøg igen."),
("Unlock with PIN", "Lås op med PIN"),
("Requires at least {} characters", "Kræver mindst {} tegn"),
("Wrong PIN", "Forkert PIN"),
("Set PIN", "Sæt PIN"),
("Enable trusted devices", "Aktivér troværdige enheder"),
("Manage trusted devices", "Administrér troværdige enheder"),
("Platform", "Platform"),
("Days remaining", "Dage tilbage"),
("enable-trusted-devices-tip", "Spring to-faktor godkendelse over på troværdige enheder"),
("Parent directory", "mappe"),
("Resume", "Fortsæt"),
("Invalid file name", "Ugyldigt filnavn"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -548,7 +548,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"),
("Filter by intersection", "Nach Schnittmenge filtern"),
("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"),
("Test", "Test"),
("Test", "Testen"),
("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."),
("No displays", "Keine Bildschirme"),
("Open in new window", "In einem neuen Fenster öffnen"),
@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "Übergeordnetes Verzeichnis"),
("Resume", "Fortsetzen"),
("Invalid file name", "Ungültiger Dateiname"),
("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."),
("Authentication Required", "Authentifizierung erforderlich"),
("Authenticate", "Authentifizieren"),
].iter().cloned().collect();
}

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -234,5 +234,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("About RustDesk", ""),
("network_error_tip", "Please check your network connection, then click retry."),
("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"),
("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."),
].iter().cloned().collect();
}

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""),
("Resume", ""),
("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

View File

@@ -394,7 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accept sessions via click", "Aceptar sesiones a través de clic"),
("Accept sessions via both", "Aceptar sesiones a través de ambos"),
("Please wait for the remote side to accept your session request...", "Por favor, espere a que el lado remoto acepte su solicitud de sesión"),
("One-time Password", "Constaseña de un solo uso"),
("One-time Password", "Contraseña de un solo uso"),
("Use one-time password", "Usar contraseña de un solo uso"),
("One-time password length", "Longitud de la contraseña de un solo uso"),
("Request access to your device", "Solicitud de acceso a su dispositivo"),
@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "Directorio superior"),
("Resume", "Continuar"),
("Invalid file name", "Nombre de archivo no válido"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect();
}

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