Compare commits

...

491 Commits
1.2.2 ... 1.2.3

Author SHA1 Message Date
RustDesk
cf0e3ec303 Merge pull request #6024 from Mr-Update/patch-8
Update de.rs
2023-10-13 17:58:32 +08:00
Mr-Update
c6bb3d6ae2 Update de.rs 2023-10-13 11:56:14 +02:00
RustDesk
b39ba92cfe Merge pull request #6018 from 21pages/sync_share_rdp
sync option share rdp
2023-10-13 15:27:17 +08:00
RustDesk
deb1c190c9 Merge pull request #6007 from cacing69/master
improve id  translate
2023-10-13 15:26:28 +08:00
21pages
5d0384f580 sync option share rdp
Signed-off-by: 21pages <pages21@163.com>
2023-10-13 14:28:31 +08:00
Ibnul Mutaki
6e80f0d93b update readme ID 2023-10-12 22:50:53 +07:00
Ibnul Mutaki
c9a923decf improve id translate 2023-10-12 22:46:33 +07:00
RustDesk
1be5f2d647 Merge pull request #6005 from 21pages/remove_option_enable_rdp
remove option enable rdp
2023-10-12 23:43:19 +08:00
RustDesk
a38bba80ee Merge pull request #6006 from fufesou/fix/linux_window_ensure_setOpacity_after_show
Fix window hide sometimes
2023-10-12 23:42:01 +08:00
fufesou
b15d84359b https://github.com/rustdesk/rustdesk/issues/5986
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-12 23:11:11 +08:00
21pages
a3670b731e remove option enable rdp
Signed-off-by: 21pages <pages21@163.com>
2023-10-12 21:44:04 +08:00
RustDesk
b89546de37 Prefer "Key" not translated. 2023-10-12 20:54:10 +08:00
RustDesk
fad4314538 Merge pull request #6003 from Kleofass/patch-2
Update lv.rs
2023-10-12 20:53:03 +08:00
Kleofass
a331961ef3 Update lv.rs 2023-10-12 15:13:23 +03:00
RustDesk
e6f62dc95e Merge pull request #5997 from solokot/master
Update ru.rs
2023-10-12 15:24:29 +08:00
solokot
bd6d863921 Update ru.rs 2023-10-12 10:12:50 +03:00
RustDesk
c11117b070 Merge pull request #5994 from bovirus/master
Add files via upload
2023-10-12 10:55:59 +08:00
bovirus
f387ccb9e4 Add files via upload 2023-10-11 19:32:57 +02:00
RustDesk
fd49830c35 Merge pull request #5987 from AnonymousWP/translation/dutch
fix(translation/Dutch): use formal translation
2023-10-11 18:16:43 +08:00
AnonymousWP
578ca6975f fix(translation/Dutch): use formal translation
Related: https://github.com/rustdesk/rustdesk/discussions/5948
2023-10-11 12:15:14 +02:00
RustDesk
c0442edb8d Merge pull request #5985 from 21pages/tag_filter_method
add option filter ab by intersection
2023-10-11 17:45:53 +08:00
21pages
1416197b62 add option filter ab by intersection
Signed-off-by: 21pages <pages21@163.com>
2023-10-11 16:50:48 +08:00
RustDesk
e1dd53f146 Merge pull request #5970 from bovirus/master
Update Italian language
2023-10-11 11:47:55 +08:00
RustDesk
73abd0f8b8 Merge pull request #5973 from sahilyeole/fix/scam_warning
Fix scam warning overflow
2023-10-11 11:47:34 +08:00
Sahil Yeole
ab982e86c3 fix scam alert buttons overflow
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-10-10 22:48:23 +05:30
Sahil Yeole
20a4cd49de update scam text height
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-10-10 21:32:05 +05:30
Sahil Yeole
387d712b67 make scam alert text scrollable
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-10-10 20:58:58 +05:30
bovirus
3d9ec91b35 Update Italian language 2023-10-10 15:52:55 +02:00
rustdesk
8892c8c883 remove doc_mac_permission from tempate 2023-10-10 11:53:27 +08:00
RustDesk
c51c98d682 Merge pull request #5959 from Akkowicz/master
Improve wording, add missing tip and documentation link [PL]
2023-10-10 11:50:57 +08:00
Mateusz Prais
3f4ac84cfb Improve wording, add missing tip and documentation link 2023-10-09 18:34:32 +02:00
RustDesk
c5a864e86b Merge pull request #5947 from AnonymousWP/translation/dutch
feat(translation): add and fix Dutch strings
2023-10-09 14:13:27 +08:00
RustDesk
e945dceab9 Merge pull request #5954 from dignow/fix/capitalization_styles
fix, uniform capitalization style
2023-10-09 14:12:22 +08:00
dignow
f176832851 fix, uniform capitalization style, capitalize the first letter of a phrase
Signed-off-by: dignow <linlong1265@gmail.com>
2023-10-09 14:06:26 +08:00
AnonymousWP
ab195ea520 feat(translation): add and fix Dutch strings 2023-10-08 23:34:41 +02:00
RustDesk
e4b861e766 Merge pull request #5944 from leroyloren/master
Update cs.rs
2023-10-08 19:48:04 +08:00
leroyloren
ead6d8d3a1 Update cs.rs 2023-10-08 13:46:04 +02:00
RustDesk
c14cb29334 Merge pull request #5936 from hms5232/fix-term-in-tw
fix wrong terms in tw lang
2023-10-07 14:14:28 +08:00
hms5232
a233d28efc fix wrong terms in tw lang
quality is "品質" in Taiwan, the "質量" is used in China.
2023-10-07 14:11:43 +08:00
RustDesk
c7bc2ca82d Merge pull request #5935 from fufesou/fix/xwayland_server_envs
Fix/xwayland server envs
2023-10-07 13:48:56 +08:00
fufesou
bd6323ccae fix, xwayland server env, check xwayland running
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-07 00:40:16 -05:00
fufesou
4326bfa504 fix, xwayland server envs, check Xwayland
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-07 00:40:16 -05:00
fufesou
b7a4c0664b fix, wayland --server, display env, better way to find the envs
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-07 00:40:16 -05:00
fufesou
da7bcf89d4 fix, wayland --server, envs
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-07 00:40:16 -05:00
RustDesk
3907cc679a Merge pull request #5932 from fufesou/fix/mobile_cursor_offset
fix, mobile curosr (hotx,hoty)
2023-10-06 23:49:05 +08:00
fufesou
8cc5aee528 fix, mobile curosr (hotx,hoty)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-06 23:36:58 +08:00
RustDesk
d0b5c4de28 Merge pull request #5925 from RayJW/bump-flatpak-runtime
Update Freedesktop runtime to 23.08 for Flatpak.
2023-10-06 19:59:11 +08:00
RayJW
431150c262 Bumped fd runtime in CI 2023-10-06 11:28:07 +02:00
RayJW
ee994ea393 Bumped fd runtime 2023-10-06 11:22:14 +02:00
RustDesk
53cd259ffa Merge pull request #5917 from bankzst/feature/update-thai-language-fields
lang: (Thai) add new words and update existing words to up-to-date
2023-10-05 21:43:09 +08:00
Phongsathorn Sae-Ung (X10)
c0ed44abf9 fix: add new words and update existing words to up-to-date 2023-10-05 18:19:35 +07:00
RustDesk
0807eec4cc Merge pull request #5907 from Kleofass/patch-1
Update lv.rs
2023-10-05 11:16:47 +08:00
Kleofass
aec7271f50 Update lv.rs 2023-10-04 15:07:57 +03:00
RustDesk
a723518346 Merge pull request #5901 from fufesou/fix/cursor_hotxy_mismatch
fix, cursor (hotx,hoty) mismatch sometimes
2023-10-03 23:37:45 +08:00
fufesou
5d6d8e68ed Update cursor id immediately after the cursor event
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-03 22:15:58 +08:00
fufesou
197a9330df fix, cursor hotxy, use _id
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-03 21:18:53 +08:00
fufesou
8b807d7b50 fix, cursor (hotx,hoty) mismatch sometimes
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-03 21:16:12 +08:00
RustDesk
03c0111017 Merge pull request #5891 from fufesou/refact/flutter_sessions_lock
refact, flutter sessions lock
2023-10-03 13:19:07 +08:00
RustDesk
e55752869b Merge pull request #5890 from 21pages/mobile_peer_actions
dynamically display mobile peer tab actions
2023-10-03 13:14:49 +08:00
RustDesk
94830cffca Merge pull request #5887 from sahilyeole/fix/wayland_screen_share_prompt
Fix wayland screen prompt on display menu
2023-10-03 13:13:52 +08:00
RustDesk
8d4319ba5f Merge pull request #5886 from enoch85/patch-1
Update sv.rs
2023-10-03 13:12:39 +08:00
fufesou
06987c4ca9 refact, flutter sessions lock
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-03 09:51:21 +08:00
21pages
70c0edcbe7 mobile peer tab right actions dropdown
Signed-off-by: 21pages <pages21@163.com>
2023-10-03 09:27:35 +08:00
Sahil Yeole
7a482fd22a fix wayland screen prompt on display menu
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-10-02 21:22:40 +05:30
Daniel Hansson
022b8ec13a Update sv.rs 2023-10-02 17:48:09 +02:00
RustDesk
b02f169764 Merge pull request #5884 from fufesou/fix/sciter_reconnect_state
fix, sciter, reconnect, check thread running before the state
2023-10-02 20:40:15 +08:00
RustDesk
6c0254b5f4 Merge pull request #5880 from fufesou/fix/touch_mode_touble_tap
Fix/touch mode touble tap
2023-10-02 20:35:44 +08:00
fufesou
0a60d7016d fix, sciter, reconnect, check thread running before the state
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-02 20:17:43 +08:00
fufesou
315a2a695f add comments
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-02 12:17:11 +08:00
fufesou
863c8de28e fix, one tap results double tap event
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-02 10:30:51 +08:00
RustDesk
92213f9228 Merge pull request #5877 from leroyloren/master
Update cs.rs
2023-10-02 08:05:47 +08:00
leroyloren
253c8118a2 Update cs.rs 2023-10-01 18:54:47 +02:00
RustDesk
290c980d5f Merge pull request #5875 from 21pages/remove_translation
remove useless translations
2023-10-01 13:10:06 +08:00
21pages
6ead1f4bd9 remove useless translations
Signed-off-by: 21pages <pages21@163.com>
2023-10-01 13:04:47 +08:00
RustDesk
6e4a5b64b7 Merge pull request #5874 from fufesou/refact/remove_cursor_mobile_2_mobile
refact, remove cursor if mobile -- mobile
2023-10-01 12:56:55 +08:00
RustDesk
6e3e60a44d Merge pull request #5873 from 21pages/cm_width
opt cm width and elevation requires keyboard permission
2023-10-01 12:49:20 +08:00
fufesou
b88fafe5ff refact, remove cursor if mobile -- mobile
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-10-01 12:16:06 +08:00
21pages
17a56bbf48 elevation: keyboard permission required and remove foreground filter
Signed-off-by: 21pages <pages21@163.com>
2023-10-01 08:50:29 +08:00
21pages
d8e51c6b14 render cm side page after window size change, calculate real window
width

Signed-off-by: 21pages <pages21@163.com>
2023-10-01 08:50:24 +08:00
RustDesk
44554cb54b Merge pull request #5868 from dignow/fix/reconnect_potential_deadlock
fix, reconnect deadlock, introduce connection round control
2023-09-30 22:26:57 +08:00
dignow
0bd86a8211 remove warn
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-30 22:09:32 +08:00
dignow
7fcb3d70bb fix, reconnect deadlock, introduce connection round control
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-30 22:07:14 +08:00
RustDesk
563cd828ad Merge pull request #5865 from dignow/fix/dialog_button_reconnect_cancel
Fix/dialog button reconnect cancel
2023-09-30 15:38:49 +08:00
dignow
3581e0beed Add comment
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-30 14:21:11 +08:00
RustDesk
da04de7b2d Merge pull request #5866 from 21pages/fix_reconnect_elevate_menu
fix request elevation menu not displayed when reconnect
2023-09-30 13:36:59 +08:00
21pages
661ce29519 fix request elevation menu not displayed when reconnect
Signed-off-by: 21pages <pages21@163.com>
2023-09-30 11:22:18 +08:00
dignow
56bed3f297 Update flutter pub lock
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-30 11:14:31 +08:00
dignow
79a8715c8b fix, dialog button reconnect cancel, change the interval and try count
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-29 22:53:58 +08:00
dignow
69062dca16 fix, dialog button, reconnect cancel in a short time
1. Two `reconnect` is called.
2. The window cannot be closed.

Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-29 21:42:49 +08:00
RustDesk
40d3085cc2 Merge pull request #5856 from 21pages/fix
show rdp menu only windows to windows
2023-09-29 13:09:13 +08:00
21pages
5e444de031 show rdp menu only windows to windows
Signed-off-by: 21pages <pages21@163.com>
2023-09-29 08:42:06 +08:00
RustDesk
3e61b89499 Merge pull request #5847 from 21pages/tab_label
try fix remote tab label update
2023-09-28 10:39:21 +08:00
RustDesk
76e9d749c9 Merge pull request #5846 from dignow/fix/cm_window_await_call_order
fix: cm await call
2023-09-28 10:39:07 +08:00
RustDesk
3e47c352a3 Merge pull request #5844 from fetzu/lang-fr
Update fr.rs
2023-09-28 10:38:10 +08:00
21pages
6028cfc1a3 fix remote tab lable update, Get.find always return the first instance
Signed-off-by: 21pages <pages21@163.com>
2023-09-28 09:05:10 +08:00
Julien
20f1a85e69 Update fr.rs
+ Added French translation for new (and some old) strings.
2023-09-27 23:02:49 +02:00
dignow
00d3d0f094 fix: cm await call
1. Cm may crash in wrong order.
2. The window may be at the left top.
3. The window may be wrong size.

Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-27 23:51:27 +08:00
RustDesk
8756ae0fe6 Merge pull request #5841 from 21pages/av1_auto_codec
Give higher priority to AV1 over VP9 in the auto codec
2023-09-27 18:50:18 +08:00
21pages
bdb1fc2ed7 Give higher priority to AV1 over VP9 in the auto codec
Signed-off-by: 21pages <pages21@163.com>
2023-09-27 18:42:57 +08:00
RustDesk
34d64fbcaf Merge pull request #5829 from 21pages/scrollbar
desktop touch pad scroll
2023-09-27 10:53:55 +08:00
21pages
3eeb0628f5 adjust scrollbar style, peers view support touch pad scroll, settings tab
remove horizontal touch pad scroll

Signed-off-by: 21pages <pages21@163.com>
2023-09-27 10:25:49 +08:00
RustDesk
3ed71fa21e Merge pull request #5832 from dignow/fix/await_calling_order
use await to guarantee the calling order
2023-09-27 10:03:41 +08:00
dignow
731ecfda64 use await to guarantee the calling order
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-27 09:16:51 +08:00
RustDesk
230eb76532 Merge pull request #5822 from 21pages/me
show username followed by a 'Me' tag
2023-09-26 18:18:01 +08:00
21pages
93f2b288b5 show username followed by a 'Me' tag, use first letter as avatar
Signed-off-by: 21pages <pages21@163.com>
2023-09-26 18:01:33 +08:00
RustDesk
d9ee9ba238 Merge pull request #5821 from 21pages/tab_label
desktop tab lable format: id/alias@hostname
2023-09-26 15:26:31 +08:00
21pages
e000fdfb50 desktop tab lable format: id/alias@hostname
Signed-off-by: 21pages <pages21@163.com>
2023-09-26 15:11:31 +08:00
rustdesk
4e97d2503b chore 2023-09-26 11:02:52 +08:00
rustdesk
f21f793343 remove check update from periodical call 2023-09-26 10:47:45 +08:00
rustdesk
c470f2734d revert pub lock to old, because the shit flutter bridge does not work 2023-09-26 10:41:29 +08:00
rustdesk
28ddf6cf07 fix a potential crash 2023-09-26 10:26:42 +08:00
RustDesk
e0302d1f09 Merge pull request #5816 from solokot/master
Update ru.rs
2023-09-25 22:56:53 +08:00
RustDesk
a536f79f6d Merge pull request #5815 from flusheDData/master
Update es.rs
2023-09-25 22:56:24 +08:00
RustDesk
5c48fb9e66 Merge pull request #5814 from BestiaPL/master
Update pl.rs
2023-09-25 22:56:06 +08:00
rustdesk
994ba1edd9 debug=true does not work, update pub 2023-09-25 22:54:00 +08:00
solokot
ed90979417 Update ru.rs 2023-09-25 17:47:41 +03:00
flusheDData
4dc2172426 Update es.rs
New term added
2023-09-25 16:31:05 +02:00
Andrzej Rudnik
daa41f8664 Update pl.rs 2023-09-25 16:29:19 +02:00
RustDesk
4e93ffb924 Merge pull request #5812 from 21pages/desktop_single_scroll
desktop only one scrollbar
2023-09-25 21:33:01 +08:00
rustdesk
f29363f56d debug=true for debug 2023-09-25 21:31:33 +08:00
21pages
23680ccb14 replace desktop outer scroll view with Column, so that there will be
only one scrollbar

Signed-off-by: 21pages <pages21@163.com>
2023-09-25 21:04:40 +08:00
RustDesk
986e58aeec Merge pull request #5809 from sahilyeole/fix/wayland_server_start_late
Fix wayland --server starting late
2023-09-25 19:53:21 +08:00
Sahil Yeole
13da75c2b6 fix wayland server starting late
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-25 16:53:34 +05:30
RustDesk
28b28cfef6 Merge pull request #5808 from 21pages/mobile
mobile scrollable peers view
2023-09-25 16:37:39 +08:00
21pages
167bf70cd6 mobile: limited height scroll tags/users, and scrollable peers
Signed-off-by: 21pages <pages21@163.com>
2023-09-25 16:18:01 +08:00
21pages
5236dcfe52 peers view show no more than 1000 after filter
Signed-off-by: 21pages <pages21@163.com>
2023-09-25 13:41:23 +08:00
21pages
a437524c8f await loadCache
Signed-off-by: 21pages <pages21@163.com>
2023-09-25 13:41:23 +08:00
RustDesk
fcbd48648c Merge pull request #5803 from Mr-Update/patch-7
Update de.rs
2023-09-25 11:03:47 +08:00
RustDesk
3f29273f6e Merge pull request #5805 from cacing69/master
update id
2023-09-25 10:57:25 +08:00
RustDesk
b78ffdad02 Merge pull request #5799 from fufesou/fix/wayland_alt_tab
fix, alt + tab, switch window, release alt state
2023-09-25 10:57:09 +08:00
Ibnul Mutaki
fd4cd3ed04 update id 2023-09-25 08:38:58 +07:00
Mr-Update
d8001fcaea Update de.rs 2023-09-24 21:06:40 +02:00
fufesou
9771c652c5 fix, alt + tab, switch window, release alt state
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-25 00:15:36 +08:00
RustDesk
a0c7bbe213 Merge pull request #5798 from 21pages/mobile
mobile still use no scrollable widgets
2023-09-24 20:51:09 +08:00
21pages
6b43042828 mobile still use no scrollable widgets
Signed-off-by: 21pages <pages21@163.com>
2023-09-24 19:54:11 +08:00
RustDesk
73f0b1e8a3 Merge pull request #5796 from bovirus/master
Update Italian language
2023-09-24 16:55:43 +08:00
bovirus
fa1b61b3e3 Update Italian language 2023-09-24 10:54:29 +02:00
RustDesk
c116e94cba Merge pull request #5795 from Kleofass/master
Update lv.rs
2023-09-24 16:20:22 +08:00
Kleofass
7229652e31 Update lv.rs 2023-09-24 10:52:03 +03:00
RustDesk
03acf7a05c Merge pull request #5793 from 21pages/fix
remove ab pull error toast, translate group pull error
2023-09-24 11:14:03 +08:00
21pages
d83d8c18fc remove ab pull error toast, translate group pull error
Signed-off-by: 21pages <pages21@163.com>
2023-09-24 09:00:24 +08:00
RustDesk
2080e56f87 Merge pull request #5788 from 21pages/fix
fix file transfer affect codec selection
2023-09-23 23:45:17 +08:00
21pages
90f0f27fca fix file transfer affect codec selection
Signed-off-by: 21pages <pages21@163.com>
2023-09-23 21:09:24 +08:00
RustDesk
5837026e83 Merge pull request #5773 from sahilyeole/fix/wayland_cursor_mismatch
Fix wayland cursor mismatch on multiple screens
2023-09-23 16:08:34 +08:00
RustDesk
edc67e5da2 Merge pull request #5785 from 21pages/user_search
search user TextField horizontal align and case insensitive
2023-09-23 12:23:24 +08:00
21pages
55b43f4612 user search TextField horizontal align and case insensitive
Signed-off-by: 21pages <pages21@163.com>
2023-09-23 10:03:09 +08:00
RustDesk
32a32e4a72 Update README.md 2023-09-21 19:06:04 +08:00
RustDesk
0eae0da781 Merge pull request #5771 from 21pages/ab
merge info from group when add id to addressbook
2023-09-21 16:37:10 +08:00
21pages
51b62ea467 merge info from group when add id to addressbook
Signed-off-by: 21pages <pages21@163.com>
2023-09-21 16:34:04 +08:00
rustdesk
462fa5999f update window_manager pub for cm now shown sometings on linux 2023-09-21 16:01:43 +08:00
RustDesk
82b730c5b8 Merge pull request #5769 from solokot/master
Update ru.rs
2023-09-21 14:13:56 +08:00
solokot
492ea7264a Update ru.rs 2023-09-21 09:11:53 +03:00
RustDesk
a1c910e3aa Merge pull request #5762 from 21pages/scroll
tags and users add scrollbar
2023-09-21 12:03:16 +08:00
Sahil Yeole
bf5f58e0ce fix wayland cursor mismatch
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-20 19:55:13 +05:30
21pages
428bc9b419 group users add scroll bar
Signed-off-by: 21pages <pages21@163.com>
2023-09-20 17:47:33 +08:00
21pages
a2742caa87 tags add scroll bar
Signed-off-by: 21pages <pages21@163.com>
2023-09-20 17:45:38 +08:00
RustDesk
38f8956bd0 Merge pull request #5758 from BestiaPL/master
Update pl.rs
2023-09-20 10:57:45 +08:00
RustDesk
ec8deab454 Merge pull request #5757 from Mr-Update/patch-6
Update de.rs
2023-09-20 10:57:06 +08:00
RustDesk
cdc92fc552 Merge pull request #5754 from dignow/fix/scale_adaptive_blurry
fix, scale adaptive blurry
2023-09-20 10:53:44 +08:00
Andrzej Rudnik
53fb48fe7d Update pl.rs 2023-09-19 23:12:47 +02:00
Mr-Update
73e4006447 Update de.rs 2023-09-19 22:15:50 +02:00
dignow
c2ff269b5f fix, scale adaptive blurry
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-19 23:49:17 +08:00
RustDesk
c7310b64ad Merge pull request #5750 from flusheDData/master
Update es.rs
2023-09-19 23:16:47 +08:00
RustDesk
83daa702f9 Merge pull request #5752 from dignow/fix/restore_texture_offset_linux
fix, set texture widget offset to int on linux
2023-09-19 23:15:50 +08:00
dignow
cb1a06270e fix, set texture widget offset to int on linux
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-19 22:57:53 +08:00
Miguel F. G
fd34f97120 Update es.rs
new term added
2023-09-19 16:21:31 +02:00
Miguel F. G
2dc8f3b3e4 Update es.rs
New terms added
2023-09-19 16:18:21 +02:00
RustDesk
7e5b81ff4d Merge pull request #5749 from dignow/fix/texture_widget_blurry_image
fix, texture render widget, blurry image
2023-09-19 22:03:51 +08:00
RustDesk
f5b945c09b Merge pull request #5748 from 21pages/group
fix group read peers
2023-09-19 22:02:04 +08:00
dignow
6014dd05a0 fix, texture render widget, blurry image
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-19 21:32:50 +08:00
21pages
0c6aa381c5 replace Wrap with builder in PeerView for efficiency
Signed-off-by: 21pages <pages21@163.com>
2023-09-19 20:33:35 +08:00
21pages
0246f050e2 fix group peers read
Signed-off-by: 21pages <pages21@163.com>
2023-09-19 16:18:59 +08:00
RustDesk
5f222c4df2 Merge pull request #5746 from cacing69/master
update id.rs
2023-09-19 09:03:06 +08:00
Ibnul Mutaki
800f3f765f upadte ud.rs 2023-09-19 07:59:07 +07:00
Ibnul Mutaki
635f346b12 update id.rs 2023-09-19 07:52:25 +07:00
RustDesk
abe79dbf64 Merge pull request #5737 from fufesou/refact/details_on_privacy_mode
Fix, privacy on, missing failed details
2023-09-18 22:23:34 +08:00
fufesou
2b72622fe8 Fix, privacy on, missing failed details
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-18 22:12:11 +08:00
rustdesk
201c7a7e49 e51fddf7f3 2023-09-18 22:03:37 +08:00
RustDesk
ef21283a61 Merge pull request #5733 from Kleofass/patch-1
Update lv.rs
2023-09-18 21:00:14 +08:00
Kleofass
4eb76cdc30 Update lv.rs 2023-09-18 15:08:42 +03:00
RustDesk
3b68f598b1 Merge pull request #5730 from 21pages/mac_tray
remove mac tray session count tooltip
2023-09-18 17:01:41 +08:00
RustDesk
31db43dbb0 Merge pull request #5731 from bovirus/master
Update Italian language
2023-09-18 17:01:23 +08:00
RustDesk
1efc4a03cc Merge pull request #5729 from borondics/change-wording
Change-wording
2023-09-18 17:00:58 +08:00
bovirus
607c818879 Update Italian language 2023-09-18 10:52:11 +02:00
borondics
6d5f044948 Changed "unremember" to "forget" everywhere. It sounds better, shorter and it is also used for variable names as 'forget'. 2023-09-18 10:09:06 +02:00
borondics
f24a8b3918 I suggest to change "unremember" to "forget", just like it is in the code. 2023-09-18 10:00:52 +02:00
RustDesk
2e402098a2 Merge pull request #5728 from 21pages/group
fix upgrade prompt condition
2023-09-18 15:49:01 +08:00
21pages
3389c798f6 remove mac tray session count tooltip, which won't be shown
Signed-off-by: 21pages <pages21@163.com>
2023-09-18 14:49:51 +08:00
21pages
738ad474c1 fix upgrade prompt condition
Signed-off-by: 21pages <pages21@163.com>
2023-09-18 14:36:55 +08:00
RustDesk
86e2ac1497 Merge pull request #5720 from 21pages/group
enable group
2023-09-18 14:16:28 +08:00
21pages
b2a4f11e0b enable group, show accessible users and peers
Signed-off-by: 21pages <pages21@163.com>
2023-09-18 13:44:41 +08:00
21pages
09d380ba8f allow hide peer tab
Signed-off-by: 21pages <pages21@163.com>
2023-09-18 09:12:58 +08:00
RustDesk
12bfa72f31 Merge pull request #5724 from dignow/fix/try_fix_build_ios
try fix build, ios
2023-09-17 23:55:30 +08:00
dignow
b0990ac6ec try fix build, ios
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-17 23:39:54 +08:00
rustdesk
6f1f07a1c4 update lock for rustdesk_desktop_multi_window 2023-09-17 22:58:45 +08:00
RustDesk
f55fdae9eb Merge pull request #5721 from dignow/fix/adjust_window_state
Change the maximized state on 'Adjust Window'
2023-09-17 22:54:13 +08:00
dignow
b0225880de Change the maximized state on 'Adjust Window'
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-17 19:49:02 +08:00
RustDesk
74be6af3e6 Merge pull request #5719 from fufesou/fix/remove_nested_read_calls
Refactor/remove nested read calls
2023-09-17 16:30:49 +08:00
fufesou
72f5fbd6ad fix build sciter
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-17 13:41:00 +08:00
fufesou
ae3efa1151 fix, RwLock, remove nested read calls
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-17 13:01:27 +08:00
RustDesk
348ed268c3 Merge pull request #5716 from fufesou/refact/privacy_mode_msgbox_details
Refact/privacy mode msgbox details
2023-09-17 11:23:24 +08:00
fufesou
df19ccf998 Set details for block input back notification
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-17 11:07:06 +08:00
fufesou
db2e4f30a7 Privacy mode, msgbox, add details
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-17 10:53:04 +08:00
RustDesk
a1e1f5aab6 Merge pull request #5715 from fufesou/fix/crash_macos_on_disconnect
fix, macos as the controlled side, crash on disconnect
2023-09-17 10:35:05 +08:00
fufesou
7ffe11b000 fix, macos as the controlled side, crash on disconnect
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-17 09:17:51 +08:00
RustDesk
eb5ea17610 Merge pull request #5713 from bovirus/master
Update italian language
2023-09-16 21:53:52 +08:00
bovirus
8657381dce Update italian language 2023-09-16 15:48:00 +02:00
RustDesk
55114082e3 Merge pull request #5710 from NicKoehler/manager-columns
Changed File Manager column behaviour
2023-09-16 18:53:31 +08:00
RustDesk
64051e9cfa Merge pull request #5709 from Kleofass/master
Update latvian lv.rs
2023-09-16 18:52:24 +08:00
NicKoehler
780d64a349 refactor functions 2023-09-16 12:28:00 +02:00
NicKoehler
bcd1827d8a Changed columns and window behaviour when resized 2023-09-16 11:54:17 +02:00
Kleofass
0097f5fc8d Update lv.rs 2023-09-16 12:42:03 +03:00
RustDesk
3766d2b97b Update vcpkg-deps-linux.yml 2023-09-16 15:04:06 +08:00
RustDesk
f2b7bfc561 Merge pull request #5706 from Mr-Update/patch-5
Update de.rs
2023-09-16 11:11:50 +08:00
RustDesk
4d3484002d Merge pull request #5708 from cacing69/master
update indo translation
2023-09-16 11:04:25 +08:00
Ibnul Mutaki
34f20f914a update indo translation 2023-09-16 08:36:39 +07:00
Mr-Update
19444353b4 Update de.rs 2023-09-15 21:36:08 +02:00
RustDesk
b4f0a13779 Merge pull request #5705 from Kleofass/master
Update latvian lv.rs
2023-09-15 22:58:21 +08:00
Kleofass
fa815af798 Update latvian lv.rs 2023-09-15 14:58:37 +03:00
Kleofass
f1e2aa8c96 Update latvian lang.rs 2023-09-15 14:58:10 +03:00
RustDesk
9fd24db257 Merge pull request #5704 from fufesou/fix/update_rdev
Fix/update rdev
2023-09-15 17:25:02 +08:00
fufesou
73414f2ee1 simple remove parentheses
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-15 15:38:42 +08:00
fufesou
7b37e5183c update rdev, fix grab system utf8, fallback on linux
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-15 15:35:44 +08:00
RustDesk
bb8438c770 Merge pull request #5702 from solokot/master
Update ru.rs
2023-09-15 14:34:11 +08:00
solokot
3dd329a999 Update ru.rs 2023-09-15 08:37:08 +03:00
RustDesk
ab2672777e Merge pull request #5700 from Integral-Tech/master
Update cn.rs
2023-09-15 12:04:27 +08:00
Integral
45aea4176f Update cn.rs 2023-09-15 11:56:04 +08:00
RustDesk
f8dfbbb0b9 Merge pull request #5697 from solokot/master
Update ru.rs
2023-09-15 09:59:15 +08:00
RustDesk
48a348c4d1 Merge branch 'master' into master 2023-09-15 09:25:01 +08:00
RustDesk
870ff56629 Merge pull request #5699 from cacing69/master
improve indonesia translation
2023-09-15 09:23:57 +08:00
RustDesk
b2ed8d8560 Merge branch 'master' into master 2023-09-15 09:23:49 +08:00
RustDesk
e8510ddc58 Merge pull request #5696 from sahilyeole/feat/optional_update_check
Feat optional update check and closable update card
2023-09-15 09:20:00 +08:00
Ibnul Mutaki
97f7575409 improve indo translation 2023-09-15 07:44:25 +07:00
Ibnul Mutaki
5220157b01 add indonesia translation 2023-09-15 07:42:45 +07:00
solokot
900f9ec9a9 Update ru.rs 2023-09-14 23:53:05 +03:00
Sahil Yeole
52ec2c2538 add lang
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-14 23:57:17 +05:30
Sahil Yeole
31101221e0 feat closable update card
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-14 20:41:25 +05:30
Sahil Yeole
ae37c2ab2a update checkbox text
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-14 19:02:53 +05:30
Sahil Yeole
e8d014d80d feat optional update check
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-14 16:31:17 +05:30
Sahil Yeole
b88f1dc79a fix flutter check for update
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-14 14:26:09 +05:30
Sahil Yeole
dccc791c99 Merge branch 'rustdesk:master' into feat/optional_update_check 2023-09-14 14:00:25 +05:30
Sahil Yeole
474d2402b7 add checkbox for update check on startup
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-14 13:57:58 +05:30
RustDesk
7a9f1e9c6c Update vcpkg-deps-linux.yml 2023-09-14 12:03:14 +08:00
RustDesk
98364e83b6 Update vcpkg-deps-linux.yml 2023-09-14 11:32:58 +08:00
RustDesk
bf65f033dd Merge pull request #5684 from Mr-Update/patch-4
Update de.rs
2023-09-14 09:34:15 +08:00
RustDesk
2d9b430327 Merge pull request #5685 from BestiaPL/master
Update pl.rs
2023-09-14 09:33:54 +08:00
RustDesk
ec54443674 Update pl.rs 2023-09-14 09:33:25 +08:00
Andrzej Rudnik
0fb84ccc49 Update pl.rs 2023-09-13 21:10:19 +02:00
Mr-Update
0b4aad4931 Update de.rs 2023-09-13 20:44:32 +02:00
RustDesk
ab470d4a4c Merge pull request #5682 from dignow/fix/scrollbar_thickness
fix, set scrollbar thickness to 12
2023-09-13 21:37:39 +08:00
dignow
88e1245d10 fix, set scrollbar thickness to 12
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-13 21:02:21 +08:00
RustDesk
bc4dbd6194 Merge pull request #5681 from bovirus/master
Update Italian language
2023-09-13 20:05:32 +08:00
bovirus
b35df54c81 Update Italian language 2023-09-13 13:59:38 +02:00
RustDesk
c53c7b1aaa Merge pull request #5674 from 21pages/auto_disconnect
auto disconnect
2023-09-13 14:04:32 +08:00
21pages
0a0653358c auto disconnect
Signed-off-by: 21pages <pages21@163.com>
2023-09-13 13:57:57 +08:00
rustdesk
e2ccaf2c91 allow hide cm for self-host pro 2023-09-13 13:54:02 +08:00
RustDesk
75f5212661 Merge pull request #5664 from sahilyeole/add/scam_warning
Add mobile scam warning window
2023-09-13 13:41:22 +08:00
Sahil Yeole
1e548af987 update lang
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-13 11:00:07 +05:30
RustDesk
2c2c828b47 Merge pull request #5659 from 21pages/fix
install service period protection
2023-09-13 13:22:21 +08:00
Sahil Yeole
19e49a7de7 Merge branch 'rustdesk:master' into add/scam_warning 2023-09-13 10:42:08 +05:30
Sahil Yeole
18c9ad25be remove lang
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-13 10:41:48 +05:30
RustDesk
9e3f0304de Merge pull request #5654 from 21pages/tray
translate placeholder && tray tooltip and pull up
2023-09-13 13:10:37 +08:00
21pages
55dbcb646b windows remove "--cm-no-ui", "--cm --hide"
Signed-off-by: 21pages <pages21@163.com>
2023-09-13 12:14:27 +08:00
21pages
db0ab2e4a9 try start tray when a new controlled connection is established for windows flutter
Signed-off-by: 21pages <pages21@163.com>
2023-09-13 12:14:27 +08:00
21pages
c254eebea2 windows/mac tray tooltip show controlled session count
* ubuntu 22.04 can't see tooltip

Signed-off-by: 21pages <pages21@163.com>
2023-09-13 12:14:25 +08:00
21pages
45b0e7dc01 translate placeholders ui:{value}, translation: {}
Signed-off-by: 21pages <pages21@163.com>
2023-09-13 12:13:18 +08:00
RustDesk
4c4c62c7e5 Update server_page.dart
change countdown to 12
2023-09-13 10:19:43 +08:00
RustDesk
cb2038442c Merge pull request #5666 from BestiaPL/master
Update pl.rs
2023-09-13 09:56:54 +08:00
RustDesk
1d219eb8f2 Merge pull request #5669 from cacing69/master
update text indo
2023-09-13 09:56:26 +08:00
RustDesk
7d33563010 Merge pull request #5656 from Mr-Update/patch-3
Update de.rs
2023-09-13 09:52:00 +08:00
Ibnul Mutaki
e3d34c46c7 update text indo 2023-09-12 13:08:19 +07:00
Andrzej Rudnik
9129e82804 Update pl.rs 2023-09-11 20:54:33 +02:00
Sahil Yeole
d4f4a64937 remove empty line 2023-09-11 21:57:45 +05:30
Sahil Yeole
2b4a51ba24 Add scam image 2023-09-11 21:55:06 +05:30
Sahil Yeole
6a8d755b27 add scam warning lang
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-11 21:53:57 +05:30
Sahil Yeole
754fea538a add scam warning window
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-11 21:48:36 +05:30
21pages
4d0b660c73 fix lan option reaction
Signed-off-by: 21pages <pages21@163.com>
2023-09-11 16:42:01 +08:00
21pages
d0173fbdc5 install service period protection
how to reproduce:
install, click stop service, click start service and click no
on uac, it'll show "Service is not running" but can be connected.

Signed-off-by: 21pages <pages21@163.com>
2023-09-11 16:04:51 +08:00
Mr-Update
62ae4aeac9 Update de.rs 2023-09-11 08:45:49 +02:00
21pages
e98aa81794 remove sciter keep cm open for file log
Signed-off-by: 21pages <pages21@163.com>
2023-09-11 11:23:00 +08:00
21pages
a5dcac137f fix dark theme error banner background color
Signed-off-by: 21pages <pages21@163.com>
2023-09-11 11:18:26 +08:00
RustDesk
96215d32b7 Merge pull request #5651 from flusheDData/master
Update es.rs
2023-09-11 08:43:57 +08:00
RustDesk
4a20989028 Merge pull request #5652 from bovirus/master
Update italian language
2023-09-11 08:43:16 +08:00
bovirus
f5fe6a36eb Update italian language
@rustdesk

Please merge. Thanks.
2023-09-10 21:42:22 +02:00
Miguel F. G
852bab6e0b Update es.rs
Some corrections plus new  terms
2023-09-10 20:34:26 +02:00
RustDesk
52ec27f785 Merge pull request #5649 from solokot/master
Update ru.rs
2023-09-10 23:04:25 +08:00
RustDesk
c6d617f190 Merge pull request #5650 from fufesou/fix/capitalization_style
fix, some case-style mismatch issues in the same option group
2023-09-10 23:04:00 +08:00
fufesou
1a6ef23ee7 fix, some capitalization style mismatch in the same option group
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-10 22:59:45 +08:00
solokot
759bbeac06 Update ru.rs 2023-09-10 16:47:50 +03:00
RustDesk
15c72fe7d3 Merge pull request #5646 from dignow/fix/android_input_connect_passwd
fix, android soft keyboard, 'delete input' on conn password
2023-09-10 21:02:47 +08:00
RustDesk
f6973f9a70 Merge pull request #5645 from fufesou/feat/scroll_mode
feat, mouse wheel and touchpad scroll mode, default or reverse
2023-09-10 21:02:36 +08:00
fufesou
f1d5afe72a Change the option 'Scroll mode' to be 'Reverse mouse wheel'
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-10 18:31:16 +08:00
dignow
e1f2cd21e7 add comment
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-10 15:41:29 +08:00
dignow
ea5c60af7a fix, android softkeyboard, 'delete input' on conn password
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-10 15:32:50 +08:00
fufesou
558567d399 remove unused mut
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-10 14:28:58 +08:00
fufesou
0c1899a0af format
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-10 14:25:41 +08:00
fufesou
28d8ad1e61 add lang
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-10 14:23:22 +08:00
fufesou
eb0a0662a3 feat, mouse wheel and touchpad scroll mode, default or reverse
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-10 14:14:57 +08:00
RustDesk
405363da59 Merge pull request #5639 from Mr-Update/patch-1
Update de.rs
2023-09-09 14:57:59 +08:00
Mr-Update
8b8cfa7a1b Update de.rs 2023-09-08 21:29:45 +02:00
RustDesk
cc3ff284f7 Merge pull request #5635 from solokot/master
Update ru.rs
2023-09-08 16:35:10 +08:00
solokot
0af6f271c6 Update ru.rs 2023-09-08 11:25:14 +03:00
RustDesk
441cddbde6 Merge pull request #5634 from bovirus/master
Update Italian language
2023-09-08 16:06:38 +08:00
bovirus
1ab09c65f0 Update Italian language
@rustdesk

Please merge. Thanks.
2023-09-08 09:10:28 +02:00
RustDesk
b9892fc2d0 Merge pull request #5633 from 21pages/mobile
mobile privacy statement
2023-09-08 13:43:50 +08:00
21pages
14bf3056de mobile privacy statement
Signed-off-by: 21pages <pages21@163.com>
2023-09-08 13:29:07 +08:00
RustDesk
b3b9555daa Merge pull request #5632 from 21pages/silent-install
windows silent install notification
2023-09-08 10:49:54 +08:00
21pages
db66ffc868 not pop up cm for file log
Signed-off-by: 21pages <pages21@163.com>
2023-09-08 10:38:22 +08:00
21pages
91decea302 windows silent install notification
Signed-off-by: 21pages <pages21@163.com>
2023-09-08 10:38:05 +08:00
RustDesk
065c19cbbc Merge pull request #5625 from dignow/feat/remember_remote_window_fullscreen
Feat/remember remote window fullscreen
2023-09-08 00:41:38 +08:00
dignow
296ebd0341 fix, macos, remote fullscreen state, debug
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-07 22:26:32 +08:00
dignow
5293e3b277 fix, macos, remote fullscreen mode
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-07 21:50:03 +08:00
RustDesk
78f5b1e607 Merge pull request #5623 from 21pages/cm_file
add file log page to cm
2023-09-07 20:33:00 +08:00
dignow
17af5622ec remember remote window fullscreen, set global state
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-07 20:04:23 +08:00
21pages
2afce3f1f4 add file log page to cm
* Only send and receive logs are shown
* For older version, client send to server doesn't have size information, because server side doesn't know the total_size
* Not switch tabs automatically when new files are transferred
* If cm side page is open, not pop up automatically when new files are transferred
* Show unread message count
* The cm tab remains open when closed if a file transfer has previously occurred

Signed-off-by: 21pages <pages21@163.com>
2023-09-07 19:51:25 +08:00
rustdesk
dc29b4afa1 qs.exe -> -qs- 2023-09-07 15:17:07 +08:00
dignow
55fc0cb63b feat, remember remote window fullscreen state
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-07 13:39:20 +08:00
RustDesk
5e1eda9e97 Merge pull request #5616 from sahilyeole/fix/frequent_loginctl_calls
Fix frequent loginctl calls
2023-09-07 08:38:12 +08:00
Sahil Yeole
d582af8cb2 Merge branch 'rustdesk:master' into fix/frequent_loginctl_calls 2023-09-07 01:53:42 +05:30
Sahil Yeole
0931341a7f prevent frequent loginctl calls
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-06 22:43:33 +05:30
RustDesk
92916d9820 Merge pull request #5607 from flusheDData/master
Update es.rs
2023-09-05 22:23:40 +08:00
Miguel F. G
12fbbbb5b3 Update es.rs
New terms added
2023-09-05 16:22:08 +02:00
RustDesk
0a603d022f Merge pull request #5606 from sahilyeole/master
Few mobile chat window improvements
2023-09-05 22:21:12 +08:00
Sahil Yeole
2ada9fbee3 remove WidgetsBindingObserver ios
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-05 18:21:51 +05:30
Sahil Yeole
6e2132c65e Merge branch 'rustdesk:master' into master 2023-09-05 18:10:32 +05:30
Sahil Yeole
71dbf0fab2 fix avoid chat window keyboard overlap ios
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-05 17:58:22 +05:30
Sahil Yeole
048e97e1ee use modified dashchat to fix ios chat window safe area
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-05 17:40:11 +05:30
rustdesk
f1c0f1d0a4 prepare for https://github.com/rustdesk/rustdesk-server-pro/discussions/65 2023-09-05 17:44:49 +08:00
rustdesk
7242d03f56 change target android sdk to 33 2023-09-05 15:09:41 +08:00
RustDesk
58bbc33aa6 Merge pull request #5600 from fufesou/fix/mobile_connecting_canvas_color
fix, mobile connecting canvas color
2023-09-04 23:23:42 +08:00
fufesou
85e82d0bd5 fix, mobile connecting canvas color
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-04 23:21:47 +08:00
Sahil Yeole
d6c23bb5f3 avoid chat window keyboard overlap ios
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-04 20:49:38 +05:30
rustdesk
8235bca664 fix mac uninstall service 2023-09-04 16:22:56 +08:00
rustdesk
cd2541a9d2 fix docker issue #5595 2023-09-04 15:50:13 +08:00
RustDesk
227c9594db Merge pull request #5597 from dignow/fix/image_over_android_toolbar
fix, the session image covers android toolbar
2023-09-04 15:07:05 +08:00
dignow
ea41a60057 fix, the session image covers android toolbar
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-04 14:17:54 +08:00
RustDesk
eee47eae61 Merge pull request #5596 from dignow/fix/generating_id
fix generating id
2023-09-04 12:13:34 +08:00
dignow
b6c1816833 fix generating id
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-04 12:11:35 +08:00
Sahil Yeole
4e359848d1 Merge branch 'rustdesk:master' into master 2023-09-04 01:47:39 +05:30
Sahil Yeole
9cce56caf8 remember chat window last position
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-04 01:45:48 +05:30
RustDesk
f5f0bae2ef Merge pull request #5593 from dignow/fix/remove_dup_maximized_state
Remove dup maximized state
2023-09-03 22:31:54 +08:00
dignow
4fc65aac84 Remote window restore, add 300 milliseconds delay for maximizing after the frame is restored
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-03 22:27:42 +08:00
dignow
bf32477f89 Remove dup maximized state
Signed-off-by: dignow <linlong1265@gmail.com>
2023-09-03 22:18:48 +08:00
Sahil Yeole
969eeff636 add ios chat window border
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-03 18:18:44 +05:30
RustDesk
958607ba9b Merge pull request #5591 from bovirus/master
Update italian language
2023-09-03 18:01:07 +08:00
bovirus
dcfcc1f271 Update italian language 2023-09-03 11:36:10 +02:00
RustDesk
bc3c6af95d Merge pull request #5574 from deep-soft/rustdesk-rustdesk
windows flutter - include version information in self-extracted executable
2023-09-03 15:01:05 +08:00
rustdesk
701220246d fix ci 2023-09-03 14:55:23 +08:00
rustdesk
e3b0cdaf69 Merge branch 'ios_1.2.2' 2023-09-03 11:28:21 +08:00
RustDesk
c61cbfc581 Merge pull request #5589 from 21pages/ios_1.2.2_chat
fix ios setting page show chat title
2023-09-03 10:46:19 +08:00
21pages
58073484fe fix ios setting page show chat title
Signed-off-by: 21pages <pages21@163.com>
2023-09-03 08:07:14 +08:00
deep-soft
8ce1bb1b0b Update flutter-build.yml 2023-09-02 20:52:29 +03:00
deep-soft
3fab42b8d1 Update build.rs 2023-09-02 20:52:07 +03:00
rustdesk
5b802e9edd Merge branch 'ios_1.2.2' 2023-09-03 00:16:08 +08:00
rustdesk
a9308dd992 higher build 2023-09-03 00:06:32 +08:00
RustDesk
937a3e7fd6 Merge pull request #5576 from fufesou/fix/mobile_enable_actions_before_first_image
mobile, enable bottom actions before first image is reached
2023-09-02 23:05:12 +08:00
deep-soft
c9caa5687a Update flutter-build.yml
fix find Runner.res
2023-09-02 14:29:03 +03:00
RustDesk
d6950c680f Merge pull request #5573 from sahilyeole/ios_1.2.2
Fix ios draggable chat window
2023-09-02 17:50:38 +08:00
deep-soft
4fe33db4f3 Update flutter-build.yml 2023-09-01 19:29:10 +03:00
fufesou
96d9604fe1 enable bottom actions before first image is reached
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-09-01 23:55:43 +08:00
deep-soft
0c6eacb141 Update Runner.rc 2023-09-01 16:13:21 +03:00
deep-soft
e83a97349f Update build.rs
# if Runner.res not found compile icon.rc
2023-09-01 16:12:13 +03:00
deep-soft
6c5f0aecb4 Update flutter-build.yml 2023-09-01 16:11:23 +03:00
Sahil Yeole
d9160f9126 Merge branch 'rustdesk:ios_1.2.2' into ios_1.2.2 2023-09-01 17:14:18 +05:30
RustDesk
1d93de8628 Merge pull request #5571 from 21pages/ios_1.2.2
mobile add display settings
2023-09-01 15:23:39 +08:00
21pages
54de5b0300 mobile add default display, merge set server and custom quality code
Signed-off-by: 21pages <pages21@163.com>
2023-09-01 15:04:36 +08:00
21pages
b7145959a7 impl Default for PeerConfig, fix default dispaly not work when ab has password
Signed-off-by: 21pages <pages21@163.com>
2023-09-01 14:57:15 +08:00
RustDesk
1d32a96b01 Merge pull request #5569 from 21pages/ios_1.2.2
replace SettingsTile.navigation with SettingsTile()
2023-09-01 14:57:12 +08:00
Sahil Yeole
9cc02d6fcb fix ios draggable chat window
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-09-01 12:08:33 +05:30
21pages
5d18c04661 replace SettingsTile.navigation with SettingsTile()
Signed-off-by: 21pages <pages21@163.com>
2023-09-01 12:49:57 +08:00
RustDesk
e88e17a4b0 Merge pull request #5557 from 21pages/fix
fix ios build
2023-08-31 09:49:58 +08:00
21pages
a75d73b8ac fix ios build
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 22:40:55 +08:00
RustDesk
bbac6b55d2 Merge pull request #5555 from 21pages/ios_1.2.2
ios add settings tab
2023-08-30 22:10:30 +08:00
21pages
3eeed39f52 ios add settings tab
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 22:04:15 +08:00
RustDesk
7356b7a104 Merge pull request #5554 from 21pages/elevate
close elevation request dialog on submit
2023-08-30 20:58:47 +08:00
21pages
54310b925d close elevation request dialog on submit to avoid frequent request
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 20:50:42 +08:00
RustDesk
85b5c60d60 Merge pull request #5551 from 21pages/android
mobile reset waitForImage
2023-08-30 20:02:08 +08:00
RustDesk
ab48ae6ca6 Merge pull request #5550 from 21pages/ios_1.2.2
Ios 1.2.2 waitForImage and login menu
2023-08-30 20:01:41 +08:00
21pages
7a62eb0ebf mobile reset waitForImage
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 19:46:47 +08:00
21pages
c9423509a9 ios show login/logout
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 19:26:15 +08:00
21pages
0c77d6d918 mobile reset waitForImage
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 18:58:05 +08:00
rustdesk
a0cc6afa7e iOS deploy can run 2023-08-30 18:25:50 +08:00
RustDesk
8a08a0211f Merge pull request #5547 from dignow/refact/ios_1.2.2_dummy_session_get_rgba
use get to call translate()
2023-08-30 14:59:46 +08:00
dignow
2c044d7262 use get to call translate()
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-30 14:56:55 +08:00
RustDesk
9ff3160808 Merge pull request #5546 from dignow/refact/ios_1.2.2_dummy_session_get_rgba
ios add dummy call and remove some lookup functions
2023-08-30 14:03:18 +08:00
dignow
7671ed857d ios add dummy call and remove some lookup functions
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-30 13:49:10 +08:00
RustDesk
b9bbe7a432 Merge pull request #5544 from 21pages/hide_cm
remove hide_cm
2023-08-30 11:56:48 +08:00
21pages
76a18f5ed3 remove hide_cm
Signed-off-by: 21pages <pages21@163.com>
2023-08-30 11:48:42 +08:00
RustDesk
0ee2b02700 Merge pull request #5528 from fufesou/refact/remove_virtual_display_tmp
remove virtual display
2023-08-28 10:48:13 +08:00
fufesou
173984ffd4 remove virtual display, beccause it sometimes unable to install(with the exclamation) and there's an connecting bug
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-28 10:44:24 +08:00
RustDesk
f309ae0c67 Merge pull request #5516 from iAbdullah/master-ar
Add Arabic translation
2023-08-27 20:22:34 +08:00
Abdullah Ibrahim
26ef2539df Update lang.rs
Replace strange Unicode character ';' with Semicolon
2023-08-27 14:43:46 +03:00
RustDesk
bc7df4c841 Merge pull request #5526 from FastAct/patch-19
Update nl.rs
2023-08-27 19:35:07 +08:00
Abdullah Ibrahim
26c95bab66 Update lang.rs 2023-08-27 11:14:29 +03:00
FastAct
95b588f58e Update nl.rs 2023-08-27 09:33:13 +02:00
RustDesk
15e8f6ffb7 Update lang.rs 2023-08-27 13:03:22 +08:00
RustDesk
10cdd7640e Merge pull request #5523 from fufesou/fix/win_try_awake_os_if_locked
win, try awake the os by mouse movement and right click
2023-08-27 13:00:28 +08:00
RustDesk
f216287aee Merge pull request #5524 from 21pages/fix
fix decrypt unicode string
2023-08-27 13:00:11 +08:00
RustDesk
38871a98b4 Merge pull request #5525 from fufesou/fix/better_way_to_detect_no_displays
better way to detect no displays
2023-08-27 12:59:33 +08:00
21pages
9158bdfcf9 fix decrypt unicode string
Signed-off-by: 21pages <pages21@163.com>
2023-08-27 11:39:06 +08:00
fufesou
4dd694ab05 better way to detect no displays
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-27 11:12:02 +08:00
fufesou
553e3ee758 win, try awake the os by mouse movement and right click
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-27 10:28:16 +08:00
Abdullah Ibrahim
a470bf127e Merge branch 'rustdesk:master' into master-ar 2023-08-26 16:11:55 +03:00
Abdullah Ibrahim
52adf51d33 Complete arabic translation 2023-08-26 16:04:50 +03:00
RustDesk
d688e34521 Merge pull request #5514 from 21pages/cm
Do not start cm if file permission not allowed
2023-08-26 19:07:25 +08:00
21pages
50b8744f24 flutter hide cm if client is empty, close cm if that last for 6 seconds
Signed-off-by: 21pages <pages21@163.com>
2023-08-26 19:05:26 +08:00
21pages
3e6faf8364 rust start cm ipc only after login request permission check or switch sides
Signed-off-by: 21pages <pages21@163.com>
2023-08-26 16:43:20 +08:00
Abdullah Ibrahim
720de651f3 add additional translations 2023-08-25 21:58:27 +03:00
Abdullah Ibrahim
3d382d0354 Initial Arabic translation 2023-08-25 19:04:44 +03:00
RustDesk
8fea5585e5 Update README-TR.md 2023-08-25 21:45:00 +08:00
RustDesk
3f12a17246 TR 2023-08-25 21:43:32 +08:00
RustDesk
087ff278aa Merge pull request #5511 from SnmzTony/SnmzTony-patch-1-1
Update tr.rs
2023-08-25 21:13:08 +08:00
Mert Sonmez
301abcaa49 Update tr.rs 2023-08-25 16:10:09 +03:00
RustDesk
e33b8cc8e5 Merge pull request #5510 from SnmzTony/SnmzTony-rustdesk-1
Snmztony rustdesk 1
2023-08-25 19:51:16 +08:00
Mert Sonmez
d1bc8a7202 Update README-TR.md 2023-08-25 14:41:37 +03:00
Mert Sonmez
77fa3bd7fc Create DEVCONTAINER-TR.md 2023-08-25 14:40:02 +03:00
Mert Sonmez
3162fcf154 Create SECURITY-TR.md 2023-08-25 14:38:38 +03:00
Mert Sonmez
c618bdfe91 Update README-TR.md 2023-08-25 14:36:28 +03:00
Mert Sonmez
dd5c9939a0 Create CONTRIBUTING-TR.md 2023-08-25 14:35:45 +03:00
Mert Sonmez
80082b0880 Create CODE_OF_CONDUCT-TR.md 2023-08-25 14:32:27 +03:00
RustDesk
b7aa115bd2 Merge pull request #5507 from SnmzTony/SnmzTony-rustdesk-1
Create README-TR.md
2023-08-25 17:26:07 +08:00
Mert Sonmez
0e9950638c Create README-TR.md 2023-08-25 12:21:33 +03:00
rustdesk
a957acd893 remove LSUIElement=1 in info.plist so that system menu can be shown 2023-08-25 15:19:00 +08:00
RustDesk
a9244333fb Merge pull request #5504 from jimmyGALLAND/update_fr.rs
update fr.rs
2023-08-25 10:07:41 +08:00
Jimmy GALLAND
63a19bc0a1 update fr.rs 2023-08-24 22:36:55 +02:00
RustDesk
2f6711dd2d Merge pull request #5498 from 21pages/audit
alarm audit more info
2023-08-24 20:04:50 +08:00
RustDesk
c450b41e8f Merge pull request #5489 from fufesou/fix/enable_menu_before_image
Fix/enable menu before image
2023-08-24 19:57:23 +08:00
fufesou
9937650062 Use Overlay to wrap RemoteToolbar to enable rebuild everytime on click
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-24 14:40:02 +08:00
fufesou
56ff88934f Add RemoteToolbar to Obx(()) to rebuild after pi is recved
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-24 13:07:36 +08:00
fufesou
c1a577797a Revert Ctrl+Alt+Del in toolbar right menu
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-24 12:50:39 +08:00
fufesou
257227920d Fix missing menu action
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-24 12:24:08 +08:00
RustDesk
461633cd83 Merge pull request #5499 from cacing69/master
add new translation
2023-08-24 12:15:06 +08:00
Ibnul Mutaki
a0887e9285 changed tips for better understanding 2023-08-24 11:12:02 +07:00
Ibnul Mutaki
23354d371f change inconsitent word and add new translation 2023-08-24 11:07:47 +07:00
fufesou
0e838d59d5 Check if peer info is set
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-24 12:03:29 +08:00
RustDesk
f305c9d96a Merge pull request #5491 from PeterDaveHello/tw
Update and improve tw translation
2023-08-24 10:24:14 +08:00
RustDesk
226665403f Merge pull request #5496 from cacing69/master
fix some translate read me for indonesian translate, add create contributing-id.md
2023-08-24 10:23:17 +08:00
21pages
88d0460e3c alarm audit more info
Signed-off-by: 21pages <pages21@163.com>
2023-08-24 10:04:34 +08:00
Cacing69
e09b4f878e Merge branch 'rustdesk:master' into master 2023-08-24 08:02:45 +07:00
Ibnul Mutaki
a7ef3ce58a fix some translate read me for indonesian translate, add create contributing-id.md 2023-08-24 08:01:41 +07:00
Peter Dave Hello
256f33b720 Update and improve tw translation 2023-08-24 01:33:06 +08:00
fufesou
e17002c6da set right menu duration from 4s to 300s
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-24 00:00:18 +08:00
fufesou
f2d96b895f hide empty waiting layer after images are reached
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-23 23:57:09 +08:00
fufesou
dade589075 fix, enable menu before image
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2023-08-23 23:29:15 +08:00
rustdesk
0d9cd25a71 Merge branch 'master' of github.com-rustdesk:rustdesk/rustdesk 2023-08-23 21:59:50 +08:00
rustdesk
cb73490107 add quick fix for https://github.com/rustdesk/rustdesk/issues/5488#issuecomment-1689997785 2023-08-23 21:59:22 +08:00
RustDesk
535405521c Merge pull request #5487 from Mr-Update/master-4
Update de.rs
2023-08-23 20:28:47 +08:00
Mr-Update
8785c08861 Update de.rs 2023-08-23 14:27:30 +02:00
RustDesk
0020a37029 Merge pull request #5486 from SergeyMy/master-3
Update ru.rs
2023-08-23 15:28:02 +08:00
SergeyMy
33cbed592a Update ru.rs 2023-08-23 12:25:43 +05:00
rustdesk
28c0e15058 bump to 1.2.3 2023-08-23 12:56:33 +08:00
rustdesk
a316411f76 Merge branch 'master' of github.com-rustdesk:rustdesk/rustdesk 2023-08-23 12:52:44 +08:00
RustDesk
66ad519dbd Merge pull request #5386 from dignow/refact/tab_2_window_flutter_data
Refact/tab 2 window flutter data
2023-08-23 12:52:19 +08:00
RustDesk
2a8dc1d34a Merge pull request #5323 from dignow/refact/android_scroll_event
Refact/android scroll event
2023-08-23 12:51:54 +08:00
RustDesk
228b670b4f Merge pull request #5485 from 21pages/ab
fix pushAb
2023-08-23 12:51:12 +08:00
21pages
da9fb46b6a fix pushAb
Signed-off-by: 21pages <pages21@163.com>
2023-08-23 12:20:19 +08:00
rustdesk
f4d120b11f remove useless line 2023-08-23 12:04:19 +08:00
RustDesk
bc954b75ce Merge pull request #5482 from SergeyMy/master-2
Update ru.rs
2023-08-23 12:03:33 +08:00
RustDesk
b787734913 Merge branch 'master' into master-2 2023-08-23 12:03:25 +08:00
RustDesk
fb5a6c20de Merge pull request #5483 from sahilyeole/master
Using github latest release url to check for software update
2023-08-23 12:02:45 +08:00
RustDesk
3fda085cbb Merge pull request #5484 from 21pages/ab
add ColorPicker translations
2023-08-23 11:59:02 +08:00
21pages
d87ea854bc add ColorPicker translations
Signed-off-by: 21pages <pages21@163.com>
2023-08-23 08:15:56 +08:00
Sahil Yeole
f41cb0d81c updated get_new_version for ui
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-08-23 00:14:32 +05:30
Sahil Yeole
8427b03a39 Using github latest release url to check for software update
Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
2023-08-23 00:13:23 +05:30
SergeyMy
9cb7786182 Update ru.rs 2023-08-22 19:26:59 +05:00
rustdesk
6666dece5d svgo gitlab.svg 2023-08-22 22:02:42 +08:00
RustDesk
efdd17fa9a Merge pull request #5481 from dignow/feat/add_oidc_gitlab_default_icon
oidc, add default gitlab icon
2023-08-22 22:00:35 +08:00
dignow
7f7d5d9f4c oidc, add default gitlab icon
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-22 21:52:23 +08:00
RustDesk
9b542f7653 Merge pull request #5476 from 21pages/ab
change tag color
2023-08-22 19:51:46 +08:00
21pages
b27c3ff169 change tag color
Signed-off-by: 21pages <pages21@163.com>
2023-08-22 19:07:01 +08:00
RustDesk
115221098a Merge pull request #5471 from 21pages/ab
mobile reuse tile peer card
2023-08-22 12:22:53 +08:00
21pages
5a6a7e8d82 mobile use _buildPeerTile
Signed-off-by: 21pages <pages21@163.com>
2023-08-22 11:41:57 +08:00
dignow
9adac5686b fix unregister texture condition
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-16 21:52:03 +08:00
dignow
fad88c2718 refact, tab to window, remove rust cache data
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-14 21:04:24 +08:00
dignow
e205577145 refact, tab to window, flutter data, init commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-14 20:58:31 +08:00
dignow
6368ab691c simple refactor, move code from flutter_ffi.rs to flutter.rs
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 16:08:30 +08:00
dignow
5b2358c97f debug android scroll
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
072430cef5 debug android scroll
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
5f7055e282 debug
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
be982d95ea tmp build
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
b9c8df7019 debug
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
e89ae475f6 fix build
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
9476d7fdbb try fix build
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:29 +08:00
dignow
da16a799fa fix build
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
9e0feb0b64 tmp debug
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
93a600a0a8 tmp commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
06ee68f836 tmp commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
d6f1abad95 tmp commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
933c99110c tmp commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
8999bbf297 tmp commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
dignow
200fc56a4a tmp commit
Signed-off-by: dignow <linlong1265@gmail.com>
2023-08-10 14:54:28 +08:00
171 changed files with 8825 additions and 3133 deletions

View File

@@ -22,7 +22,7 @@ env:
# vcpkg version: 2023.04.15
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2"
VERSION: "1.2.3"
NDK_VERSION: "r25c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
@@ -82,6 +82,7 @@ jobs:
- name: Install flutter rust bridge deps
run: |
git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@@ -96,6 +97,22 @@ jobs:
VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
continue-on-error: true
shell: bash
run: |
runner_res=$(find . -name "Runner.res");
if [ "$runner_res" == "" ]; then
echo "Runner.res: not found";
else
echo "Runner.res: $runner_res";
cp $runner_res ./libs/portable/Runner.res;
echo "list ./libs/portable/Runner.res";
ls -l ./libs/portable/Runner.res;
fi
- name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7
if: env.UPLOAD_ARTIFACT == 'true'
@@ -198,6 +215,22 @@ jobs:
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
echo "output_folder=./Release" >> $GITHUB_OUTPUT
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
continue-on-error: true
shell: bash
run: |
runner_res=$(find . -name "Runner.res");
if [ "$runner_res" == "" ]; then
echo "Runner.res: not found";
else
echo "Runner.res: $runner_res";
cp $runner_res ./libs/portable/Runner.res;
echo "list ./libs/portable/Runner.res";
ls -l ./libs/portable/Runner.res;
fi
- name: Sign rustdesk files
uses: GermanBluefox/code-sign-action@v7
if: env.UPLOAD_ARTIFACT == 'true'
@@ -428,6 +461,13 @@ jobs:
prefix-key: rustdesk-lib-cache
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
- name: Install flutter rust bridge deps
shell: bash
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h
- name: Build rustdesk lib
env:
VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg
@@ -439,7 +479,9 @@ jobs:
shell: bash
run: |
pushd flutter
flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
# for easy debugging
flutter build ipa --release --no-codesign
# - name: Upload Artifacts
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -1572,8 +1614,8 @@ jobs:
# apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git
# # flatpak deps
# flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
# flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08
# flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08
# flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
# flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# # package
# pushd flatpak
# git clone https://github.com/flathub/shared-modules.git --depth=1
@@ -1635,8 +1677,8 @@ jobs:
apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git
# flatpak deps
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# package
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1

View File

@@ -15,4 +15,4 @@ jobs:
secrets: inherit
with:
upload-artifact: true
upload-tag: "1.2.2"
upload-tag: "1.2.3"

View File

@@ -10,7 +10,7 @@ env:
# vcpkg version: 2022.05.10
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
VERSION: "1.2.2"
VERSION: "1.2.3"
jobs:
build-for-history-windows:

View File

@@ -24,7 +24,7 @@ jobs:
path: /opt/artifacts
key: vcpkg-${{ matrix.job.arch }}
- uses: Kingtous/run-on-arch-action@amd64-support
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Run vcpkg install on ${{ matrix.job.arch }}
id: vcpkg
with:
@@ -40,12 +40,16 @@ jobs:
apt update -y
case "${{ matrix.job.arch }}" in
x86_64)
# CMake 3.15+
apt install -y gpg wget ca-certificates
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
apt update -y
apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev
apt install -y curl zip unzip tar git g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev libssl-dev
wget https://github.com/Kitware/CMake/releases/download/v3.27.5/cmake-3.27.5.tar.gz
apt remove -y --purge cmake
tar -zxvf cmake-3.27.5.tar.gz
cd cmake-3.27.5
./bootstrap
make
make install
cd -
cmake --version
gcc -v
;;
@@ -85,4 +89,4 @@ jobs:
with:
name: vcpkg-artifact-${{ matrix.job.arch }}
path: |
/opt/artifacts/vcpkg/installed
/opt/artifacts/vcpkg/installed

69
Cargo.lock generated
View File

@@ -4422,7 +4422,7 @@ dependencies = [
"base64",
"indexmap",
"line-wrap",
"quick-xml",
"quick-xml 0.28.2",
"serde 1.0.163",
"time 0.3.21",
]
@@ -4622,6 +4622,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "quick-xml"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.28.2"
@@ -4872,7 +4881,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
source = "git+https://github.com/fufesou/rdev#ee3057bd97c91529e8b9daf2ca133a5c49f0c0eb"
source = "git+https://github.com/fufesou/rdev#2e8221d653f4995c831ad52966e79a514516b1fa"
dependencies = [
"cocoa",
"core-foundation",
@@ -5124,7 +5133,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.2.2"
version = "1.2.3"
dependencies = [
"android_logger",
"arboard",
@@ -5199,6 +5208,7 @@ dependencies = [
"sys-locale",
"system_shutdown",
"tao",
"tauri-winrt-notification",
"tray-icon",
"url",
"users 0.11.0",
@@ -5971,6 +5981,16 @@ dependencies = [
"serde_json 0.9.10",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5bff1d532fead7c43324a0fa33643b8621a47ce2944a633be4cb6c0240898f"
dependencies = [
"quick-xml 0.23.1",
"windows 0.39.0",
]
[[package]]
name = "tempfile"
version = "3.5.0"
@@ -6824,6 +6844,19 @@ dependencies = [
"windows_x86_64_msvc 0.34.0",
]
[[package]]
name = "windows"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
dependencies = [
"windows_aarch64_msvc 0.39.0",
"windows_i686_gnu 0.39.0",
"windows_i686_msvc 0.39.0",
"windows_x86_64_gnu 0.39.0",
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows"
version = "0.44.0"
@@ -6973,6 +7006,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
[[package]]
name = "windows_aarch64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -6997,6 +7036,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
[[package]]
name = "windows_i686_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -7021,6 +7066,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
[[package]]
name = "windows_i686_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -7045,6 +7096,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
[[package]]
name = "windows_x86_64_gnu"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -7081,6 +7138,12 @@ version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
[[package]]
name = "windows_x86_64_msvc"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.2.2"
version = "1.2.3"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -97,6 +97,7 @@ virtual_display = { path = "libs/virtual_display", optional = true }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
shared_memory = "0.12"
shutdown_hooks = "0.1"
tauri-winrt-notification = "0.1.2"
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"

View File

@@ -5,13 +5,15 @@
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>]<br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
</p>
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -2,7 +2,7 @@
version: 1
script:
- rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb
- bsdtar -zxvf ../rustdesk-1.2.3.deb
- tar -xvf ./data.tar.xz
- mkdir ./AppDir
- mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.2.2
version: 1.2.3
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -2,7 +2,7 @@
version: 1
script:
- rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.2.deb
- bsdtar -zxvf ../rustdesk-1.2.3.deb
- tar -xvf ./data.tar.xz
- mkdir ./AppDir
- mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.2.2
version: 1.2.3
exec: usr/lib/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -545,13 +545,6 @@ def main():
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# https://github.com/sindresorhus/create-dmg
system2('/bin/rm -rf *.dmg')
plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist"
txt = open(plist).read()
with open(plist, "wt") as fh:
fh.write(txt.replace("</dict>", """
<key>LSUIElement</key>
<string>1</string>
</dict>"""))
pa = os.environ.get('P')
if pa:
system2('''

View File

@@ -0,0 +1,89 @@
# Katkıda Bulunanların Davranış Kuralları
## Taahhüdümüz
Biz üyeler, katkıda bulunanlar ve liderler olarak, yaş, beden büyüklüğü, görünür veya görünmez engellilik, etnik köken, cinsiyet özellikleri, cinsiyet kimliği ve ifadesi, deneyim seviyesi, eğitim, sosyo-ekonomik durum, milliyet, kişisel görünüm, ırk, din veya cinsel kimlik ve yönelim ayrımı gözetmeksizin herkes için topluluğumuzdaki katılımı taciz içermeyen bir deneyim haline getirmeyi taahhüt ederiz.
ık, hoşgörülü, çeşitli, kapsayıcı ve sağlıklı bir topluluğa katkıda bulunacak şekillerde hareket etmeyi ve etkileşimde bulunmayı taahhüt ederiz.
## Standartlarımız
Topluluğumuz için olumlu bir ortam yaratmaya katkıda bulunan davranış örnekleri şunlardır:
* Diğer insanlara empati ve nezaket göstermek
* Farklı görüşlere, bakış açılarına ve deneyimlere saygılı olmak
* Yapıcı eleştiriyi vermek ve zarifçe kabul etmek
* Hatalarımızdan etkilenenlere sorumluluk kabul etmek, özür dilemek ve deneyimden öğrenmek
* Sadece bireyler olarak değil, aynı zamanda genel topluluk için en iyisi üzerine odaklanmak
Kabul edilemez davranış örnekleri şunları içerir:
* Cinselleştirilmiş dil veya imgelerin kullanımı ve cinsel ilgi veya herhangi bir türdeki yaklaşımlar
* Trollük, aşağılayıcı veya hakaret içeren yorumlar ve kişisel veya siyasi saldırılar
* Kamuoyu veya özel taciz
* Başkalarının fiziksel veya e-posta adresi gibi özel bilgilerini, açık izinleri olmadan yayınlamak
* Profesyonel bir ortamda makul bir şekilde uygunsuz kabul edilebilecek diğer davranışlar
## Uygulama Sorumlulukları
Topluluk liderleri, kabul edilebilir davranış standartlarımızııklığa kavuşturmak ve uygulamakla sorumludur ve uygunsuz, tehditkar, saldırgan veya zarar verici herhangi bir davranışa yanıt olarak uygun ve adil düzeltici önlemler alacaklardır.
Topluluk liderleri, bu Davranış Kurallarına uyumlu olmayan yorumları, taahhütlerini veya kodu, wiki düzenlemelerini, sorunları ve diğer katkıları kaldırma, düzenleme veya reddetme hakkına sahiptir. Denetim kararlarının nedenlerini uygun olduğunda ileteceklerdir.
## Kapsam
Bu Davranış Kuralları, tüm topluluk alanlarında geçerlidir ve aynı zamanda birey resmi olarak topluluğu halka açık alanlarda temsil ettiğinde de geçerlidir. Topluluğumuzu temsil etme örnekleri, resmi bir e-posta adresi kullanmak, resmi bir sosyal medya hesabı üzerinden gönderi yapmak veya çevrimiçi veya çevrimdışı bir etkinlikte atanmış bir temsilci olarak hareket etmeyi içerir.
## Uygulama
Taciz edici, rahatsız edici veya başka türlü kabul edilemez davranış örnekleri, [info@rustdesk.com](mailto:info@rustdesk.com) adresindeki uygulama sorumlularına bildirilebilir. Tüm şikayetler hızlı ve adil bir şekilde incelenecek ve araştırılacaktır.
Tüm topluluk liderleri, olayın raporlayıcısının gizliliğine ve güvenliğine saygı gösterme yükümlülüğündedir.
## Uygulama Kılavuzları
Topluluk liderleri, bu Davranış Kurallarını ihlal olarak değerlendirdikleri herhangi bir eylem için bu Topluluk Etkisi Kılavuzlarını izleyeceklerdir:
### 1. Düzeltme
**Topluluk Etkisi**: Topluluk içinde profesyonel veya hoşgörülü olmayan uygun olmayan dil veya diğer davranışların kullanımı.
**Sonuç**: Topluluk liderlerinden özel ve yazılı bir uyarı almak, ihlalin niteliği ve davranışın nedeninin açıklığa kavuşturulması. Bir kamu özrü istenebilir.
### 2. Uyarı
**Topluluk Etkisi**: Tek bir olay veya dizi aracılığıyla bir ihlal.
**Sonuç**: Devam eden davranış için sonuçları olan bir uyarı. Topluluk liderleri de dahil olmak üzere ihlalle ilgili kişilerle etkileşim, belirli bir süre boyunca önerilmez. Bu, topluluk alanlarında ve sosyal medya gibi harici kanallarda etkileşimleri içerir. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
### 3. Geçici Yasak
**Topluluk Etkisi**: Sürekli uygunsuz davranış da dahil olmak üzere topluluk standartlarının ciddi bir ihlali.
**Sonuç**: Belirli bir süre için toplulukla herhangi bir türdeki etkileşim veya halka açık iletişimden geçici bir yasak. Bu dönem boyunca, toplul
ukla veya uygulama kurallarını uygulayanlarla her türlü kamuoyu veya özel etkileşim izin verilmez. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
### 4. Kalıcı Yasak
**Topluluk Etkisi**: Topluluk standartlarının ihlalinde sürekli bir desen sergilemek, bireye sürekli olarak uygun olmayan davranışlarda bulunmak, bir bireye tacizde bulunmak veya birey sınıflarına karşı saldırganlık veya aşağılama yapmak.
**Sonuç**: Topluluk içinde her türlü halka açık etkileşimden kalıcı bir yasak.
## Atıf
Bu Davranış Kuralları, [Contributor Covenant][anasayfa], 2.0 sürümünden uyarlanmıştır ve
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] adresinde bulunmaktadır.
Topluluk Etkisi Kılavuzları,
[Mozilla'nın davranış kuralları uygulama merdiveni][Mozilla DK] tarafından ilham alınarak oluşturulmuştur.
Bu davranış kuralları hakkında yaygın soruların cevapları için, SSS'ye göz atın:
[https://www.contributor-covenant.org/faq][SSS]. Çeviriler,
[https://www.contributor-covenant.org/translations][çeviriler] adresinde bulunabilir.
[anasayfa]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla DK]: https://github.com/mozilla/diversity
[SSS]: https://www.contributor-covenant.org/faq
[çeviriler]: https://www.contributor-covenant.org/translations

31
docs/CONTRIBUTING-ID.md Normal file
View File

@@ -0,0 +1,31 @@
# Berkontribusi dalam pengembangan RustDesk
RustDesk mengajak semua orang untuk ikut berkontribusi. Berikut ini adalah panduan jika kamu sedang mempertimbangkan untuk memberikan bantuan kepada kami:
## Kontirbusi
Untuk melakukan kontribusi pada RustDesk atau dependensinya, sebaiknya dilakukan dalam bentuk pull request di GitHub. Setiap permintaan pull request akan ditinjau oleh kontributor utama atau seseorang yang memiliki wewenang untuk menggabungkan perubahan kode, baik yang sudah dimasukkan ke dalam struktur utama ataupun memberikan umpan balik untuk perubahan yang akan diperlukan. Setiap kontribusi harus sesuai dengan format ini, juga termasuk yang berasal dari kontributor utama.
Apabila kamu ingin mengatasi sebuah masalah yang sudah ada di daftar issue, harap klaim terlebih dahulu dengan memberikan komentar pada GitHub issue yang ingin kamu kerjakan. Hal ini dilakukan untuk mencegah terjadinya duplikasi dari kontributor pada daftar issue yang sama.
## Pemeriksaan Pull Request
- Branch yang menjadi acuan adalah branch master dari repositori utama dan, jika diperlukan, lakukan rebase ke branch master yang terbaru sebelum kamu mengirim pull request. Apabila terdapat masalah kita melakukan proses merge ke branch master kemungkinan kamu akan diminta untuk melakukan rebase pada perubahan yang sudah dibuat.
- Sebaiknya buatlah commit seminimal mungkin, sambil memastikan bahwa setiap commit yang dibuat sudah benar (contohnya, setiap commit harus bisa di kompilasi dan berhasil melewati tahap test).
- Setiap commit harus disertai dengan tanda tangan Sertifikat Asal Pengembang (Developer Certificate of Origin) (<http://developercertificate.org>), yang mengindikasikan bahwa kamu (and your employer if applicable) bersedia untuk patuh terhadap persyaratan dari [lisensi projek](../LICENCE). Di git bash, ini adalah opsi parameter `-s` pada `git commit`
- Jika perubahan yang kamu buat tidak mendapat tinjauan atau kamu membutuhkan orang tertentu untuk meninjaunya, kamu bisa @-reply seorang reviewer meminta peninjauan dalam permintaan pull request atau komentar, atau kamu bisa meminta tinjauan melalui [email](mailto:info@rustdesk.com).
- Sertakan test yang relevan terhadap bug atau fitur baru yang sudah dikerjakan.
Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Tindakan
<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
## Komunikasi
Kontributor RustDesk sering berkunjung ke [Discord](https://discord.gg/nDceKgxnkV).

31
docs/CONTRIBUTING-TR.md Normal file
View File

@@ -0,0 +1,31 @@
# RustDesk'a Katkı Sağlamak
RustDesk, herkesten katkıyı memnuniyetle karşılar. Eğer bize yardımcı olmayı düşünüyorsanız, işte rehberlik eden kurallar:
## Katkılar
RustDesk veya bağımlılıklarına yapılan katkılar, GitHub pull istekleri şeklinde yapılmalıdır. Her bir pull isteği, çekirdek katkıcı tarafından gözden geçirilecek (yamaları kabul etme izni olan biri) ve ana ağaca kabul edilecek veya gerekli değişiklikler için geri bildirim verilecektir. Tüm katkılar bu formata uymalıdır, çekirdek katkıcılardan gelenler bile.
Eğer bir konu üzerinde çalışmak isterseniz, önce üzerinde çalışmak istediğinizi belirten bir yorum yaparak konuyu talep ediniz. Bu, katkı sağlayanların aynı konuda çift çalışmasını engellemek içindir.
## Pull İstek Kontrol Listesi
- Master dalından dallandırın ve gerekiyorsa pull isteğinizi göndermeden önce mevcut master dalına rebase yapın. Eğer master ile temiz bir şekilde birleşmezse, değişikliklerinizi rebase yapmanız istenebilir.
- Her bir commit mümkün olduğunca küçük olmalıdır, ancak her commit'in bağımsız olarak doğru olduğundan emin olun (örneğin, her commit derlenebilir ve testleri geçmelidir).
- Commit'ler, bir Geliştirici Sertifikası ile desteklenmelidir (http://developercertificate.org). Bu, [proje lisansının](../LICENCE) koşullarına uymayı kabul ettiğinizi gösteren bir onaydır. Git'te bunu `git commit` seçeneği olarak `-s` seçeneği ile yapabilirsiniz.
- Yamalarınız gözden geçirilmiyorsa veya belirli bir kişinin gözden geçirmesine ihtiyacınız varsa, çekme isteği veya yorum içinde bir gözden geçirmeyi istemek için bir inceleyiciyi @etiketleyebilir veya inceleme için [e-posta](mailto:info@rustdesk.com) ile talep edebilirsiniz.
- Düzelttiğiniz hatanın veya eklediğiniz yeni özelliğin ilgili testlerini ekleyin.
Daha spesifik git talimatları için, [GitHub iş akışı 101](https://github.com/servo/servo/wiki/GitHub-workflow)'e bakınız.
## Davranış
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-TR.md
## İletişim
RustDesk katkı sağlayıcıları, [Discord](https://discord.gg/nDceKgxnkV) kanalını sık sık ziyaret ederler.

12
docs/DEVCONTAINER-TR.md Normal file
View File

@@ -0,0 +1,12 @@
Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur.
Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır.
Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır.
Komut | Derleme Türü | Mod
-|-|-
`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama
`.devcontainer/build.sh --release linux` | Linux | sürüm
`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama
`.devcontainer/build.sh --release android` | Android-arm64 | sürüm

View File

@@ -13,15 +13,27 @@ Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Merupakan perangkat lunak Remote Desktop yang baru, dibangun dengan Rust. kamu bisa langsung menggunakannya tanpa perlu konfigurasi tambahan. Serta ,emiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo).
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) untuk melihat panduan.
Merupakan perangkat lunak Remote Desktop yang baru, dan dibangun dengan Rust. Bahkan kamu bisa langsung menggunakannya tanpa perlu melakukan konfigurasi tambahan. Serta memiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING-ID.md`](CONTRIBUTING-ID.md) untuk melihat panduan.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**UNDUH BINARY**](https://github.com/rustdesk/rustdesk/releases)
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Server Publik Gratis
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring waktu kemungkinan akan terjadi perubahan spesifikasi pada setiap server. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
| Lokasi | Penyedia | Spesifikasi |
| --------- | ------------- | ------------------ |
| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
@@ -31,11 +43,11 @@ Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring waktu kemun
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Apabila kamu sudah menginstall VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
## Dependensi
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter, tutorial ini hanya berlaku untuk Sciter
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter
Kamu bisa mengunduh Sciter dynamic library disini.

223
docs/README-TR.md Normal file
View File

@@ -0,0 +1,223 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Uzak masaüstü uygulamanız"><br>
<a href="#free-public-servers">Sunucular</a> •
<a href="#raw-steps-to-build">Derleme</a> •
<a href="#how-to-build-with-docker">Docker ile Derleme</a> •
<a href="#file-structure">Dosya Yapısı</a> •
<a href="#snapshot">Ekran Görüntüleri</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>]<br>
<b>README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ve <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Belge</a>'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
</p>
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kullanıma hazır, hiçbir yapılandırma gerektirmez. Verilerinizin tam kontrolünü elinizde tutarsınız ve güvenlikle ilgili endişeleriniz olmaz. Kendi buluş/iletme sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi buluş/iletme sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BİNARİ İNDİR**](https://github.com/rustdesk/rustdesk/releases)
[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="F-Droid'de Alın"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## Ücretsiz Genel Sunucular
Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
| Konum | Sağlayıcı | Özellikler |
| --------- | ------------- | ------------------ |
| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
## Geliştirici Konteyneri
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
## Bağımlılıklar
Masaüstü sürümleri GUI için
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Temel Derleme Adımları
- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın.
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` çevresel değişkenini doğru bir şekilde ayarlayın.
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/macOS: vcpkg install libvpx libyuv opus aom
- `cargo run` komutunu çalıştırın.
## [Derleme](https://rustdesk.com/docs/en/dev/build/)
## Linux Üzerinde Derleme Nasıl Yapılır
### Ubuntu 18 (Debian 10)
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
```
### Arch (Manjaro)
```sh
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### vcpkg'yi Yükleyin
```sh
git clone https://github.com/microsoft/vcpkg
cd vcpkg
git checkout 2023.04.15
cd ..
vcpkg/bootstrap-vcpkg.sh
export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### libvpx'i Düzeltin (Fedora için)
```sh
cd vcpkg/buildtrees/libvpx/src
cd *
./configure
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
make
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Derleme
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
### Wayland'ı X11 (Xorg) Olarak Değiştirme
RustDesk, Wayland'ı desteklemez. Xorg'u GNOME oturumu olarak varsayılan olarak ayarlamak için [burayı](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) kontrol edin.
## Wayland Desteği
Wayland'ın diğer pencerelere tuş vuruşu göndermek için herhangi bir API sağlamadığı görünmektedir. Bu nedenle, RustDesk daha düşük bir seviyeden, yani Linux çekirdek seviyesindeki `/dev/uinput` cihazının API'sini kullanır.
Wayland tarafı kontrol edildiğinde, aşağıdaki şekilde başlatmanız gerekir:
```bash
# uinput servisini başlatın
$ sudo rustdesk --service
$ rustdesk
```
**Uyarı**: Wayland ekran kaydı farklı arayüzler kullanır. RustDesk şu anda yalnızca org.freedesktop.portal.ScreenCast'ı destekler.
```bash
$ dbus-send --session --print-reply \
--dest=org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop \
org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.portal.ScreenCast string:version
# Desteklenmez
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
# Desteklenir
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
variant uint32 4
```
## Docker ile Derleme Nasıl Yapılır
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
docker build -t "rustdesk-builder" .
```
Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
İlk derleme, bağımlılıklar önbelleğe alınmadan önce daha uzun sürebilir, sonraki derlemeler daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu
komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
```sh
target/debug/rustdesk
```
Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
```sh
target/release/rustdesk
```
Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır ve ana makinede değil.
## Dosya Yapısı
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodlayıcı, yapılandırma, tcp/udp sarmalayıcı, protobuf, dosya transferi için fs işlevleri ve diğer bazı yardımcı işlevler
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
## Ekran Görüntüleri
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
```

9
docs/SECURITY-TR.md Normal file
View File

@@ -0,0 +1,9 @@
# Güvenlik Politikası
## Bir Güvenlik Açığı Bildirme
Projemiz için güvenliği çok önemsiyoruz. Kullanıcıların keşfettikleri herhangi bir güvenlik açığını bize bildirmelerini teşvik ediyoruz.
Eğer RustDesk projesinde bir güvenlik açığı bulursanız, lütfen info@rustdesk.com adresine sorumlu bir şekilde bildirin.
Şu an için bir hata ödül programımız bulunmamaktadır. Büyük bir sorunu çözmeye çalışan küçük bir ekibiz. Herhangi bir güvenlik açığını sorumlu bir şekilde bildirmenizi rica ederiz,
böylece tüm topluluk için güvenli bir uygulama oluşturmaya devam edebiliriz.

View File

@@ -1,7 +1,7 @@
{
"id": "com.rustdesk.RustDesk",
"runtime": "org.freedesktop.Platform",
"runtime-version": "21.08",
"runtime-version": "23.08",
"sdk": "org.freedesktop.Sdk",
"command": "rustdesk",
"icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
@@ -12,7 +12,7 @@
"name": "rustdesk",
"buildsystem": "simple",
"build-commands": [
"bsdtar -zxvf rustdesk-1.2.2.deb",
"bsdtar -zxvf rustdesk-1.2.3.deb",
"tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
@@ -26,7 +26,7 @@
"sources": [
{
"type": "file",
"path": "../rustdesk-1.2.2.deb"
"path": "../rustdesk-1.2.3.deb"
},
{
"type": "file",

View File

@@ -46,7 +46,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb"
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@@ -26,6 +26,13 @@ const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331
const val WHEEL_UP = 963
const val TOUCH_SCALE_START = 1
const val TOUCH_SCALE = 2
const val TOUCH_SCALE_END = 3
const val TOUCH_PAN_START = 4
const val TOUCH_PAN_UPDATE = 5
const val TOUCH_PAN_END = 6
const val WHEEL_STEP = 120
const val WHEEL_DURATION = 50L
const val LONG_TAP_DELAY = 200L
@@ -167,6 +174,30 @@ class InputService : AccessibilityService() {
}
}
@RequiresApi(Build.VERSION_CODES.N)
fun onTouchInput(mask: Int, _x: Int, _y: Int) {
when (mask) {
TOUCH_PAN_UPDATE -> {
mouseX -= _x * SCREEN_INFO.scale
mouseY -= _y * SCREEN_INFO.scale
mouseX = max(0, mouseX);
mouseY = max(0, mouseY);
continueGesture(mouseX, mouseY)
}
TOUCH_PAN_START -> {
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
startGesture(mouseX, mouseY)
}
TOUCH_PAN_END -> {
endGesture(mouseX, mouseY)
mouseX = max(0, _x) * SCREEN_INFO.scale
mouseY = max(0, _y) * SCREEN_INFO.scale
}
else -> {}
}
}
@RequiresApi(Build.VERSION_CODES.N)
private fun consumeWheelActions() {
if (isWheelActionsPolling) {

View File

@@ -71,17 +71,26 @@ class MainService : Service() {
@Keep
@RequiresApi(Build.VERSION_CODES.N)
fun rustMouseInput(mask: Int, x: Int, y: Int) {
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && mask == LIFT_DOWN) {
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) {
Log.d(logTag,"Turn on Screen, WakeLock release")
Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release()
}
Log.d(logTag,"Turn on Screen")
wakeLock.acquire(5000)
} else {
InputService.ctx?.onMouseInput(mask,x,y)
when (kind) {
"touch" -> {
InputService.ctx?.onTouchInput(mask, x, y)
}
"mouse" -> {
InputService.ctx?.onMouseInput(mask, x, y)
}
else -> {
}
}
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="236" preserveAspectRatio="xMidYMid"><path fill="#e24329" d="m128.075 236.075 47.104-144.97H80.97z"/><path fill="#fc6d26" d="M128.075 236.074 80.97 91.104H14.956z"/><path fill="#fca326" d="M14.956 91.104.642 135.16a9.752 9.752 0 0 0 3.542 10.903l123.891 90.012z"/><path fill="#e24329" d="M14.956 91.105H80.97L52.601 3.79c-1.46-4.493-7.816-4.492-9.275 0z"/><path fill="#fc6d26" d="m128.075 236.074 47.104-144.97h66.015z"/><path fill="#fca326" d="m241.194 91.104 14.314 44.056a9.752 9.752 0 0 1-3.543 10.903l-123.89 90.012z"/><path fill="#e24329" d="M241.194 91.105h-66.015l28.37-87.315c1.46-4.493 7.816-4.492 9.275 0z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696255389449" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1922" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M435.2 704c-9 0-17.8-3.8-23.8-10.6l-115.2-128c-11.8-13.2-10.8-33.4 2.4-45.2 13.2-11.8 33.4-10.8 45.2 2.4l90.6 100.6 245.2-291.8c11.4-13.6 31.6-15.2 45-4 13.6 11.4 15.2 31.6 4 45l-268.8 320c-6 7-14.6 11.2-24 11.4-0.2 0.2-0.4 0.2-0.6 0.2z" p-id="1923"></path><path d="M800 928H224c-70.6 0-128-57.4-128-128V224c0-70.6 57.4-128 128-128h576c70.6 0 128 57.4 128 128v576c0 70.6-57.4 128-128 128zM224 160c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64H224z" p-id="1924"></path></svg>

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1696245886035" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4133" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 132.717714c-9.435429 0-18.852571 3.84-29.147429 12.434286L194.011429 379.574857c-7.277714 6.418286-11.556571 15.433143-11.556572 28.288 0 22.272 16.713143 39.003429 39.424 39.003429 8.996571 0 18.432-3.437714 28.288-11.154286L512 222.281143l261.851429 213.430857c9.874286 7.716571 19.291429 11.154286 28.708571 11.154286 22.308571 0 39.003429-16.731429 39.003429-39.003429 0-12.854857-4.278857-21.869714-11.556572-28.288L541.147429 144.713143c-10.294857-8.137143-19.291429-11.995429-29.147429-11.995429z m0 758.564572c9.856 0 18.852571-3.84 29.147429-11.995429L829.988571 644.425143c7.277714-6.418286 11.556571-15.433143 11.556572-28.288 0-22.272-16.713143-39.424-38.985143-39.424-9.435429 0-18.870857 3.858286-28.708571 11.574857L512 801.718857 250.148571 588.288c-9.874286-7.716571-19.291429-11.574857-28.288-11.574857-22.710857 0-39.424 17.152-39.424 39.424 0 12.854857 4.278857 21.869714 11.556572 28.288l288.859428 234.422857c10.294857 8.594286 19.712 12.434286 29.147429 12.434286z" p-id="4134"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694049173782" class="icon" viewBox="0 0 1024 1024" width="24" height="24" fill="#fff" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="992" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M891.64 184.73H620.41c-27.41 0-54.41-7.77-77.32-22.5L428.13 87.36C402.77 71 372.91 62 342.64 62H131.95C93.5 62 62 93.5 62 132.36v759.68C62 930.91 93.5 962 131.95 962h759.68c38.86 0 70.36-31.09 70.36-69.96V255.09c0.01-38.86-31.49-70.36-70.35-70.36zM480.5 753.77c0 16.77-13.5 30.68-30.68 30.68-16.77 0-30.68-13.91-30.68-30.68V523.04l-31.91 55.64c-8.59 14.32-27.41 19.64-42.14 11.04-14.32-8.59-19.64-27.41-11.05-41.73l89.18-154.64c6.96-12.27 21.27-18 34.77-14.32 13.09 3.27 22.5 15.55 22.5 29.45v345.29z m209.04-139.5l-89.18 154.64c-5.32 9.82-15.55 15.55-26.59 15.55-2.46 0-5.32-0.41-7.77-1.23-13.5-3.68-22.91-15.55-22.91-29.46V408.5c0-16.77 13.91-30.68 30.68-30.68 17.18 0 30.68 13.91 30.68 30.68v230.73l31.91-55.64c8.59-14.73 27.41-19.64 42.14-11.05 14.73 8.6 19.64 27.01 11.04 41.73z" p-id="993"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
flutter/assets/scam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

View File

@@ -1,2 +1,5 @@
#!/usr/bin/env bash
flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# https://docs.flutter.dev/deployment/ios
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
# no obfuscate, because no easy to check errors
flutter build ipa --release

View File

@@ -75,7 +75,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
@@ -106,7 +106,7 @@ EXTERNAL SOURCES:
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios"
:path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
sqflite:
@@ -141,6 +141,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd
PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1
COCOAPODS: 1.12.0
COCOAPODS: 1.12.1

View File

@@ -208,6 +208,7 @@
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -437,6 +438,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -634,6 +636,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -723,6 +726,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View File

@@ -13,9 +13,7 @@ import Flutter
}
public func dummyMethodToEnforceBundling() {
get_rgba();
// free_rgba(nil);
// get_by_name("", "");
// set_by_name("", "");
dummy_method_to_enforce_bundling();
session_get_rgba(nil);
}
}

View File

@@ -1,122 +1,122 @@
{
"images": [
"images" : [
{
"filename": "Icon-App-20x20@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
"filename" : "Icon-App-20x20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename": "Icon-App-20x20@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
"filename" : "Icon-App-20x20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename": "Icon-App-29x29@1x.png",
"idiom": "iphone",
"scale": "1x",
"size": "29x29"
"filename" : "Icon-App-29x29@1x.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename": "Icon-App-29x29@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "29x29"
"filename" : "Icon-App-29x29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename": "Icon-App-29x29@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "29x29"
"filename" : "Icon-App-29x29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename": "Icon-App-40x40@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "40x40"
"filename" : "Icon-App-40x40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename": "Icon-App-40x40@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "40x40"
"filename" : "Icon-App-40x40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename": "Icon-App-60x60@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
"filename" : "Icon-App-60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename": "Icon-App-60x60@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
"filename" : "Icon-App-60x60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename": "Icon-App-20x20@1x.png",
"idiom": "ipad",
"scale": "1x",
"size": "20x20"
"filename" : "Icon-App-20x20@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename": "Icon-App-20x20@2x.png",
"idiom": "ipad",
"scale": "2x",
"size": "20x20"
"filename" : "Icon-App-20x20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename": "Icon-App-29x29@1x.png",
"idiom": "ipad",
"scale": "1x",
"size": "29x29"
"filename" : "Icon-App-29x29@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename": "Icon-App-29x29@2x.png",
"idiom": "ipad",
"scale": "2x",
"size": "29x29"
"filename" : "Icon-App-29x29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename": "Icon-App-40x40@1x.png",
"idiom": "ipad",
"scale": "1x",
"size": "40x40"
"filename" : "Icon-App-40x40@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename": "Icon-App-40x40@2x.png",
"idiom": "ipad",
"scale": "2x",
"size": "40x40"
"filename" : "Icon-App-40x40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename": "Icon-App-76x76@1x.png",
"idiom": "ipad",
"scale": "1x",
"size": "76x76"
"filename" : "Icon-App-76x76@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename": "Icon-App-76x76@2x.png",
"idiom": "ipad",
"scale": "2x",
"size": "76x76"
"filename" : "Icon-App-76x76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename": "Icon-App-83.5x83.5@2x.png",
"idiom": "ipad",
"scale": "2x",
"size": "83.5x83.5"
"filename" : "Icon-App-83.5x83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename": "Icon-App-1024x1024@1x.png",
"idiom": "ios-marketing",
"scale": "1x",
"size": "1024x1024"
"filename" : "Icon-App-1024x1024@1x.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info": {
"author": "icons_launcher",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}

View File

@@ -1,23 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="48" y="-2"/>
</scene>
</scenes>
</document>

View File

@@ -1,3 +1,3 @@
#import "GeneratedPluginRegistrant.h"
#import "ffi.h"
#import "bridge_generated.h"

View File

@@ -1,4 +0,0 @@
void* get_rgba();
void free_rgba(void*);
void set_by_name(const char*, const char*);
const char* get_by_name(const char*, const char*);

View File

@@ -91,7 +91,6 @@ class IconFont {
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook");
static const IconData checkbox = IconData(0xe7d6, fontFamily: "CheckBox");
}
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
@@ -101,6 +100,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
required this.highlight,
required this.drag_indicator,
required this.shadow,
required this.errorBannerBg,
required this.me,
});
final Color? border;
@@ -108,6 +109,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
final Color? highlight;
final Color? drag_indicator;
final Color? shadow;
final Color? errorBannerBg;
final Color? me;
static final light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
@@ -115,6 +118,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: Color(0xFFE5E5E5),
drag_indicator: Colors.grey[800],
shadow: Colors.black,
errorBannerBg: Color(0xFFFDEEEB),
me: Colors.green,
);
static final dark = ColorThemeExtension(
@@ -123,6 +128,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: Color(0xFF3F3F3F),
drag_indicator: Colors.grey,
shadow: Colors.grey,
errorBannerBg: Color(0xFF470F2D),
me: Colors.greenAccent,
);
@override
@@ -132,6 +139,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
Color? highlight,
Color? drag_indicator,
Color? shadow,
Color? errorBannerBg,
Color? me,
}) {
return ColorThemeExtension(
border: border ?? this.border,
@@ -139,6 +148,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: highlight ?? this.highlight,
drag_indicator: drag_indicator ?? this.drag_indicator,
shadow: shadow ?? this.shadow,
errorBannerBg: errorBannerBg ?? this.errorBannerBg,
me: me ?? this.me,
);
}
@@ -154,6 +165,8 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
highlight: Color.lerp(highlight, other.highlight, t),
drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
shadow: Color.lerp(shadow, other.shadow, t),
errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t),
me: Color.lerp(shadow, other.me, t),
);
}
}
@@ -258,6 +271,32 @@ class MyTheme {
? EdgeInsets.only(left: dialogPadding)
: EdgeInsets.only(left: dialogPadding / 3);
static ScrollbarThemeData scrollbarTheme = ScrollbarThemeData(
thickness: MaterialStateProperty.all(6),
thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.dragged)) {
return Colors.grey[900];
} else if (states.contains(MaterialState.hovered)) {
return Colors.grey[700];
} else {
return Colors.grey[500];
}
}),
crossAxisMargin: 4,
);
static ScrollbarThemeData scrollbarThemeDark = scrollbarTheme.copyWith(
thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
if (states.contains(MaterialState.dragged)) {
return Colors.grey[100];
} else if (states.contains(MaterialState.hovered)) {
return Colors.grey[300];
} else {
return Colors.grey[500];
}
}),
);
static ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
hoverColor: Color.fromARGB(255, 224, 224, 224),
@@ -273,6 +312,7 @@ class MyTheme {
),
),
),
scrollbarTheme: scrollbarTheme,
inputDecorationTheme: isDesktop
? InputDecorationTheme(
fillColor: grayBg,
@@ -357,6 +397,7 @@ class MyTheme {
),
),
),
scrollbarTheme: scrollbarThemeDark,
inputDecorationTheme: isDesktop
? InputDecorationTheme(
fillColor: Color(0xFF24252B),
@@ -383,9 +424,6 @@ class MyTheme {
tabBarTheme: const TabBarTheme(
labelColor: Colors.white70,
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
),
tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null,
@@ -555,7 +593,7 @@ closeConnection({String? id}) {
}
}
void windowOnTop(int? id) async {
Future<void> windowOnTop(int? id) async {
if (!isDesktop) {
return;
}
@@ -614,6 +652,7 @@ class OverlayDialogManager {
int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry;
RxBool mobileActionsOverlayVisible = false.obs;
void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState;
@@ -780,12 +819,14 @@ class OverlayDialogManager {
});
overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay;
mobileActionsOverlayVisible.value = true;
}
void hideMobileActionsOverlay() {
if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null;
mobileActionsOverlayVisible.value = false;
return;
}
}
@@ -954,11 +995,22 @@ void msgBox(SessionID sessionId, String type, String title, String text,
}));
}
if (reconnect != null && title == "Connection Error") {
buttons.insert(
0,
dialogButton('Reconnect', isOutline: true, onPressed: () {
reconnect(dialogManager, sessionId, false);
}));
// `enabled` is used to disable the dialog button once the button is clicked.
final enabled = true.obs;
final button = Obx(
() => dialogButton(
'Reconnect',
isOutline: true,
onPressed: enabled.isTrue
? () {
// Disable the button
enabled.value = false;
reconnect(dialogManager, sessionId, false);
}
: null,
),
);
buttons.insert(0, button);
}
if (link.isNotEmpty) {
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
@@ -1077,7 +1129,7 @@ Color str2color(String str, [alpha = 0xFF]) {
return Color((hash & 0xFF7FFF) | (alpha << 24));
}
Color str2color2(String str, [alpha = 0xFF]) {
Color str2color2(String str, {List<int> existing = const []}) {
Map<String, Color> colorMap = {
"red": Colors.red,
"green": Colors.green,
@@ -1094,10 +1146,10 @@ Color str2color2(String str, [alpha = 0xFF]) {
};
final color = colorMap[str.toLowerCase()];
if (color != null) {
return color.withAlpha(alpha);
return color.withAlpha(0xFF);
}
if (str.toLowerCase() == 'yellow') {
return Colors.yellow.withAlpha(alpha);
return Colors.yellow.withAlpha(0xFF);
}
var hash = 0;
for (var i = 0; i < str.length; i++) {
@@ -1105,7 +1157,15 @@ Color str2color2(String str, [alpha = 0xFF]) {
}
List<Color> colorList = colorMap.values.toList();
hash = hash % colorList.length;
return colorList[hash].withAlpha(alpha);
var result = colorList[hash].withAlpha(0xFF);
if (existing.contains(result.value)) {
Color? notUsed =
colorList.firstWhereOrNull((e) => !existing.contains(e.value));
if (notUsed != null) {
result = notUsed;
}
}
return result;
}
const K = 1024;
@@ -1381,9 +1441,10 @@ class LastWindowPosition {
double? offsetWidth;
double? offsetHeight;
bool? isMaximized;
bool? isFullscreen;
LastWindowPosition(this.width, this.height, this.offsetWidth,
this.offsetHeight, this.isMaximized);
this.offsetHeight, this.isMaximized, this.isFullscreen);
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -1392,6 +1453,7 @@ class LastWindowPosition {
"offsetWidth": offsetWidth,
"offsetHeight": offsetHeight,
"isMaximized": isMaximized,
"isFullscreen": isFullscreen,
};
}
@@ -1407,7 +1469,7 @@ class LastWindowPosition {
try {
final m = jsonDecode(content);
return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
m["offsetHeight"], m["isMaximized"]);
m["offsetHeight"], m["isMaximized"], m["isFullscreen"]);
} catch (e) {
debugPrintStack(
label:
@@ -1428,6 +1490,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
late Offset position;
late Size sz;
late bool isMaximized;
bool isFullscreen = stateGlobal.fullscreen ||
(Platform.isMacOS && stateGlobal.closeOnFullscreen);
setFrameIfMaximized() {
if (isMaximized) {
final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
@@ -1473,20 +1537,21 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
}
final pos = LastWindowPosition(
sz.width, sz.height, position.dx, position.dy, isMaximized);
sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
debugPrint(
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}");
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
await bind.setLocalFlutterOption(
k: kWindowPrefix + type.name, v: pos.toString());
if (type == WindowType.RemoteDesktop && windowId != null) {
await _saveSessionWindowPosition(type, windowId, isMaximized, pos);
await _saveSessionWindowPosition(
type, windowId, isMaximized, isFullscreen, pos);
}
}
Future _saveSessionWindowPosition(WindowType windowType, int windowId,
bool isMaximized, LastWindowPosition pos) async {
bool isMaximized, bool isFullscreen, LastWindowPosition pos) async {
final remoteList = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventGetRemoteList, null);
getPeerPos(String peerId) {
@@ -1499,7 +1564,8 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
lpos?.height ?? pos.offsetHeight,
lpos?.offsetWidth ?? pos.offsetWidth,
lpos?.offsetHeight ?? pos.offsetHeight,
isMaximized)
isMaximized,
isFullscreen)
.toString();
} else {
return pos.toString();
@@ -1689,9 +1755,18 @@ Future<bool> restoreWindowPosition(WindowType type,
await wc.setFrame(frame);
}
}
if (lpos.isMaximized == true) {
if (lpos.isFullscreen == true) {
await restoreFrame();
await wc.maximize();
// An duration is needed to avoid the window being restored after fullscreen.
Future.delayed(Duration(milliseconds: 300), () async {
stateGlobal.setFullscreen(true);
});
} else if (lpos.isMaximized == true) {
await restoreFrame();
// An duration is needed to avoid the window being restored after maximized.
Future.delayed(Duration(milliseconds: 300), () async {
await wc.maximize();
});
} else {
await restoreFrame();
}
@@ -1759,10 +1834,10 @@ enum UriLinkType {
// uri link handler
bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
List<String>? args;
if (cmdArgs != null) {
if (cmdArgs != null && cmdArgs.isNotEmpty) {
args = cmdArgs;
// rustdesk <uri link>
if (args.isNotEmpty && args[0].startsWith(kUniLinksPrefix)) {
if (args[0].startsWith(kUniLinksPrefix)) {
final uri = Uri.tryParse(args[0]);
if (uri != null) {
args = urlLinkToCmdArgs(uri);
@@ -2263,7 +2338,7 @@ String getWindowName({WindowType? overrideType}) {
}
String getWindowNameWithId(String id, {WindowType? overrideType}) {
return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}";
return "${DesktopTab.tablabelGetter(id).value} - ${getWindowName(overrideType: overrideType)}";
}
Future<void> updateSystemWindowTheme() async {
@@ -2440,3 +2515,77 @@ String toCapitalized(String s) {
}
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
Widget buildErrorBanner(BuildContext context,
{required RxBool loading,
required RxString err,
required Function? retry,
required Function close}) {
const double height = 25;
return Obx(() => Offstage(
offstage: !(!loading.value && err.value.isNotEmpty),
child: Center(
child: Container(
height: height,
color: MyTheme.color(context).errorBannerBg,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FittedBox(
child: Icon(
Icons.info,
color: Color.fromARGB(255, 249, 81, 81),
),
).marginAll(4),
Flexible(
child: Align(
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
if (retry != null)
InkWell(
onTap: () {
retry.call();
},
child: Text(
translate("Retry"),
style: TextStyle(color: MyTheme.accent),
)).marginSymmetric(horizontal: 5),
FittedBox(
child: InkWell(
onTap: () {
close.call();
},
child: Icon(Icons.close).marginSymmetric(horizontal: 5),
),
).marginAll(4)
],
),
)).marginOnly(bottom: 14),
));
}
String getDesktopTabLabel(String peerId, String alias) {
String label = alias.isEmpty ? peerId : alias;
try {
String peer = bind.mainGetPeerSync(id: peerId);
Map<String, dynamic> config = jsonDecode(peer);
if (config['info']['hostname'] is String) {
String hostname = config['info']['hostname'];
if (hostname.isNotEmpty &&
!label.toLowerCase().contains(hostname.toLowerCase())) {
label += "@$hostname";
}
}
} catch (e) {
debugPrint("Failed to get hostname:$e");
}
return label;
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart';
@@ -48,11 +49,18 @@ class UserPayload {
};
return map;
}
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
};
return map;
}
}
class PeerPayload {
String id = '';
String info = '';
Map<String, dynamic> info = {};
int? status;
String user = '';
String user_name = '';
@@ -60,14 +68,45 @@ class PeerPayload {
PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
info = json['info'] ?? '',
info = (json['info'] is Map<String, dynamic>) ? json['info'] : {},
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
return Peer.fromJson({"id": p.id, "username": p.user_name});
return Peer.fromJson({
"id": p.id,
'loginName': p.user_name,
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
});
}
static String? _platform(dynamic field) {
if (field == null) {
return null;
}
final fieldStr = field.toString();
List<String> list = fieldStr.split(' / ');
if (list.isEmpty) return null;
final os = list[0];
switch (os.toLowerCase()) {
case 'windows':
return kPeerPlatformWindows;
case 'linux':
return kPeerPlatformLinux;
case 'macos':
return kPeerPlatformMacOS;
case 'android':
return kPeerPlatformAndroid;
default:
if (fieldStr.toLowerCase().contains('linux')) {
return kPeerPlatformLinux;
}
return null;
}
}
}

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
@@ -7,6 +10,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import '../../common.dart';
import 'dialog.dart';
@@ -34,7 +38,7 @@ class _AddressBookState extends State<AddressBook> {
@override
Widget build(BuildContext context) => Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
if (!gFFI.userModel.isLogin) {
return Center(
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
@@ -48,11 +52,13 @@ class _AddressBookState extends State<AddressBook> {
children: [
// NOT use Offstage to wrap LinearProgressIndicator
if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
_buildErrorBanner(
buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pullError,
retry: null,
close: () => gFFI.abModel.pullError.value = ''),
_buildErrorBanner(
buildErrorBanner(context,
loading: gFFI.abModel.abLoading,
err: gFFI.abModel.pushError,
retry: () => gFFI.abModel.pushAb(isRetry: true),
close: () => gFFI.abModel.pushError.value = ''),
@@ -65,61 +71,6 @@ class _AddressBookState extends State<AddressBook> {
}
});
Widget _buildErrorBanner(
{required RxString err,
required Function? retry,
required Function close}) {
const double height = 25;
return Obx(() => Offstage(
offstage: !(!gFFI.abModel.abLoading.value && err.value.isNotEmpty),
child: Center(
child: Container(
height: height,
color: Color.fromARGB(255, 253, 238, 235),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FittedBox(
child: Icon(
Icons.info,
color: Color.fromARGB(255, 249, 81, 81),
),
).marginAll(4),
Flexible(
child: Align(
alignment: Alignment.centerLeft,
child: Tooltip(
message: translate(err.value),
child: Text(
translate(err.value),
overflow: TextOverflow.ellipsis,
),
)).marginSymmetric(vertical: 2),
),
if (retry != null)
InkWell(
onTap: () {
retry.call();
},
child: Text(
translate("Retry"),
style: TextStyle(color: MyTheme.accent),
)).marginSymmetric(horizontal: 5),
FittedBox(
child: InkWell(
onTap: () {
close.call();
},
child: Icon(Icons.close).marginSymmetric(horizontal: 5),
),
).marginAll(4)
],
),
)).marginOnly(bottom: 14),
));
}
Widget _buildAddressBookDesktop() {
return Row(
children: [
@@ -208,20 +159,31 @@ class _AddressBookState extends State<AddressBook> {
} else {
tags = gFFI.abModel.tags;
}
return Wrap(
children: tags
.map((e) => AddressBookTag(
name: e,
tags: gFFI.abModel.selectedTags,
onTap: () {
if (gFFI.abModel.selectedTags.contains(e)) {
gFFI.abModel.selectedTags.remove(e);
} else {
gFFI.abModel.selectedTags.add(e);
}
}))
.toList(),
);
tagBuilder(String e) {
return AddressBookTag(
name: e,
tags: gFFI.abModel.selectedTags,
onTap: () {
if (gFFI.abModel.selectedTags.contains(e)) {
gFFI.abModel.selectedTags.remove(e);
} else {
gFFI.abModel.selectedTags.add(e);
}
});
}
final gridView = DynamicGridView.builder(
shrinkWrap: isMobile,
gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length,
itemBuilder: (BuildContext context, int index) {
final e = tags[index];
return tagBuilder(e);
});
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView);
});
}
@@ -229,11 +191,10 @@ class _AddressBookState extends State<AddressBook> {
return Expanded(
child: Align(
alignment: Alignment.topLeft,
child: Obx(() => AddressBookPeersView(
menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member
initPeers: gFFI.abModel.peers.value,
))),
child: AddressBookPeersView(
menuPadding: widget.menuPadding,
initPeers: gFFI.abModel.peers,
)),
);
}
@@ -268,6 +229,22 @@ class _AddressBookState extends State<AddressBook> {
);
}
@protected
MenuEntryBase<String> filterMenuItem() {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate('Filter by intersection'),
getter: () async {
return filterAbTagByIntersection();
},
setter: (bool v) async {
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
gFFI.abModel.filterByIntersection.value = v;
},
dismissOnClicked: true,
);
}
void _showMenu(RelativeRect pos) {
final items = [
getEntry(translate("Add ID"), abAddId),
@@ -275,6 +252,7 @@ class _AddressBookState extends State<AddressBook> {
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(),
syncMenuItem(),
filterMenuItem(),
];
mod_menu.showMenu(
@@ -513,7 +491,7 @@ class AddressBookTag extends StatelessWidget {
child: Obx(() => Container(
decoration: BoxDecoration(
color: tags.contains(name)
? str2color2(name, 0xFF)
? gFFI.abModel.getTagColor(name)
: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(4)),
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
@@ -528,7 +506,7 @@ class AddressBookTag extends StatelessWidget {
shape: BoxShape.circle,
color: tags.contains(name)
? Colors.white
: str2color2(name)),
: gFFI.abModel.getTagColor(name)),
).marginOnly(right: radius / 2),
Expanded(
child: Text(name,
@@ -568,6 +546,30 @@ class AddressBookTag extends StatelessWidget {
Future.delayed(Duration.zero, () => Get.back());
});
}),
getEntry(translate(translate('Change Color')), () async {
final model = gFFI.abModel;
Color oldColor = model.getTagColor(name);
Color newColor = await showColorPickerDialog(
context,
oldColor,
pickersEnabled: {
ColorPickerType.accent: false,
ColorPickerType.wheel: true,
},
pickerTypeLabels: {
ColorPickerType.primary: translate("Primary Color"),
ColorPickerType.wheel: translate("HSV Color"),
},
actionButtons: ColorPickerActionButtons(
dialogOkButtonLabel: translate("OK"),
dialogCancelButtonLabel: translate("Cancel")),
showColorCode: true,
);
if (oldColor != newColor) {
model.setTagColor(name, newColor);
model.pushAb();
}
}),
getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb();

View File

@@ -1,9 +1,9 @@
import 'dart:async';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -302,6 +302,53 @@ Future<String> changeDirectAccessPort(
return controller.text;
}
Future<String> changeAutoDisconnectTimeout(String old) async {
final controller = TextEditingController(text: old);
await gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate("Timeout in minutes")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8.0),
Row(
children: [
Expanded(
child: TextField(
maxLines: null,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '10',
isCollapsed: true,
suffix: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.clear, size: 16),
onPressed: () => controller.clear())),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
autofocus: true),
),
],
),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async {
await bind.mainSetOption(
key: 'auto-disconnect-timeout', value: controller.text);
close();
}),
],
onCancel: close,
);
});
return controller.text;
}
class DialogTextField extends StatelessWidget {
final String title;
final String? hintText;
@@ -664,6 +711,13 @@ void showWaitUacDialog(
(setState, close, context) => CustomAlertDialog(
title: null,
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
actions: [
dialogButton(
'OK',
icon: Icon(Icons.done_rounded),
onPressed: close,
),
],
));
}
@@ -812,6 +866,8 @@ void showRequestElevationDialog(
} else {
bind.sessionElevateDirect(sessionId: sessionId);
}
close();
showWaitUacDialog(sessionId, dialogManager, "wait-uac");
}
return CustomAlertDialog(
@@ -882,7 +938,7 @@ void showElevationError(SessionID sessionId, String type, String title,
dialogButton('Cancel', onPressed: () {
close();
}, isOutline: true),
dialogButton('Retry', onPressed: submit),
if (text != 'No permission') dialogButton('Retry', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@@ -1223,76 +1279,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
qualityInitValue =
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
const qualityMinValue = 10.0;
const qualityMoreThresholdValue = 100.0;
const qualityMaxValue = 2000.0;
if (qualityInitValue < qualityMinValue) {
qualityInitValue = qualityMinValue;
if (qualityInitValue < 10 || qualityInitValue > 2000) {
qualityInitValue = 50;
}
if (qualityInitValue > qualityMaxValue) {
qualityInitValue = qualityMaxValue;
}
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
final moreQualityInitValue = qualityInitValue > qualityMoreThresholdValue;
final RxBool moreQualityChecked = RxBool(moreQualityInitValue);
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(quality: v);
},
initialValue: qualityInitValue,
);
final qualitySlider = Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: qualitySliderValue.value,
min: qualityMinValue,
max: moreQualityChecked.value
? qualityMaxValue
: qualityMoreThresholdValue,
divisions: 18,
onChanged: (double value) {
qualitySliderValue.value = value;
debouncerQuality.value = value;
},
)),
Expanded(
flex: 1,
child: Text(
'${qualitySliderValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 1,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 1,
child: Row(
children: [
Checkbox(
value: moreQualityChecked.value,
onChanged: (bool? value) {
moreQualityChecked.value = value!;
if (!value &&
qualitySliderValue.value >
qualityMoreThresholdValue) {
qualitySliderValue.value = qualityMoreThresholdValue;
debouncerQuality.value = qualityMoreThresholdValue;
}
},
).marginOnly(right: 5),
Expanded(
child: Text(translate('More')),
)
],
)),
],
));
// fps
final fpsOption =
await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
@@ -1300,55 +1289,20 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
if (fpsInitValue < 5 || fpsInitValue > 120) {
fpsInitValue = 30;
}
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(fps: v);
},
initialValue: qualityInitValue,
);
bool? direct;
try {
direct =
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
} catch (_) {}
final fpsSlider = Offstage(
offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
child: Row(
children: [
Expanded(
flex: 3,
child: Obx((() => Slider(
value: fpsSliderValue.value,
min: 5,
max: 120,
divisions: 23,
onChanged: (double value) {
fpsSliderValue.value = value;
debouncerFps.value = value;
},
)))),
Expanded(
flex: 1,
child: Obx(() => Text(
'${fpsSliderValue.value.round()}',
style: const TextStyle(fontSize: 15),
))),
Expanded(
flex: 2,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
),
);
bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
final content = Column(
children: [qualitySlider, fpsSlider],
);
final content = customImageQualityWidget(
initQuality: qualityInitValue,
initFps: fpsInitValue,
setQuality: (v) => setCustomValues(quality: v),
setFps: (v) => setCustomValues(fps: v),
showFps: !notShowFps);
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
}

View File

@@ -14,6 +14,7 @@ import './dialog.dart';
const kOpSvgList = [
'github',
'gitlab',
'google',
'apple',
'okta',
@@ -72,6 +73,11 @@ class ButtonOP extends StatelessWidget {
@override
Widget build(BuildContext context) {
final opLabel = {
'github': 'GitHub',
'gitlab': 'GitLab'
}[op.toLowerCase()] ??
toCapitalized(op);
return Row(children: [
Container(
height: height,
@@ -97,8 +103,7 @@ class ButtonOP extends StatelessWidget {
child: FittedBox(
fit: BoxFit.scaleDown,
child: Center(
child: Text(
'${translate("Continue with")} ${op.toLowerCase() == "github" ? "GitHub" : toCapitalized(op)}')),
child: Text('${translate("Continue with")} $opLabel')),
),
),
],

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart';
@@ -29,49 +31,28 @@ class _MyGroupState extends State<MyGroup> {
@override
Widget build(BuildContext context) {
return Obx(() {
// use username to be same with ab
if (gFFI.userModel.userName.value.isEmpty) {
if (!gFFI.userModel.isLogin) {
return Center(
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
}
return buildBody(context);
});
}
Widget buildBody(BuildContext context) {
return Obx(() {
if (gFFI.groupModel.groupLoading.value) {
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (gFFI.groupModel.groupLoadError.isNotEmpty) {
return _buildShowError(gFFI.groupModel.groupLoadError.value);
}
if (isDesktop) {
return _buildDesktop();
} else {
return _buildMobile();
}
return Column(
children: [
buildErrorBanner(context,
loading: gFFI.groupModel.groupLoading,
err: gFFI.groupModel.groupLoadError,
retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded(child: isDesktop ? _buildDesktop() : _buildMobile())
],
);
});
}
Widget _buildShowError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(error)),
TextButton(
onPressed: () {
gFFI.groupModel.pull();
},
child: Text(translate("Retry")))
],
));
}
Widget _buildDesktop() {
return Row(
children: [
@@ -100,10 +81,9 @@ class _MyGroupState extends State<MyGroup> {
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView(
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member
initPeers: gFFI.groupModel.peersShow.value))),
initPeers: gFFI.groupModel.peers)),
)
],
);
@@ -133,16 +113,16 @@ class _MyGroupState extends State<MyGroup> {
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: Obx(() => MyGroupPeerView(
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
// ignore: invalid_use_of_protected_member
initPeers: gFFI.groupModel.peersShow.value))),
initPeers: gFFI.groupModel.peers)),
)
],
);
}
Widget _buildLeftHeader() {
final fontSize = 14.0;
return Row(
children: [
Expanded(
@@ -151,16 +131,16 @@ class _MyGroupState extends State<MyGroup> {
onChanged: (value) {
searchUserText.value = value;
},
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
decoration: InputDecoration(
filled: false,
prefixIcon: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
).paddingOnly(top: 2),
hintText: translate("Search"),
hintStyle:
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
hintStyle: TextStyle(fontSize: fontSize),
border: InputBorder.none,
isDense: true,
),
@@ -171,16 +151,22 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserContacts() {
return Obx(() {
return Column(
children: gFFI.groupModel.users
.where((p0) {
if (searchUserText.isNotEmpty) {
return p0.name.contains(searchUserText.value);
}
return true;
})
.map((e) => _buildUserItem(e))
.toList());
final items = gFFI.groupModel.users.where((p0) {
if (searchUserText.isNotEmpty) {
return p0.name
.toLowerCase()
.contains(searchUserText.value.toLowerCase());
}
return true;
}).toList();
final listView = ListView.builder(
shrinkWrap: isMobile,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop
? listView
: LimitedBox(maxHeight: maxHeight, child: listView);
});
}
@@ -195,6 +181,8 @@ class _MyGroupState extends State<MyGroup> {
}, child: Obx(
() {
bool selected = selectedUser.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container(
decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null,
@@ -206,9 +194,42 @@ class _MyGroupState extends State<MyGroup> {
child: Container(
child: Row(
children: [
Icon(Icons.person_rounded, color: Colors.grey, size: 16)
.marginOnly(right: 4),
Expanded(child: Text(username)),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: str2color(username, 0xAF),
shape: BoxShape.circle,
),
child: Align(
alignment: Alignment.center,
child: Center(
child: Text(
username.characters.first.toUpperCase(),
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
).marginOnly(right: 4),
if (isMe) Flexible(child: Text(username)),
if (isMe)
Flexible(
child: Container(
margin: EdgeInsets.only(left: 5),
padding: EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: colorMe.withAlpha(20),
borderRadius: BorderRadius.all(Radius.circular(2)),
border: Border.all(color: colorMe.withAlpha(100))),
child: Text(
translate('Me'),
style: TextStyle(
color: colorMe.withAlpha(200), fontSize: 12),
),
),
),
if (!isMe) Expanded(child: Text(username)),
],
).paddingSymmetric(vertical: 4),
),

View File

@@ -26,15 +26,32 @@ class DraggableChatWindow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Draggable(
return isIOS
? IOSDraggable (
position: position,
chatModel: chatModel,
width: width,
height: height,
builder: (context) {
return Column(
children: [
_buildMobileAppBar(context),
Expanded(
child: ChatPage(chatModel: chatModel),
),
],
);
},
)
: Draggable(
checkKeyboard: true,
position: position,
width: width,
height: height,
chatModel: chatModel,
builder: (context, onPanUpdate) {
final child = isIOS
? ChatPage(chatModel: chatModel)
: Scaffold(
final child =
Scaffold(
resizeToAvoidBottomInset: false,
appBar: CustomAppBar(
onPanUpdate: onPanUpdate,
@@ -226,6 +243,7 @@ class Draggable extends StatefulWidget {
this.position = Offset.zero,
required this.width,
required this.height,
this.chatModel,
required this.builder})
: super(key: key);
@@ -234,6 +252,7 @@ class Draggable extends StatefulWidget {
final Offset position;
final double width;
final double height;
final ChatModel? chatModel;
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override
@@ -242,6 +261,7 @@ class Draggable extends StatefulWidget {
class _DraggableState extends State<Draggable> {
late Offset _position;
late ChatModel? _chatModel;
bool _keyboardVisible = false;
double _saveHeight = 0;
double _lastBottomHeight = 0;
@@ -250,6 +270,7 @@ class _DraggableState extends State<Draggable> {
void initState() {
super.initState();
_position = widget.position;
_chatModel = widget.chatModel;
}
void onPanUpdate(DragUpdateDetails d) {
@@ -276,6 +297,7 @@ class _DraggableState extends State<Draggable> {
setState(() {
_position = Offset(x, y);
});
_chatModel?.setChatWindowPosition(_position);
}
checkScreenSize() {}
@@ -331,6 +353,107 @@ class _DraggableState extends State<Draggable> {
}
}
class IOSDraggable extends StatefulWidget {
const IOSDraggable({
Key? key,
this.position = Offset.zero,
this.chatModel,
required this.width,
required this.height,
required this.builder})
: super(key: key);
final Offset position;
final ChatModel? chatModel;
final double width;
final double height;
final Widget Function(BuildContext) builder;
@override
_IOSDraggableState createState() => _IOSDraggableState();
}
class _IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel;
late double _width;
late double _height;
bool _keyboardVisible = false;
double _saveHeight = 0;
double _lastBottomHeight = 0;
@override
void initState() {
super.initState();
_position = widget.position;
_chatModel = widget.chatModel;
_width = widget.width;
_height = widget.height;
}
checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0;
// save
if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy;
}
// reset
if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() {
_position = Offset(_position.dx, _saveHeight);
});
}
// onKeyboardVisible
if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) {
final y = contextHeight - sumHeight;
setState(() {
_position = Offset(_position.dx, y);
});
}
}
_keyboardVisible = currentVisible;
_lastBottomHeight = bottomHeight;
}
@override
Widget build(BuildContext context) {
checkKeyboard();
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
});
_chatModel?.setChatWindowPosition(_position);
},
child: Material(
child:
Container(
width: _width,
height: _height,
decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
child: widget.builder(context),
),
),
),
),
],
);
}
}
class QualityMonitor extends StatelessWidget {
final QualityMonitorModel qualityMonitorModel;
QualityMonitor(this.qualityMonitorModel);

View File

@@ -1,11 +1,9 @@
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -63,75 +61,29 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildMobile() {
final peer = super.widget.peer;
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final PeerTabModel peerTabModel = Provider.of(context);
final child = Card(
return Card(
margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector(
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
},
child: Container(
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer.id, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer.id, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: BorderRadius.circular(4),
),
padding: const EdgeInsets.all(6),
child: getPlatformImage(peer.platform)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
getOnline(4, peer.online),
Text(peer.alias.isEmpty
? formatID(peer.id)
: peer.alias)
]),
Text(name)
],
).paddingOnly(left: 8.0),
),
checkBoxOrActionMoreMobile(peer),
],
),
)));
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList();
return Tooltip(
message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
if (colors.isNotEmpty)
Positioned(
top: 2,
right: 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
)
]),
);
child: _buildPeerTile(context, peer, null)),
));
}
Widget _buildDesktop() {
@@ -178,87 +130,96 @@ class _PeerCardState extends State<_PeerCard>
}
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Obx(
() => Container(
foregroundDecoration: deco.value,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
alignment: Alignment.center,
width: 42,
child: getPlatformImage(peer.platform, size: 30).paddingAll(6),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
final child = Row(
mainAxisSize: MainAxisSize.max,
children: [
Container(
decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile
? BorderRadius.circular(_tileRadius)
: BorderRadius.only(
topLeft: Radius.circular(_tileRadius),
bottomLeft: Radius.circular(_tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(children: [
getOnline(8, peer.online),
Expanded(
child: Text(
peer.alias.isEmpty
? formatID(peer.id)
: peer.alias,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(bottom: 0, top: 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
),
alignment: Alignment.center,
width: isMobile ? 50 : 42,
height: isMobile ? 50 : null,
child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
.paddingAll(6),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.only(
topRight: Radius.circular(_tileRadius),
bottomRight: Radius.circular(_tileRadius),
),
),
child: Row(
children: [
Expanded(
child: Column(
children: [
Row(children: [
getOnline(isMobile ? 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),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isMobile ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
],
).marginOnly(top: 2),
),
isMobile
? checkBoxOrActionMoreMobile(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true),
],
).paddingOnly(left: 10.0, top: 3.0),
),
)
],
);
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList();
final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip(
message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
message: isMobile
? ''
: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '',
child: Stack(children: [
child,
deco == null
? child
: Obx(
() => Container(
foregroundDecoration: deco.value,
child: child,
),
),
if (colors.isNotEmpty)
Positioned(
top: 2,
right: 10,
right: isMobile ? 20 : 10,
child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors),
),
@@ -349,7 +310,8 @@ class _PeerCardState extends State<_PeerCard>
),
);
final colors = _frontN(peer.tags, 25).map((e) => str2color2(e)).toList();
final colors =
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
return Tooltip(
message: peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}'
@@ -765,17 +727,18 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _unrememberPasswordAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Unremember Password'),
translate('Forget Password'),
style: style,
),
proc: () async {
bool result = gFFI.abModel.changePassword(id, '');
await bind.mainForgetPassword(id: id);
bool toast = false;
if (result) {
bool toast = tab == PeerTabIndex.ab;
toast = tab == PeerTabIndex.ab;
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
}
showToast(translate('Successful'));
if (!toast) showToast(translate('Successful'));
},
padding: menuPadding,
dismissOnClicked: true,
@@ -900,12 +863,12 @@ class RecentPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
@@ -954,12 +917,12 @@ class FavoritePeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
@@ -1008,12 +971,12 @@ class DiscoveredPeerCard extends BasePeerCard {
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(_wolAction(peer.id));
@@ -1058,12 +1021,12 @@ class AddressBookPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
@@ -1126,21 +1089,26 @@ class MyGroupPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
// menuItems.add(MenuEntryDivider());
// menuItems.add(_renameAction(peer.id));
// if (await bind.mainPeerHasPassword(id: peer.id)) {
// menuItems.add(_unrememberPasswordAction(peer.id));
// }
if (gFFI.userModel.userName.isNotEmpty) {
if (!gFFI.abModel.idContainBy(peer.id)) {
menuItems.add(_addToAb(peer));
}
}
return menuItems;
}

View File

@@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
@@ -5,14 +7,18 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:pull_down_button/pull_down_button.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
@@ -63,6 +69,7 @@ class _PeerTabPageState extends State<PeerTabPage>
({dynamic hint}) => gFFI.groupModel.pull(force: hint == null),
),
];
RelativeRect? mobileTabContextMenuPos;
@override
void initState() {
@@ -102,40 +109,17 @@ class _PeerTabPageState extends State<PeerTabPage>
child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: _createSwitchBar(context)),
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(),
_createMultiSelection(),
Offstage(
offstage: !isDesktop,
child: _createPeerViewTypeSwitch(context)),
Offstage(
offstage: gFFI.peerTabModel.currentTab == 0,
child: PeerSortDropdown(),
),
Offstage(
offstage: gFFI.peerTabModel.currentTab != 3,
child: _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel",
value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
},
),
),
Expanded(
child:
visibleContextMenuListener(_createSwitchBar(context))),
if (isMobile)
..._mobileRightActions(context)
else
..._desktopRightActions(context)
],
)),
),
),
).paddingOnly(right: isDesktop ? 12 : 0),
_createPeersView(),
],
);
@@ -147,7 +131,7 @@ class _PeerTabPageState extends State<PeerTabPage>
return ListView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
children: model.indexs.map((t) {
children: model.visibleIndexs.map((t) {
final selected = model.currentTab == t;
final color = selected
? MyTheme.tabbar(context).selectedTextColor
@@ -163,11 +147,13 @@ class _PeerTabPageState extends State<PeerTabPage>
));
return Obx(() => InkWell(
child: Container(
decoration:
selected ? decoBorder : (hover.value ? deco : null),
decoration: (hover.value
? (selected ? decoBorder : deco)
: (selected ? decoBorder : null)),
child: Tooltip(
message:
model.tabTooltip(t, gFFI.groupModel.groupName.value),
preferBelow: false,
message: model.tabTooltip(t),
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
child: Icon(model.tabIcon(t), color: color),
).paddingSymmetric(horizontal: 4),
).paddingSymmetric(horizontal: 4),
@@ -184,14 +170,15 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createPeersView() {
final model = Provider.of<PeerTabModel>(context);
Widget child;
if (model.indexs.isEmpty) {
child = Center(
child: Text(translate('Right click to select tabs')),
);
if (model.visibleIndexs.isEmpty) {
child = visibleContextMenuListener(Row(
children: [Expanded(child: InkWell())],
));
} else {
if (model.indexs.contains(model.currentTab)) {
if (model.visibleIndexs.contains(model.currentTab)) {
child = entries[model.currentTab].widget;
} else {
debugPrint("should not happen! currentTab not in visibleIndexs");
Future.delayed(Duration.zero, () {
model.setCurrentTab(model.indexs[0]);
});
@@ -202,17 +189,19 @@ class _PeerTabPageState extends State<PeerTabPage>
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
}
Widget _createRefresh() {
Widget _createRefresh(
{required PeerTabIndex index, required RxBool loading}) {
final model = Provider.of<PeerTabModel>(context);
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage(
offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index,
offstage: model.currentTab != index.index,
child: RefreshWidget(
onPressed: () {
if (gFFI.peerTabModel.currentTab < entries.length) {
entries[gFFI.peerTabModel.currentTab].load();
}
},
spinning: gFFI.abModel.abLoading,
spinning: loading,
child: RotatedBox(
quarterTurns: 2,
child: Tooltip(
@@ -254,22 +243,113 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createMultiSelection() {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context);
if (model.currentTabCachedPeers.isEmpty) return Offstage();
return _hoverAction(
context: context,
onTap: () {
model.setMultiSelectionMode(true);
if (isMobile && Navigator.canPop(context)) {
Navigator.pop(context);
}
},
child: Tooltip(
message: translate('Select'),
child: Icon(
IconFont.checkbox,
size: 18,
child: SvgPicture.asset(
"assets/checkbox-outline.svg",
width: 18,
height: 18,
color: textColor,
)),
);
}
void mobileShowTabVisibilityMenu() {
final model = gFFI.peerTabModel;
final items = List<PopupMenuItem>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisible[i]),
child: Row(
children: [
Checkbox(
value: model.isVisible[i],
onChanged: (_) {
model.setTabVisible(i, !model.isVisible[i]);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}),
Expanded(child: Text(model.tabTooltip(i))),
],
),
));
}
if (mobileTabContextMenuPos != null) {
showMenu(
context: context, position: mobileTabContextMenuPos!, items: items);
}
}
Widget visibleContextMenuListener(Widget child) {
if (isMobile) {
return GestureDetector(
onLongPressDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
mobileTabContextMenuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onLongPressUp: () {
mobileShowTabVisibilityMenu();
},
child: child,
);
} else {
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
if (e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return visibleContextMenu(cancelFunc);
},
target: e.position,
);
}
},
child: child);
}
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
final model = Provider.of<PeerTabModel>(context);
final menu = List<MenuEntrySwitch>.empty(growable: true);
for (int i = 0; i < model.tabNames.length; i++) {
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: model.tabTooltip(i),
getter: () async {
return model.isVisible[i];
},
setter: (show) async {
model.setTabVisible(i, show);
cancelFunc();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
}
Widget createMultiSelectionBar() {
final model = Provider.of<PeerTabModel>(context);
return Row(
@@ -288,6 +368,9 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget deleteSelection() {
final model = Provider.of<PeerTabModel>(context);
if (model.currentTab == PeerTabIndex.group.index) {
return Offstage();
}
return _hoverAction(
context: context,
onTap: () {
@@ -457,6 +540,130 @@ class _PeerTabPageState extends State<PeerTabPage>
Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
.marginOnly(left: 6);
}
Widget _toggleTags() {
return _hoverAction(
context: context,
hoverableWhenfalse: hideAbTagsPanel,
child: Tooltip(
message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
)),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
});
}
List<Widget> _desktopRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
Offstage(
offstage: model.currentTabCachedPeers.isEmpty,
child: _createMultiSelection(),
),
_createPeerViewTypeSwitch(context),
Offstage(
offstage: model.currentTab == PeerTabIndex.recent.index,
child: PeerSortDropdown(),
),
Offstage(
offstage: model.currentTab != PeerTabIndex.ab.index,
child: _toggleTags(),
),
];
}
List<Widget> _mobileRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
final leftActionsSize =
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length;
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
final searchWidth = 120;
final otherActionWidth = 18 + 10;
dropDown(List<Widget> menus) {
final padding = 6.0;
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return PullDownButton(
buttonBuilder:
(BuildContext context, Future<void> Function() showMenu) {
return _hoverAction(
context: context,
child: Tooltip(
message: translate('More'),
child: SvgPicture.asset(
"assets/chevron_up_chevron_down.svg",
width: 18,
height: 18,
color: textColor,
)),
onTap: showMenu,
);
},
routeTheme: PullDownMenuRouteTheme(
width: menus.length * (otherActionWidth + padding * 2) * 1.0),
itemBuilder: (context) => [
PullDownMenuEntryImpl(
child: Row(
mainAxisSize: MainAxisSize.min,
children: menus
.map((e) =>
Material(child: e.paddingSymmetric(horizontal: padding)))
.toList(),
),
)
],
);
}
// Always show search, refresh
List<Widget> actions = [
const PeerSearchBar(),
if (model.currentTab == PeerTabIndex.ab.index)
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
if (model.currentTab == PeerTabIndex.group.index)
_createRefresh(
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
];
final List<Widget> dynamicActions = [
if (model.currentTabCachedPeers.isNotEmpty) _createMultiSelection(),
if (model.currentTab != PeerTabIndex.recent.index) PeerSortDropdown(),
if (model.currentTab == PeerTabIndex.ab.index) _toggleTags()
];
final rightWidth = availableWidth -
searchWidth -
(actions.length == 2 ? otherActionWidth : 0);
final availablePositions = rightWidth ~/ otherActionWidth;
debugPrint(
"dynamic action count:${dynamicActions.length}, available positions: $availablePositions");
if (availablePositions < dynamicActions.length &&
dynamicActions.length > 1) {
if (availablePositions < 2) {
actions.addAll([
dropDown(dynamicActions),
]);
} else {
actions.addAll([
...dynamicActions.sublist(0, availablePositions - 1),
dropDown(dynamicActions.sublist(availablePositions - 1)),
]);
}
} else {
actions.addAll(dynamicActions);
}
return actions;
}
}
class PeerSearchBar extends StatefulWidget {
@@ -732,3 +939,14 @@ Widget _hoverAction(
child: Container(padding: padding, child: child))),
);
}
class PullDownMenuEntryImpl extends StatelessWidget
implements PullDownMenuEntry {
final Widget child;
const PullDownMenuEntryImpl({super.key, required this.child});
@override
Widget build(BuildContext context) {
return child;
}
}

View File

@@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:collection';
import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -35,6 +37,7 @@ class LoadEvent {
static const String favorite = 'load_fav_peers';
static const String lan = 'load_lan_peers';
static const String addressBook = 'load_address_book_peers';
static const String group = 'load_group_peers';
}
/// for peer search text, global obs value
@@ -93,6 +96,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return width;
}();
final _scrollController = ScrollController();
_PeersViewState() {
_startCheckOnlines();
}
@@ -174,16 +179,16 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
return FutureBuilder<List<Peer>>(
builder: (context, snapshot) {
if (snapshot.hasData) {
final peers = snapshot.data!;
var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
final cards = <Widget>[];
for (final peer in peers) {
buildOnePeer(Peer peer) {
final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer),
);
cards.add(isDesktop
return isDesktop
? Obx(
() => SizedBox(
width: 220,
@@ -192,10 +197,34 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
child: visibilityChild,
),
)
: SizedBox(width: mobileWidth, child: visibilityChild));
: SizedBox(width: mobileWidth, child: visibilityChild);
}
final child =
Wrap(spacing: space, runSpacing: space, children: cards);
final Widget child;
if (isMobile) {
child = DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
},
);
} else {
child = 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]);
}),
);
}
if (updateEvent == UpdateEvent.load) {
_curPeers.clear();
_curPeers.addAll(peers.map((e) => e.id));
@@ -312,7 +341,7 @@ abstract class BasePeersView extends StatelessWidget {
final String loadEvent;
final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder;
final List<Peer> initPeers;
final RxList<Peer>? initPeers;
const BasePeersView({
Key? key,
@@ -326,7 +355,7 @@ abstract class BasePeersView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _PeersView(
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers),
peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers),
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder);
}
@@ -343,7 +372,7 @@ class RecentPeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: [],
initPeers: null,
);
@override
@@ -365,7 +394,7 @@ class FavoritePeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: [],
initPeers: null,
);
@override
@@ -387,7 +416,7 @@ class DiscoveredPeersView extends BasePeersView {
peer: peer,
menuPadding: menuPadding,
),
initPeers: [],
initPeers: null,
);
@override
@@ -403,7 +432,7 @@ class AddressBookPeersView extends BasePeersView {
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required List<Peer> initPeers})
required RxList<Peer> initPeers})
: super(
key: key,
name: 'address book peer',
@@ -421,12 +450,21 @@ class AddressBookPeersView extends BasePeersView {
if (selectedTags.isEmpty) {
return true;
}
for (final tag in selectedTags) {
if (idents.contains(tag)) {
return true;
if (gFFI.abModel.filterByIntersection.value) {
for (final tag in selectedTags) {
if (!idents.contains(tag)) {
return false;
}
}
return true;
} else {
for (final tag in selectedTags) {
if (idents.contains(tag)) {
return true;
}
}
return false;
}
return false;
}
}
@@ -435,11 +473,11 @@ class MyGroupPeerView extends BasePeersView {
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required List<Peer> initPeers})
required RxList<Peer> initPeers})
: super(
key: key,
name: 'my group peer',
loadEvent: 'load_my_group_peers',
name: 'group peer',
loadEvent: LoadEvent.group,
peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer,
@@ -450,12 +488,12 @@ class MyGroupPeerView extends BasePeersView {
static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) {
if (!peer.username.contains(gFFI.groupModel.searchUserText)) {
if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
return false;
}
}
if (gFFI.groupModel.selectedUser.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.username) {
if (gFFI.groupModel.selectedUser.value != peer.loginName) {
return false;
}
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/input_model.dart';
@@ -92,6 +93,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (handleTouch) {
// Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
inputModel.tapDown(MouseButtons.left);
}
@@ -111,7 +113,10 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
inputModel.tap(MouseButtons.left);
if (!handleTouch) {
// Mobile, "Mouse mode"
inputModel.tap(MouseButtons.left);
}
}
onDoubleTapDown(TapDownDetails d) {
@@ -263,9 +268,9 @@ class _RawTouchGestureDetectorRegionState
if (scale != 0) {
bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode({
'touch': {'scale': scale}
}));
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
.toJson()));
}
} else {
// mobile
@@ -283,9 +288,8 @@ class _RawTouchGestureDetectorRegionState
if (isDesktop) {
bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode({
'touch': {'scale': 0}
}));
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
} else {
// mobile
_scale = 1;

View File

@@ -0,0 +1,277 @@
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
customImageQualityWidget(
{required double initQuality,
required double initFps,
required Function(double) setQuality,
required Function(double) setFps,
required bool showFps}) {
final qualityValue = initQuality.obs;
final fpsValue = initFps.obs;
final RxBool moreQualityChecked = RxBool(qualityValue.value > 100);
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setQuality(v);
},
initialValue: qualityValue.value,
);
final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setFps(v);
},
initialValue: fpsValue.value,
);
onMoreChanged(bool? value) {
if (value == null) return;
moreQualityChecked.value = value;
if (!value && qualityValue.value > 100) {
qualityValue.value = 100;
}
debouncerQuality.value = qualityValue.value;
}
return Column(
children: [
Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: qualityValue.value,
min: 10.0,
max: moreQualityChecked.value ? 2000 : 100,
divisions: moreQualityChecked.value ? 199 : 18,
onChanged: (double value) async {
qualityValue.value = value;
debouncerQuality.value = value;
},
),
),
Expanded(
flex: 1,
child: Text(
'${qualityValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: isMobile ? 2 : 1,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
)),
// mobile doesn't have enough space
if (!isMobile)
Expanded(
flex: 1,
child: Row(
children: [
Checkbox(
value: moreQualityChecked.value,
onChanged: onMoreChanged,
),
Expanded(
child: Text(translate('More')),
)
],
))
],
)),
if (isMobile)
Obx(() => Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Checkbox(
value: moreQualityChecked.value,
onChanged: onMoreChanged,
),
),
),
Expanded(
child: Text(translate('More')),
)
],
)),
if (showFps)
Obx(() => Row(
children: [
Expanded(
flex: 3,
child: Slider(
value: fpsValue.value,
min: 5.0,
max: 120.0,
divisions: 23,
onChanged: (double value) async {
fpsValue.value = value;
debouncerFps.value = value;
},
),
),
Expanded(
flex: 1,
child: Text(
'${fpsValue.value.round()}',
style: const TextStyle(fontSize: 15),
)),
Expanded(
flex: 2,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
)),
],
);
}
customImageQualitySetting() {
final qualityKey = 'custom_image_quality';
final fpsKey = 'custom-fps';
var initQuality =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0);
if (initQuality < 10 || initQuality > 2000) {
initQuality = 50;
}
var initFps =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0);
if (initFps < 5 || initFps > 120) {
initFps = 30;
}
return customImageQualityWidget(
initQuality: initQuality,
initFps: initFps,
setQuality: (v) {
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
},
setFps: (v) {
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
},
showFps: true);
}
Future<bool> setServerConfig(
List<TextEditingController> controllers,
List<RxString> errMsgs,
ServerConfig config,
) async {
config.idServer = config.idServer.trim();
config.relayServer = config.relayServer.trim();
config.apiServer = config.apiServer.trim();
config.key = config.key.trim();
// id
if (config.idServer.isNotEmpty) {
errMsgs[0].value =
translate(await bind.mainTestIfValidServer(server: config.idServer));
if (errMsgs[0].isNotEmpty) {
return false;
}
}
// relay
if (config.relayServer.isNotEmpty) {
errMsgs[1].value =
translate(await bind.mainTestIfValidServer(server: config.relayServer));
if (errMsgs[1].isNotEmpty) {
return false;
}
}
// api
if (config.apiServer.isNotEmpty) {
if (!config.apiServer.startsWith('http://') &&
!config.apiServer.startsWith('https://')) {
errMsgs[2].value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final oldApiServer = await bind.mainGetApiServer();
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: config.idServer);
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
await bind.mainSetOption(key: 'key', value: config.key);
final newApiServer = await bind.mainGetApiServer();
if (oldApiServer.isNotEmpty &&
oldApiServer != newApiServer &&
gFFI.userModel.isLogin) {
gFFI.userModel.logOut(apiServer: oldApiServer);
}
return true;
}
List<Widget> ServerConfigImportExportWidgets(
List<TextEditingController> controllers,
List<RxString> errMsgs,
) {
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
controllers[0].text = sc.idServer;
controllers[1].text = sc.relayServer;
controllers[2].text = sc.apiServer;
controllers[3].text = sc.key;
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
success.then((value) {
if (value) {
showToast(
translate('Import server configuration successfully'));
} else {
showToast(translate('Invalid server configuration'));
}
});
} else {
showToast(translate('Invalid server configuration'));
}
} catch (e) {
showToast(translate('Invalid server configuration'));
}
} else {
showToast(translate('Clipboard is empty'));
}
});
}
export() {
final text = ServerConfig(
idServer: controllers[0].text.trim(),
relayServer: controllers[1].text.trim(),
apiServer: controllers[2].text.trim(),
key: controllers[3].text.trim())
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
return [
Tooltip(
message: translate('Import Server Config'),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton(
icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
];
}

View File

@@ -49,7 +49,8 @@ class TToggleMenu {
handleOsPasswordEditIcon(
SessionID sessionId, OverlayDialogManager dialogManager) {
isEditOsPassword = true;
showSetOSPassword(sessionId, false, dialogManager, null, () => isEditOsPassword = false);
showSetOSPassword(
sessionId, false, dialogManager, null, () => isEditOsPassword = false);
}
handleOsPasswordAction(
@@ -62,7 +63,8 @@ handleOsPasswordAction(
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
'';
if (password.isEmpty) {
showSetOSPassword(sessionId, true, dialogManager, password, () => isEditOsPassword = false);
showSetOSPassword(sessionId, true, dialogManager, password,
() => isEditOsPassword = false);
} else {
bind.sessionInputOsPassword(sessionId: sessionId, value: password);
}
@@ -76,7 +78,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
List<TTextMenu> v = [];
// elevation
if (ffi.elevationModel.showRequestMenu) {
if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) {
v.add(
TTextMenu(
child: Text(translate('Request Elevation')),

View File

@@ -5,6 +5,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1;
const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows";
@@ -12,6 +13,8 @@ const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
const double kScrollbarThickness = 12.0;
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main";
@@ -38,7 +41,7 @@ const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventCloseForSeparateWindow = "close_for_separate_window";
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionOpenInTabs = "allow-open-in-tabs";
@@ -54,6 +57,9 @@ const String kTabLabelSettingPage = "Settings";
const String kWindowPrefix = "wm_";
const int kWindowMainId = 0;
const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
// the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
@@ -68,10 +74,6 @@ const int kDesktopDefaultDisplayHeight = 720;
const int kMobileMaxDisplaySize = 1280;
const int kDesktopMaxDisplaySize = 3840;
const double kDesktopFileTransferNameColWidth = 200;
const double kDesktopFileTransferModifiedColWidth = 120;
const double kDesktopFileTransferMinimumWidth = 100;
const double kDesktopFileTransferMaximumWidth = 300;
const double kDesktopFileTransferRowHeight = 30.0;
const double kDesktopFileTransferHeaderHeight = 25.0;
@@ -134,6 +136,12 @@ const kRemoteScrollStyleAuto = 'scrollauto';
/// [kRemoteScrollStyleBar] Scroll image with scroll bar.
const kRemoteScrollStyleBar = 'scrollbar';
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
const kScrollModeDefault = 'default';
/// [kScrollModeReverse] Mouse or touchpad, the reverse scroll mode.
const kScrollModeReverse = 'reverse';
/// [kRemoteImageQualityBest] Best image quality.
const kRemoteImageQualityBest = 'best';

View File

@@ -7,7 +7,6 @@ import 'dart:io';
import 'package:auto_size_text/auto_size_text.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:url_launcher/url_launcher_string.dart';
@@ -33,9 +32,6 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Controller for the id input bar.
final _idController = IDTextEditingController();
/// Nested scroll controller
final _scrollController = ScrollController();
Timer? _updateTimer;
final RxBool _idInputFocused = false.obs;
@@ -106,7 +102,8 @@ class _ConnectionPageState extends State<ConnectionPage>
@override
void onWindowLeaveFullScreen() {
// Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value = kWindowEdgeSize;
stateGlobal.resizeEdgeSize.value =
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
}
@override
@@ -120,30 +117,18 @@ class _ConnectionPageState extends State<ConnectionPage>
return Column(
children: [
Expanded(
child: DesktopScrollWrapper(
scrollController: _scrollController,
child: CustomScrollView(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
Row(
children: [
Flexible(child: _buildRemoteIDTextField(context)),
],
).marginOnly(top: 22),
SizedBox(height: 12),
Divider().paddingOnly(right: 12),
])),
SliverFillRemaining(
hasScrollBody: false,
child: PeerTabPage().paddingOnly(right: 12.0),
)
child: Column(
children: [
Row(
children: [
Flexible(child: _buildRemoteIDTextField(context)),
],
).paddingOnly(left: 12.0),
),
),
).marginOnly(top: 22),
SizedBox(height: 12),
Divider().paddingOnly(right: 12),
Expanded(child: PeerTabPage()),
],
).paddingOnly(left: 12.0)),
const Divider(height: 1),
buildStatus()
],

View File

@@ -48,6 +48,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var watchIsInputMonitoring = false;
var watchIsCanRecordAudio = false;
Timer? _updateTimer;
bool isCardClosed = false;
@override
Widget build(BuildContext context) {
@@ -321,14 +322,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty) {
if (updateUrl.isNotEmpty && !isCardClosed) {
return buildInstallCard(
"Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
});
},
closeButton: true);
}
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});
@@ -394,11 +396,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget buildInstallCard(String title, String content, String btnText,
GestureTapCallback onPressed,
{String? help, String? link}) {
return Container(
margin: EdgeInsets.only(top: 20),
child: Container(
decoration: BoxDecoration(
{String? help, String? link, bool? closeButton}) {
void closeCard() {
setState(() {
isCardClosed = true;
});
}
return Stack(
children: [
Container(
margin: EdgeInsets.only(top: 20),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
@@ -467,19 +478,33 @@ class _DesktopHomePageState extends State<DesktopHomePage>
)).marginOnly(top: 6)),
]
: <Widget>[]))),
),
if (closeButton != null && closeButton == true)
Positioned(
top: 18,
right: 0,
child: IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
size: 20,
),
onPressed: closeCard,
),
),
],
);
}
@override
void initState() {
super.initState();
Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
});
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID();
final url = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl != url) {
updateUrl = url;
setState(() {});
}
final error = await bind.mainGetError();
if (systemError != error) {
systemError = error;

View File

@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
@@ -17,7 +18,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:window_manager/window_manager.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
@@ -88,6 +88,11 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
controller = PageController(initialPage: widget.initialPage);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() {
if (controller.page != null) {
selectedIndex.value = controller.page!.toInt();
}
});
}
@override
@@ -154,7 +159,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
scrollController: controller,
child: PageView(
controller: controller,
physics: DraggableNeverScrollableScrollPhysics(),
physics: NeverScrollableScrollPhysics(),
children: _children(),
)),
),
@@ -330,6 +335,12 @@ class _GeneralState extends State<_General> {
child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'),
));
children.add(_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
));
if (bind.mainShowOption(key: 'allow-linux-headless')) {
children.add(_OptionCheckBox(
context, 'Allow linux headless', 'allow-linux-headless'));
@@ -708,8 +719,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword)
_SubButton('Set permanent password', setPasswordDialog,
permEnabled && !locked),
if (usePassword)
hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
// if (usePassword)
// hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
if (usePassword) radios[2],
]);
})));
@@ -718,16 +729,12 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget more(BuildContext context) {
bool enabled = !locked;
return _Card(title: 'Security', children: [
Offstage(
offstage: !Platform.isWindows,
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
enabled: enabled),
),
shareRdp(context, enabled),
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled),
...directIp(context),
whitelist(),
...autoDisconnect(context),
]);
}
@@ -906,6 +913,63 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
));
}));
}
List<Widget> autoDisconnect(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
RxBool applyEnabled = false.obs;
final optionKey = 'allow-auto-disconnect';
final timeoutKey = 'auto-disconnect-timeout';
return [
_OptionCheckBox(context, 'auto_disconnect_option_tip', optionKey,
update: update, enabled: !locked),
() {
bool enabled =
option2bool(optionKey, bind.mainGetOptionSync(key: optionKey));
if (!enabled) applyEnabled.value = false;
controller.text = bind.mainGetOptionSync(key: timeoutKey);
return Offstage(
offstage: !enabled,
child: _SubLabeledWidget(
context,
'Timeout in minutes',
Row(children: [
SizedBox(
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked,
onChanged: (_) => applyEnabled.value = true,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
decoration: const InputDecoration(
hintText: '10',
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value && enabled && !locked
? () async {
applyEnabled.value = false;
await bind.mainSetOption(
key: timeoutKey, value: controller.text);
}
: null,
child: Text(
translate('Apply'),
),
))
]),
enabled: enabled && !locked,
),
);
}(),
];
}
}
class _Network extends StatefulWidget {
@@ -966,54 +1030,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key'));
set(String idServer, String relayServer, String apiServer,
String key) async {
idServer = idServer.trim();
relayServer = relayServer.trim();
apiServer = apiServer.trim();
key = key.trim();
if (idServer.isNotEmpty) {
idErrMsg.value =
translate(await bind.mainTestIfValidServer(server: idServer));
if (idErrMsg.isNotEmpty) {
return false;
}
}
if (relayServer.isNotEmpty) {
relayErrMsg.value =
translate(await bind.mainTestIfValidServer(server: relayServer));
if (relayErrMsg.isNotEmpty) {
return false;
}
}
if (apiServer.isNotEmpty) {
if (!apiServer.startsWith('http://') &&
!apiServer.startsWith('https://')) {
apiErrMsg.value =
'${translate("API Server")}: ${translate("invalid_http")}';
return false;
}
}
final oldApiServer = await bind.mainGetApiServer();
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
await bind.mainSetOption(key: 'relay-server', value: relayServer);
await bind.mainSetOption(key: 'api-server', value: apiServer);
await bind.mainSetOption(key: 'key', value: key);
final newApiServer = await bind.mainGetApiServer();
if (oldApiServer.isNotEmpty && oldApiServer != newApiServer) {
await gFFI.userModel.logOut(apiServer: oldApiServer);
}
return true;
}
final controllers = [
idController,
relayController,
apiController,
keyController,
];
final errMsgs = [
idErrMsg,
relayErrMsg,
apiErrMsg,
];
submit() async {
bool result = await set(idController.text, relayController.text,
apiController.text, keyController.text);
bool result = await setServerConfig(
controllers,
errMsgs,
ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text));
if (result) {
setState(() {});
showToast(translate('Successful'));
@@ -1022,83 +1059,28 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
}
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
idController.text = sc.idServer;
relayController.text = sc.relayServer;
apiController.text = sc.apiServer;
keyController.text = sc.key;
Future<bool> success =
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
success.then((value) {
if (value) {
showToast(
translate('Import server configuration successfully'));
} else {
showToast(translate('Invalid server configuration'));
}
});
} else {
showToast(translate('Invalid server configuration'));
}
} catch (e) {
showToast(translate('Invalid server configuration'));
}
} else {
showToast(translate('Clipboard is empty'));
}
});
}
export() {
final text = ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text)
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
bool secure = !enabled;
return _Card(title: 'ID/Relay Server', title_suffix: [
Tooltip(
message: translate('Import Server Config'),
child: IconButton(
icon: Icon(Icons.paste, color: Colors.grey),
onPressed: enabled ? import : null),
),
Tooltip(
message: translate('Export Server Config'),
child: IconButton(
icon: Icon(Icons.copy, color: Colors.grey),
onPressed: enabled ? export : null)),
], children: [
Column(
return _Card(
title: 'ID/Relay Server',
title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
children: [
Obx(() => _LabeledTextField(context, 'ID Server', idController,
idErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'Relay Server',
relayController, relayErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'API Server', apiController,
apiErrMsg.value, enabled, secure)),
_LabeledTextField(
context, 'Key', keyController, '', enabled, secure),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
).marginOnly(top: 10),
],
)
]);
Column(
children: [
Obx(() => _LabeledTextField(context, 'ID Server', idController,
idErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'Relay Server',
relayController, relayErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'API Server',
apiController, apiErrMsg.value, enabled, secure)),
_LabeledTextField(
context, 'Key', keyController, '', enabled, secure),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
).marginOnly(top: 10),
],
)
]);
}
return tmpWrapper();
@@ -1182,15 +1164,6 @@ class _DisplayState extends State<_Display> {
}
final groupValue = bind.mainGetUserDefaultOption(key: key);
final qualityKey = 'custom_image_quality';
final qualityValue =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
50.0)
.obs;
final fpsKey = 'custom-fps';
final fpsValue =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
.obs;
return _Card(title: 'Default Image Quality', children: [
_Radio(context,
value: kRemoteImageQualityBest,
@@ -1214,64 +1187,7 @@ class _DisplayState extends State<_Display> {
onChanged: onChanged),
Offstage(
offstage: groupValue != kRemoteImageQualityCustom,
child: Column(
children: [
Obx(() => Row(
children: [
Slider(
value: qualityValue.value,
min: 10.0,
max: 100.0,
divisions: 18,
onChanged: (double value) async {
qualityValue.value = value;
await bind.mainSetUserDefaultOption(
key: qualityKey, value: value.toString());
},
),
SizedBox(
width: 40,
child: Text(
'${qualityValue.value.round()}%',
style: const TextStyle(fontSize: 15),
)),
SizedBox(
width: 50,
child: Text(
translate('Bitrate'),
style: const TextStyle(fontSize: 15),
))
],
)),
Obx(() => Row(
children: [
Slider(
value: fpsValue.value,
min: 5.0,
max: 120.0,
divisions: 23,
onChanged: (double value) async {
fpsValue.value = value;
await bind.mainSetUserDefaultOption(
key: fpsKey, value: value.toString());
},
),
SizedBox(
width: 40,
child: Text(
'${fpsValue.value.round()}',
style: const TextStyle(fontSize: 15),
)),
SizedBox(
width: 50,
child: Text(
translate('FPS'),
style: const TextStyle(fontSize: 15),
))
],
)),
],
),
child: customImageQualitySetting(),
)
]);
}
@@ -1364,6 +1280,7 @@ class _DisplayState extends State<_Display> {
otherRow('Disable clipboard', 'disable_clipboard'),
otherRow('Lock after session end', 'lock_after_session_end'),
otherRow('Privacy mode', 'privacy_mode'),
otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'),
]);
}
}
@@ -1684,9 +1601,14 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
isServer
? await mainSetBoolOption(key, option)
: await mainSetLocalBoolOption(key, option);
ref.value = isServer
final readOption = isServer
? mainGetBoolOptionSync(key)
: mainGetLocalBoolOptionSync(key);
if (reverse) {
ref.value = !readOption;
} else {
ref.value = readOption;
}
update?.call();
}
}

View File

@@ -364,15 +364,20 @@ class _FileManagerViewState extends State<FileManagerView> {
final _breadCrumbScroller = ScrollController();
final _keyboardNode = FocusNode();
final _listSearchBuffer = TimeoutStringBuffer();
final _nameColWidth = kDesktopFileTransferNameColWidth.obs;
final _modifiedColWidth = kDesktopFileTransferModifiedColWidth.obs;
final _nameColWidth = 0.0.obs;
final _modifiedColWidth = 0.0.obs;
final _sizeColWidth = 0.0.obs;
final _fileListScrollController = ScrollController();
final _globalHeaderKey = GlobalKey();
/// [_lastClickTime], [_lastClickEntry] help to handle double click
var _lastClickTime =
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
Entry? _lastClickEntry;
double? _windowWidthPrev;
double _fileTransferMinimumWidth = 0.0;
FileController get controller => widget.controller;
bool get isLocal => widget.controller.isLocal;
FFI get _ffi => widget._ffi;
@@ -398,6 +403,7 @@ class _FileManagerViewState extends State<FileManagerView> {
@override
Widget build(BuildContext context) {
_handleColumnPorportions();
return Container(
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(8.0),
@@ -429,6 +435,27 @@ class _FileManagerViewState extends State<FileManagerView> {
);
}
void _handleColumnPorportions() {
final windowWidthNow = MediaQuery.of(context).size.width;
if (_windowWidthPrev == null) {
_windowWidthPrev = windowWidthNow;
final defaultColumnWidth = windowWidthNow * 0.115;
_fileTransferMinimumWidth = defaultColumnWidth / 3;
_nameColWidth.value = defaultColumnWidth;
_modifiedColWidth.value = defaultColumnWidth;
_sizeColWidth.value = defaultColumnWidth;
}
if (_windowWidthPrev != windowWidthNow) {
final difference = windowWidthNow / _windowWidthPrev!;
_windowWidthPrev = windowWidthNow;
_fileTransferMinimumWidth *= difference;
_nameColWidth.value *= difference;
_modifiedColWidth.value *= difference;
_sizeColWidth.value *= difference;
}
}
void onLocationFocusChanged() {
debugPrint("focus changed on local");
if (_locationNode.hasFocus) {
@@ -1143,9 +1170,21 @@ class _FileManagerViewState extends State<FileManagerView> {
return false;
}
void _onDrag(double dx, RxDouble column1, RxDouble column2) {
if (column1.value + dx <= _fileTransferMinimumWidth ||
column2.value - dx <= _fileTransferMinimumWidth) {
return;
}
column1.value += dx;
column2.value -= dx;
column1.value = max(_fileTransferMinimumWidth, column1.value);
column2.value = max(_fileTransferMinimumWidth, column2.value);
}
Widget _buildFileBrowserHeader(BuildContext context) {
final padding = EdgeInsets.all(1.0);
return SizedBox(
key: _globalHeaderKey,
height: kDesktopFileTransferHeaderHeight,
child: Row(
children: [
@@ -1155,11 +1194,8 @@ class _FileManagerViewState extends State<FileManagerView> {
),
DraggableDivider(
axis: Axis.vertical,
onPointerMove: (dx) {
_nameColWidth.value += dx;
_nameColWidth.value = min(kDesktopFileTransferMaximumWidth,
max(kDesktopFileTransferMinimumWidth, _nameColWidth.value));
},
onPointerMove: (dx) =>
_onDrag(dx, _nameColWidth, _modifiedColWidth),
padding: padding,
),
Obx(
@@ -1168,15 +1204,12 @@ class _FileManagerViewState extends State<FileManagerView> {
),
DraggableDivider(
axis: Axis.vertical,
onPointerMove: (dx) {
_modifiedColWidth.value += dx;
_modifiedColWidth.value = min(
kDesktopFileTransferMaximumWidth,
max(kDesktopFileTransferMinimumWidth,
_modifiedColWidth.value));
},
onPointerMove: (dx) =>
_onDrag(dx, _modifiedColWidth, _sizeColWidth),
padding: padding),
Expanded(child: headerItemFunc(null, SortBy.size, translate("Size")))
Expanded(
child: headerItemFunc(
_sizeColWidth.value, SortBy.size, translate("Size")))
],
),
);
@@ -1201,23 +1234,20 @@ class _FileManagerViewState extends State<FileManagerView> {
height: kDesktopFileTransferHeaderHeight,
child: Row(
children: [
Flexible(
flex: 2,
Expanded(
child: Text(
name,
style: headerTextStyle,
overflow: TextOverflow.ellipsis,
).marginSymmetric(horizontal: 4),
).marginOnly(left: 4),
),
Flexible(
flex: 1,
child: ascending.value != null
? Icon(
ascending.value!
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
)
: const Offstage())
ascending.value != null
? Icon(
ascending.value!
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
)
: SizedBox()
],
),
),

View File

@@ -99,7 +99,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.labelGetterAlias,
labelGetter: DesktopTab.tablabelGetter,
)),
);
return Platform.isMacOS || kUseCompatibleUiMode

View File

@@ -266,7 +266,7 @@ class _PortForwardPageState extends State<PortForwardPage>
}
void refreshTunnelConfig() async {
String peer = await bind.mainGetPeer(id: widget.id);
String peer = bind.mainGetPeerSync(id: widget.id);
Map<String, dynamic> config = jsonDecode(peer);
List<dynamic> infos = config['port_forwards'] as List;
List<_PortForward> result = List.empty(growable: true);

View File

@@ -108,7 +108,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
return true;
},
tail: AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.labelGetterAlias,
labelGetter: DesktopTab.tablabelGetter,
)),
);
return Platform.isMacOS || kUseCompatibleUiMode

View File

@@ -35,6 +35,7 @@ class RemotePage extends StatefulWidget {
Key? key,
required this.id,
required this.sessionId,
required this.tabWindowId,
required this.password,
required this.toolbarState,
required this.tabController,
@@ -44,6 +45,7 @@ class RemotePage extends StatefulWidget {
final String id;
final SessionID? sessionId;
final int? tabWindowId;
final String? password;
final ToolbarState toolbarState;
final String? switchUuid;
@@ -106,6 +108,7 @@ class _RemotePageState extends State<RemotePage>
password: widget.password,
switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -206,7 +209,7 @@ class _RemotePageState extends State<RemotePage>
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
await _renderTexture.destroy(closeSession);
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
_ffi.inputModel.enterOrLeave(false);
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose();
@@ -225,49 +228,70 @@ class _RemotePageState extends State<RemotePage>
removeSharedStates(widget.id);
}
Widget buildBody(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
body: BlockableOverlay(
Widget emptyOverlay() => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: Colors.black,
child: RawKeyFocusScope(
focusNode: _rawKeyFocusNode,
onFocusChange: (bool imageFocused) {
debugPrint(
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
// See [onWindowBlur].
if (Platform.isWindows) {
if (_isWindowBlur) {
imageFocused = false;
Future.delayed(Duration.zero, () {
_rawKeyFocusNode.unfocus();
});
color: Colors.transparent,
),
);
Widget buildBody(BuildContext context) {
remoteToolbar(BuildContext context) => RemoteToolbar(
id: widget.id,
ffi: _ffi,
state: widget.toolbarState,
onEnterOrLeaveImageSetter: (func) =>
_onEnterOrLeaveImage4Toolbar = func,
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Stack(
children: [
Container(
color: Colors.black,
child: RawKeyFocusScope(
focusNode: _rawKeyFocusNode,
onFocusChange: (bool imageFocused) {
debugPrint(
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
// See [onWindowBlur].
if (Platform.isWindows) {
if (_isWindowBlur) {
imageFocused = false;
Future.delayed(Duration.zero, () {
_rawKeyFocusNode.unfocus();
});
}
if (imageFocused) {
_ffi.inputModel.enterOrLeave(true);
} else {
_ffi.inputModel.enterOrLeave(false);
}
}
if (imageFocused) {
_ffi.inputModel.enterOrLeave(true);
} else {
_ffi.inputModel.enterOrLeave(false);
}
}
},
inputModel: _ffi.inputModel,
child: getBodyForDesktop(context))),
upperLayer: [
OverlayEntry(
builder: (context) => RemoteToolbar(
id: widget.id,
ffi: _ffi,
state: widget.toolbarState,
onEnterOrLeaveImageSetter: (func) =>
_onEnterOrLeaveImage4Toolbar = func,
onEnterOrLeaveImageCleaner: () =>
_onEnterOrLeaveImage4Toolbar = null,
))
},
inputModel: _ffi.inputModel,
child: getBodyForDesktop(context))),
Obx(() => Stack(
children: [
_ffi.ffiModel.pi.isSet.isTrue &&
_ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay()
: () {
_ffi.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
// Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue
? Overlay(initialEntries: [
OverlayEntry(builder: remoteToolbar)
])
: remoteToolbar(context),
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
],
)),
],
),
);
@@ -305,7 +329,7 @@ class _RemotePageState extends State<RemotePage>
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
bind.sessionEnterOrLeave(sessionId: sessionId, enter: true);
_ffi.inputModel.enterOrLeave(true);
}
}
@@ -325,7 +349,7 @@ class _RemotePageState extends State<RemotePage>
}
// See [onWindowBlur].
if (!Platform.isWindows) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
_ffi.inputModel.enterOrLeave(false);
}
}
@@ -385,7 +409,7 @@ class _RemotePageState extends State<RemotePage>
keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
textureId: _renderTexture.textureId,
useTextureRender: _renderTexture.useTextureRender,
useTextureRender: RenderTexture.useTextureRender,
listenerBuilder: (child) =>
_buildRawTouchAndPointerRegion(child, enterView, leaveView),
);
@@ -461,21 +485,20 @@ class _ImagePaintState extends State<ImagePaint> {
var c = Provider.of<CanvasModel>(context);
final s = c.scale;
bool isViewAdaptive() => c.viewStyle.style == kRemoteViewStyleAdaptive;
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
mouseRegion({child}) => Obx(() {
double getCursorScale() {
var c = Provider.of<CanvasModel>(context);
var cursorScale = 1.0;
if (Platform.isWindows) {
// debug win10
final isViewAdaptive =
c.viewStyle.style == kRemoteViewStyleAdaptive;
if (zoomCursor.value && isViewAdaptive) {
if (zoomCursor.value && isViewAdaptive()) {
cursorScale = s * c.devicePixelRatio;
}
} else {
final isViewOriginal =
c.viewStyle.style == kRemoteViewStyleOriginal;
if (zoomCursor.value || isViewOriginal) {
if (zoomCursor.value || isViewOriginal()) {
cursorScale = s;
}
}
@@ -515,7 +538,11 @@ class _ImagePaintState extends State<ImagePaint> {
imageWidget = SizedBox(
width: imageWidth,
height: imageHeight,
child: Obx(() => Texture(textureId: widget.textureId.value)),
child: Obx(() => Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
)),
);
} else {
imageWidget = CustomPaint(
@@ -549,14 +576,20 @@ class _ImagePaintState extends State<ImagePaint> {
late final Widget imageWidget;
if (c.size.width > 0 && c.size.height > 0) {
if (widget.useTextureRender) {
final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x;
final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y;
imageWidget = Stack(
children: [
Positioned(
left: c.x.toInt().toDouble(),
top: c.y.toInt().toDouble(),
left: x,
top: y,
width: c.getDisplayWidth() * s,
height: c.getDisplayHeight() * s,
child: Texture(textureId: widget.textureId.value),
child: Texture(
textureId: widget.textureId.value,
filterQuality:
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
),
)
],
);
@@ -581,7 +614,7 @@ class _ImagePaintState extends State<ImagePaint> {
} else {
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
debugPrint("Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always
@@ -670,6 +703,7 @@ class _ImagePaintState extends State<ImagePaint> {
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
customMouseWheelScrollConfig: scrollConfig,
child: RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: _horizontal,
thumbVisibility: false,
@@ -687,6 +721,7 @@ class _ImagePaintState extends State<ImagePaint> {
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
customMouseWheelScrollConfig: scrollConfig,
child: RawScrollbar(
thickness: kScrollbarThickness,
thumbColor: Colors.grey,
controller: _vertical,
thumbVisibility: false,

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
@@ -55,6 +56,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
RemoteCountState.init();
peerId = params['id'];
final sessionId = params['session_id'];
final tabWindowId = params['tab_window_id'];
if (peerId != null) {
ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
@@ -77,6 +79,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(peerId),
id: peerId!,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
password: params['password'],
toolbarState: _toolbarState,
tabController: tabController,
@@ -98,13 +101,20 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
windowOnTop(windowId());
if (tabController.length == 0) {
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
stateGlobal.setFullscreen(true);
}
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
@@ -118,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
@@ -147,12 +158,24 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventCloseForSeparateWindow) {
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final peerId = call.arguments;
closeSessionOnDispose[peerId] = false;
tabController.closeBy(peerId);
try {
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == peerId)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (returnValue != null) {
closeSessionOnDispose[peerId] = false;
tabController.closeBy(peerId);
}
}
_update_remote_count();
return returnValue;
});
Future.delayed(Duration.zero, () {
restoreWindowPosition(
@@ -187,7 +210,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.labelGetterAlias,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
@@ -249,7 +272,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
if (e.buttons == 2) {
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as RemotePage;
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
@@ -337,7 +364,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
));
}
if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {}
if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {
menu.add(RemoteMenuEntry.insertLock(sessionId, padding,
dismissFunc: cancelFunc));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(RemoteMenuEntry.insertCtrlAltDel(sessionId, padding,
dismissFunc: cancelFunc));
}
}
menu.addAll([
MenuEntryDivider<String>(),
@@ -380,7 +415,24 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) {
await WindowController.fromWindowId(windowId()).close();
stateGlobal.setFullscreen(false, procWnd: false);
// Keep calling until the window status is hidden.
//
// Workaround for Windows:
// If you click other buttons and close in msgbox within a very short period of time, the close may fail.
// `await WindowController.fromWindowId(windowId()).close();`.
Future<void> loopCloseWindow() async {
int c = 0;
final windowController = WindowController.fromWindowId(windowId());
while (c < 20 &&
tabController.state.value.tabs.isEmpty &&
(!await windowController.isHidden())) {
await windowController.close();
await Future.delayed(Duration(milliseconds: 100));
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);
_update_remote_count();

View File

@@ -2,6 +2,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
@@ -9,12 +10,14 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../common.dart';
import '../../common/widgets/chat_page.dart';
import '../../models/file_model.dart';
import '../../models/platform_model.dart';
import '../../models/server_model.dart';
@@ -32,6 +35,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
void initState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this);
Get.put(tabController);
tabController.onRemoved = (_, id) {
onRemoveId(id);
};
@@ -111,6 +115,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
});
}
windowManager.setTitle(getWindowNameWithId(client.peerId));
gFFI.cmFileModel.updateCurrentClientId(client.id);
}
}
};
@@ -170,40 +175,65 @@ class ConnectionManagerState extends State<ConnectionManager> {
],
);
},
pageViewBuilder: (pageView) => Row(
children: [
Consumer<ChatModel>(
builder: (_, model, child) => model.isShowCMChatPage
? Expanded(
child: buildRemoteBlock(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child:
ChatPage(type: ChatPageType.desktopCM)),
),
flex: (kConnectionManagerWindowSizeOpenChat.width -
kConnectionManagerWindowSizeClosedChat
.width)
.toInt(),
)
: Offstage(),
),
Expanded(
child: pageView,
flex: kConnectionManagerWindowSizeClosedChat.width
.toInt() -
4 // prevent stretch of the page view when chat is open,
),
],
pageViewBuilder: (pageView) => LayoutBuilder(
builder: (context, constrains) {
var borderWidth = 0.0;
if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width) {
borderWidth = kConnectionManagerWindowSizeOpenChat.width -
constrains.maxWidth;
} else {
borderWidth = kConnectionManagerWindowSizeClosedChat.width -
constrains.maxWidth;
}
if (borderWidth < 0 || borderWidth > 50) {
borderWidth = 0;
}
final realClosedWidth =
kConnectionManagerWindowSizeClosedChat.width -
borderWidth;
final realChatPageWidth =
constrains.maxWidth - realClosedWidth;
return Row(children: [
if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width)
Consumer<ChatModel>(
builder: (_, model, child) => SizedBox(
width: realChatPageWidth,
child: buildRemoteBlock(
child: Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
child: buildSidePage()),
),
)),
SizedBox(
width: realClosedWidth,
child:
SizedBox(width: realClosedWidth, child: pageView)),
]);
},
),
),
);
}
Widget buildSidePage() {
final selected = gFFI.serverModel.tabController.state.value.selected;
if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
return Offstage();
}
final clientType = gFFI.serverModel.clients[selected].type_();
if (clientType == ClientType.file) {
return _FileTransferLogPage();
} else {
return ChatPage(type: ChatPageType.desktopCM);
}
}
Widget buildTitleBar() {
return SizedBox(
height: kDesktopRemoteTabBarHeight,
@@ -447,14 +477,21 @@ class _CmHeaderState extends State<_CmHeader>
),
),
Offstage(
offstage: !client.authorized || client.type_() != ClientType.remote,
offstage: !client.authorized ||
(client.type_() != ClientType.remote &&
client.type_() != ClientType.file),
child: IconButton(
onPressed: () => checkClickTime(
client.id,
() => gFFI.chatModel
.toggleCMChatPage(MessageKey(client.peerId, client.id)),
),
icon: SvgPicture.asset('assets/chat2.svg'),
onPressed: () => checkClickTime(client.id, () {
if (client.type_() != ClientType.file) {
gFFI.chatModel.toggleCMSidePage();
} else {
gFFI.chatModel
.toggleCMChatPage(MessageKey(client.peerId, client.id));
}
}),
icon: SvgPicture.asset(client.type_() == ClientType.file
? 'assets/file_transfer.svg'
: 'assets/chat2.svg'),
splashRadius: kDesktopIconButtonSplashRadius,
),
)
@@ -912,3 +949,181 @@ void checkClickTime(int id, Function() callback) async {
if (d > 120) callback();
});
}
class _FileTransferLogPage extends StatefulWidget {
_FileTransferLogPage({Key? key}) : super(key: key);
@override
State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
}
class __FileTransferLogPageState extends State<_FileTransferLogPage> {
@override
Widget build(BuildContext context) {
return statusList();
}
Widget generateCard(Widget child) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.all(
Radius.circular(15.0),
),
),
child: child,
);
}
Widget statusList() {
return PreferredSize(
preferredSize: const Size(200, double.infinity),
child: Container(
padding: const EdgeInsets.all(12.0),
child: Obx(
() {
final jobTable = gFFI.cmFileModel.currentJobTable;
statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(),
itemBuilder: (BuildContext context, int index) {
final item = jobs[index];
return Padding(
padding: const EdgeInsets.only(bottom: 5),
child: generateCard(
Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 50,
child: Column(
children: [
Transform.rotate(
angle: item.isRemoteToLocal ? 0 : pi,
child: SvgPicture.asset(
"assets/arrow.svg",
color: Theme.of(context)
.tabBarTheme
.labelColor,
),
),
Text(item.isRemoteToLocal
? translate('Send')
: translate('Receive'))
],
),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item.fileName,
).paddingSymmetric(vertical: 10),
if (item.totalSize > 0)
Text(
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
if (item.totalSize > 0)
Offstage(
offstage: item.state !=
JobState.inProgress,
child: Text(
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage:
item.state == JobState.inProgress,
child: Text(
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
if (item.totalSize > 0)
Offstage(
offstage: item.state !=
JobState.inProgress,
child: LinearPercentIndicator(
padding:
EdgeInsets.only(right: 15),
animateFromLastPercent: true,
center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
),
barRadius: Radius.circular(15),
percent: item.finishedSize /
item.totalSize,
progressColor: MyTheme.accent,
backgroundColor:
Theme.of(context).hoverColor,
lineHeight:
kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [],
),
],
),
],
).paddingSymmetric(vertical: 10),
),
);
},
itemCount: jobTable.length,
);
return jobTable.isEmpty
? generateCard(
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/transfer.svg",
color: Theme.of(context).tabBarTheme.labelColor,
height: 40,
).paddingOnly(bottom: 10),
Text(
translate("No transfers in progress"),
textAlign: TextAlign.center,
textScaleFactor: 1.20,
style: TextStyle(
color:
Theme.of(context).tabBarTheme.labelColor),
),
],
),
),
)
: statusListView(jobTable);
},
)),
);
}
}

View File

@@ -101,6 +101,9 @@ class ToolbarState {
class _ToolbarTheme {
static const Color blueColor = MyTheme.button;
static const Color hoverBlueColor = MyTheme.accent;
static Color inactiveColor = Colors.grey[800]!;
static Color hoverInactiveColor = Colors.grey[850]!;
static const Color redColor = Colors.redAccent;
static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
@@ -543,9 +546,11 @@ class _PinMenu extends StatelessWidget {
assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar',
onPressed: state.switchPin,
color: state.pin ? _ToolbarTheme.blueColor : Colors.grey[800]!,
hoverColor:
state.pin ? _ToolbarTheme.hoverBlueColor : Colors.grey[850]!,
color:
state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
hoverColor: state.pin
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
),
);
}
@@ -558,13 +563,18 @@ class _MobileActionMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!ffi.ffiModel.isPeerAndroid) return Offstage();
return _IconMenuButton(
assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions',
onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
);
return Obx(() => _IconMenuButton(
assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions',
onPressed: () =>
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor,
hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.hoverBlueColor
: _ToolbarTheme.hoverInactiveColor,
));
}
}
@@ -765,13 +775,14 @@ class ScreenAdjustor {
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
stateGlobal.setMaximized(false);
}
}
updateScreen() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
final String valueStr = v;
final String valueStr = v.result;
if (valueStr.isEmpty) {
_screen = null;
} else {
@@ -1042,10 +1053,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
FfiModel get ffiModel => widget.ffi.ffiModel;
Display get display => ffiModel.display;
List<Resolution> get resolutions => pi.resolutions;
bool get isWayland => bind.mainCurrentIsWayland();
@override
void initState() {
super.initState();
_getLocalResolutionWayland();
}
@override
@@ -1054,7 +1067,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
final visible =
ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1);
if (!visible) return Offstage();
_getLocalResolution();
final showOriginalBtn =
display.isOriginalResolutionSet && !display.isOriginalResolution;
final showFitLocalBtn = !_isRemoteResolutionFitLocal();
@@ -1090,6 +1102,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
);
}
Future<void> _getLocalResolutionWayland() async {
if (!isWayland) return _getLocalResolution();
final window = await window_size.getWindowInfo();
final screen = window.screen;
if (screen != null) {
setState(() {
_localResolution = Resolution(
screen.frame.width.toInt(),
screen.frame.height.toInt(),
);
});
}
}
_getLocalResolution() {
_localResolution = null;
final String currentDisplay = bind.mainGetCurrentDisplay();
@@ -1299,23 +1325,25 @@ class _KeyboardMenu extends StatelessWidget {
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildren: [
mode(modeOnly),
keyboardMode(modeOnly),
localKeyboardType(),
Divider(),
view_mode(),
viewMode(),
Divider(),
reverseMouseWheel(),
]);
}
mode(String? modeOnly) {
keyboardMode(String? modeOnly) {
return futureBuilder(future: () async {
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
_kKeyLegacyMode;
}(), hasData: (data) {
final groupValue = data as String;
List<KeyboardModeMenu> modes = [
KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
List<InputModeMenu> modes = [
InputModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'),
InputModeMenu(key: _kKeyMapMode, menu: 'Map mode'),
InputModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'),
];
List<RdoMenuButton> list = [];
final enabled = !ffi.ffiModel.viewOnly;
@@ -1325,7 +1353,7 @@ class _KeyboardMenu extends StatelessWidget {
sessionId: ffi.sessionId, value: value);
}
for (KeyboardModeMenu mode in modes) {
for (InputModeMenu mode in modes) {
if (modeOnly != null && mode.key != modeOnly) {
continue;
} else if (!bind.sessionIsKeyboardModeSupported(
@@ -1374,7 +1402,7 @@ class _KeyboardMenu extends StatelessWidget {
);
}
view_mode() {
viewMode() {
final ffiModel = ffi.ffiModel;
final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard;
return CkbMenuButton(
@@ -1390,6 +1418,30 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi,
child: Text(translate('View Mode')));
}
reverseMouseWheel() {
return futureBuilder(future: () async {
final v =
await bind.sessionGetReverseMouseWheel(sessionId: ffi.sessionId);
if (v != null && v != '') {
return v;
}
return bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
}(), hasData: (data) {
final enabled = !ffi.ffiModel.viewOnly;
onChanged(bool? value) async {
if (value == null) return;
await bind.sessionSetReverseMouseWheel(
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
}
return CkbMenuButton(
value: data == 'Y',
onChanged: enabled ? onChanged : null,
child: Text(translate('Reverse mouse wheel')),
ffi: ffi);
});
}
}
class _ChatMenu extends StatefulWidget {
@@ -1587,26 +1639,26 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
width: _ToolbarTheme.buttonSize,
height: _ToolbarTheme.buttonSize,
child: MenuItemButton(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.transparent),
padding: MaterialStatePropertyAll(EdgeInsets.zero),
overlayColor: MaterialStatePropertyAll(Colors.transparent)),
onHover: (value) => setState(() {
hover = value;
}),
onPressed: widget.onPressed,
child: Tooltip(
message: translate(widget.tooltip),
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_ToolbarTheme.iconRadius),
color: hover ? widget.hoverColor : widget.color,
),
child: icon)),
)
),
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.transparent),
padding: MaterialStatePropertyAll(EdgeInsets.zero),
overlayColor: MaterialStatePropertyAll(Colors.transparent)),
onHover: (value) => setState(() {
hover = value;
}),
onPressed: widget.onPressed,
child: Tooltip(
message: translate(widget.tooltip),
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(_ToolbarTheme.iconRadius),
color: hover ? widget.hoverColor : widget.color,
),
child: icon)),
)),
).marginSymmetric(
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
@@ -1670,18 +1722,17 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
onHover: (value) => setState(() {
hover = value;
}),
child: Tooltip(
message: translate(widget.tooltip),
child: Material(
child: Tooltip(
message: translate(widget.tooltip),
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(_ToolbarTheme.iconRadius),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(_ToolbarTheme.iconRadius),
color: hover ? widget.hoverColor : widget.color,
),
child: icon))
),
),
child: icon))),
menuChildren: widget.menuChildren
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
.toList()));
@@ -1968,11 +2019,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
}
}
class KeyboardModeMenu {
class InputModeMenu {
final String key;
final String menu;
KeyboardModeMenu({required this.key, required this.menu});
InputModeMenu({required this.key, required this.menu});
}
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();

View File

@@ -8,7 +8,6 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
@@ -77,7 +76,7 @@ CancelFunc showRightMenu(ToastBuilder builder,
targetContext: context,
verticalOffset: 0,
horizontalOffset: 0,
duration: Duration(seconds: 4),
duration: Duration(seconds: 300),
animationDuration: Duration(milliseconds: 0),
animationReverseDuration: Duration(milliseconds: 0),
preferDirection: PreferDirection.rightTop,
@@ -267,13 +266,9 @@ class DesktopTab extends StatelessWidget {
tabType == DesktopTabType.install;
}
static RxString labelGetterAlias(String peerId) {
final opt = 'alias';
PeerStringOption.init(peerId, opt, () {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: opt);
return alias.isEmpty ? peerId : alias;
});
return PeerStringOption.find(peerId, opt);
static RxString tablabelGetter(String peerId) {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
return RxString(getDesktopTabLabel(peerId, alias));
}
@override
@@ -440,7 +435,6 @@ class DesktopTab extends StatelessWidget {
tabType: tabType,
state: state,
tail: tail,
isMaximized: stateGlobal.isMaximized,
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
@@ -455,7 +449,6 @@ class WindowActionPanel extends StatefulWidget {
final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final RxBool isMaximized;
final bool showMinimize;
final bool showMaximize;
@@ -468,7 +461,6 @@ class WindowActionPanel extends StatefulWidget {
required this.isMainWindow,
required this.tabType,
required this.state,
required this.isMaximized,
this.tail,
this.showMinimize = true,
this.showMaximize = true,
@@ -485,6 +477,8 @@ class WindowActionPanel extends StatefulWidget {
class WindowActionPanelState extends State<WindowActionPanel>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
@override
void initState() {
@@ -495,18 +489,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (widget.isMaximized.value != maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized));
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
} else {
final wc = WindowController.fromWindowId(kWindowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (widget.isMaximized.value != maximized) {
if (stateGlobal.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized));
(_) => setState(() => stateGlobal.setMaximized(maximized)));
}
});
}
@@ -517,6 +511,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
_macOSCheckRestoreTimer?.cancel();
super.dispose();
}
@@ -535,10 +530,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override
void onWindowMaximize() {
// catch maximize from system
if (!widget.isMaximized.value) {
widget.isMaximized.value = true;
}
stateGlobal.setMinimized(false);
_setMaximized(true);
super.onWindowMaximize();
@@ -546,10 +537,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override
void onWindowUnmaximize() {
// catch unmaximize from system
if (widget.isMaximized.value) {
widget.isMaximized.value = false;
}
stateGlobal.setMinimized(false);
_setMaximized(false);
super.onWindowUnmaximize();
@@ -577,6 +564,33 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override
void onWindowClose() async {
mainWindowClose() async => await windowManager.hide();
notMainWindowClose(WindowController controller) async {
await controller.hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
}
macOSWindowClose(
Future<void> Function() restoreFunc,
Future<bool> Function() checkFullscreen,
Future<void> Function() closeFunc) async {
await restoreFunc();
_macOSCheckRestoreCounter = 0;
_macOSCheckRestoreTimer =
Timer.periodic(Duration(milliseconds: 30), (timer) async {
_macOSCheckRestoreCounter++;
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
_macOSCheckRestoreTimer?.cancel();
_macOSCheckRestoreTimer = null;
Timer(Duration(milliseconds: 700), () async => await closeFunc());
}
});
}
// hide window on close
if (widget.isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
@@ -584,23 +598,28 @@ class WindowActionPanelState extends State<WindowActionPanel>
}
// macOS specific workaround, the window is not hiding when in fullscreen.
if (Platform.isMacOS && await windowManager.isFullScreen()) {
await windowManager.setFullScreen(false);
await Future.delayed(Duration(seconds: 1));
stateGlobal.closeOnFullscreen = true;
await macOSWindowClose(
() async => await windowManager.setFullScreen(false),
() async => await windowManager.isFullScreen(),
mainWindowClose);
} else {
stateGlobal.closeOnFullscreen = false;
await mainWindowClose();
}
await windowManager.hide();
} else {
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (Platform.isMacOS && await controller.isFullScreen()) {
await controller.setFullscreen(false);
await Future.delayed(Duration(seconds: 1));
stateGlobal.closeOnFullscreen = true;
await macOSWindowClose(
() async => await controller.setFullscreen(false),
() async => await controller.isFullScreen(),
() async => await notMainWindowClose(controller));
} else {
stateGlobal.closeOnFullscreen = false;
await notMainWindowClose(controller);
}
await controller.hide();
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
}
super.onWindowClose();
}
@@ -632,9 +651,10 @@ class WindowActionPanelState extends State<WindowActionPanel>
Offstage(
offstage: !widget.showMaximize || Platform.isMacOS,
child: Obx(() => ActionIcon(
message:
widget.isMaximized.value ? 'Restore' : 'Maximize',
icon: widget.isMaximized.value
message: stateGlobal.isMaximized.isTrue
? 'Restore'
: 'Maximize',
icon: stateGlobal.isMaximized.isTrue
? IconFont.restore
: IconFont.max,
onTap: _toggleMaximize,
@@ -671,10 +691,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
void _toggleMaximize() {
toggleMaximize(widget.isMainWindow).then((maximize) {
if (widget.isMaximized.value != maximize) {
// update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
widget.isMaximized.value = maximize;
}
// update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
stateGlobal.setMaximized(maximize);
});
}
}
@@ -898,14 +916,17 @@ class _TabState extends State<_Tab> with RestorationMixin {
final labelWidget = Obx(() {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Text(
translate(widget.label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor),
overflow: TextOverflow.ellipsis,
child: Tooltip(
message: translate(widget.label.value),
child: Text(
translate(widget.label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor),
overflow: TextOverflow.ellipsis,
),
));
});

View File

@@ -125,7 +125,7 @@ void runMainApp(bool startService) async {
bind.pluginSyncUi(syncTo: kAppTypeMain);
bind.pluginListReload();
}
gFFI.abModel.loadCache();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
// Set window option.
@@ -153,7 +153,7 @@ void runMobileApp() async {
await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit();
platformFFI.syncAndroidServiceAppDirConfigPath();
gFFI.abModel.loadCache();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser();
runApp(App());
}
@@ -223,6 +223,7 @@ void runConnectionManagerScreen(bool hide) async {
const DesktopServerPage(),
MyTheme.currentThemeMode(),
);
gFFI.serverModel.hideCm = hide;
if (hide) {
await hideCmWindow(isStartup: true);
} else {
@@ -233,19 +234,24 @@ void runConnectionManagerScreen(bool hide) async {
listenUniLinks(handleByFlutter: false);
}
bool _isCmReadyToShow = false;
showCmWindow({bool isStartup = false}) async {
if (isStartup) {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.waitUntilReadyToShow(windowOptions, () async {
bind.mainHideDocker();
await windowManager.show();
await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]);
// ensure initial window size to be changed
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
});
} else {
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
await Future.wait([
windowManager.show(),
windowManager.focus(),
windowManager.setOpacity(1)
]);
// ensure initial window size to be changed
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 1) {
await windowManager.setOpacity(1);
await windowManager.focus();
@@ -262,16 +268,18 @@ hideCmWindow({bool isStartup = false}) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
windowManager.setOpacity(0);
windowManager.waitUntilReadyToShow(windowOptions, () async {
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
});
} else {
await windowManager.setOpacity(0);
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
_isCmReadyToShow = true;
} else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 0) {
await windowManager.setOpacity(0);
bind.mainHideDocker();
await windowManager.minimize();
await windowManager.hide();
}
}
}
@@ -395,7 +403,7 @@ class _AppState extends State<App> {
themeMode: MyTheme.currentThemeMode(),
home: isDesktop
? const DesktopTabPage()
: !isAndroid
: isWeb
? WebHomePage()
: HomePage(),
localizationsDelegates: const [

View File

@@ -28,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection");
@override
final appBarActions = !isAndroid ? <Widget>[const WebMenu()] : <Widget>[];
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
@override
State<ConnectionPage> createState() => _ConnectionPageState();
@@ -57,7 +57,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}();
}
if (isAndroid) {
Timer(const Duration(seconds: 5), () async {
Timer(const Duration(seconds: 1), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (_updateUrl.isNotEmpty) setState(() {});
});
@@ -80,7 +80,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
_buildRemoteIDTextField(),
])),
SliverFillRemaining(
hasScrollBody: false,
hasScrollBody: true,
child: PeerTabPage(),
)
],
@@ -211,25 +211,6 @@ class WebMenu extends StatefulWidget {
}
class _WebMenuState extends State<WebMenu> {
String url = "";
@override
void initState() {
super.initState();
() async {
final urlRes = await bind.mainGetApiServer();
var update = false;
if (urlRes != url) {
url = urlRes;
update = true;
}
if (update) {
setState(() {});
}
}();
}
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
@@ -251,16 +232,14 @@ class _WebMenuState extends State<WebMenu> {
child: Text(translate('ID/Relay Server')),
)
] +
(url.contains('admin.rustdesk.com')
? <PopupMenuItem<String>>[]
: [
PopupMenuItem(
value: "login",
child: Text(gFFI.userModel.userName.value.isEmpty
? translate("Login")
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
)
]) +
[
PopupMenuItem(
value: "login",
child: Text(gFFI.userModel.userName.value.isEmpty
? translate("Login")
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
)
] +
[
PopupMenuItem(
value: "about",

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/overlay.dart';
import 'package:flutter_hbb/mobile/pages/server_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:get/get.dart';
@@ -26,7 +25,9 @@ class _HomePageState extends State<HomePage> {
var _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
final List<PageShape> _pages = [];
final _blockableOverlayState = BlockableOverlayState();
bool get isChatPageCurrentTab => isAndroid
? _selectedIndex == 1
: false; // change this when ios have chat page
void refreshPages() {
setState(() {
@@ -38,7 +39,6 @@ class _HomePageState extends State<HomePage> {
void initState() {
super.initState();
initPages();
_blockableOverlayState.applyFfi(gFFI);
}
void initPages() {
@@ -82,13 +82,15 @@ class _HomePageState extends State<HomePage> {
unselectedItemColor: MyTheme.darkGray,
onTap: (index) => setState(() {
// close chat overlay when go chat page
if (index == 1 && _selectedIndex != index) {
gFFI.chatModel.hideChatIconOverlay();
gFFI.chatModel.hideChatWindowOverlay();
gFFI.chatModel
.mobileClearClientUnread(gFFI.chatModel.currentKey.connId);
if (_selectedIndex != index) {
_selectedIndex = index;
if (isChatPageCurrentTab) {
gFFI.chatModel.hideChatIconOverlay();
gFFI.chatModel.hideChatWindowOverlay();
gFFI.chatModel.mobileClearClientUnread(
gFFI.chatModel.currentKey.connId);
}
}
_selectedIndex = index;
}),
),
body: _pages.elementAt(_selectedIndex),
@@ -98,7 +100,7 @@ class _HomePageState extends State<HomePage> {
Widget appTitle() {
final currentUser = gFFI.chatModel.currentUser;
final currentKey = gFFI.chatModel.currentKey;
if (_selectedIndex == 1 &&
if (isChatPageCurrentTab &&
currentUser != null &&
currentKey.peerId.isNotEmpty) {
final connected =

View File

@@ -39,6 +39,8 @@ class _RemotePageState extends State<RemotePage> {
String _value = '';
Orientation? _currentOrientation;
final _blockableOverlayState = BlockableOverlayState();
final keyboardVisibilityController = KeyboardVisibilityController();
late final StreamSubscription<bool> keyboardSubscription;
final FocusNode _mobileFocusNode = FocusNode();
@@ -67,6 +69,8 @@ class _RemotePageState extends State<RemotePage> {
initSharedStates(widget.id);
gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
_blockableOverlayState.applyFfi(gFFI);
}
@override
@@ -88,6 +92,19 @@ class _RemotePageState extends State<RemotePage> {
removeSharedStates(widget.id);
}
// to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
// But I don't know why and how to fix it.
Widget emptyOverlay(Color bgColor) => BlockableOverlay(
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
/// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
color: bgColor,
),
);
void onSoftKeyboardChanged(bool visible) {
if (!visible) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -198,13 +215,19 @@ class _RemotePageState extends State<RemotePage> {
});
}
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
Widget _bottomWidget() => _showGestureHelp
? getGestureHelp()
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
? getBottomAppBar(keyboard)
: Offstage());
@override
Widget build(BuildContext context) {
final pi = Provider.of<FfiModel>(context).pi;
final keyboardIsVisible =
keyboardVisibilityController.isVisible && _showEdit;
final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
return WillPopScope(
onWillPop: () async {
@@ -241,11 +264,22 @@ class _RemotePageState extends State<RemotePage> {
}
});
}),
bottomNavigationBar: _showGestureHelp
? getGestureHelp()
: (_showBar && pi.displays.isNotEmpty
? getBottomAppBar(keyboard)
: null),
bottomNavigationBar: Obx(() => Stack(
alignment: Alignment.bottomCenter,
children: [
gFFI.ffiModel.pi.isSet.isTrue &&
gFFI.ffiModel.waitForFirstImage.isTrue
? emptyOverlay(MyTheme.canvasColor)
: () {
gFFI.ffiModel.tryShowAndroidActionsOverlay();
return Offstage();
}(),
_bottomWidget(),
gFFI.ffiModel.pi.isSet.isFalse
? emptyOverlay(MyTheme.canvasColor)
: Offstage(),
],
)),
body: Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
@@ -284,12 +318,17 @@ class _RemotePageState extends State<RemotePage> {
Widget getRawPointerAndKeyBody(Widget child) {
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
return RawPointerMouseRegion(
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
inputModel: inputModel,
child: RawKeyFocusScope(
focusNode: _physicalFocusNode,
inputModel: inputModel,
child: child));
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
inputModel: inputModel,
// Disable RawKeyFocusScope before the connecting is established.
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
child: gFFI.ffiModel.pi.isSet.isTrue
? RawKeyFocusScope(
focusNode: _physicalFocusNode,
inputModel: inputModel,
child: child)
: child,
);
}
Widget getBottomAppBar(bool keyboard) {
@@ -368,17 +407,23 @@ class _RemotePageState extends State<RemotePage> {
},
),
]),
IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: () {
setState(() => _showBar = !_showBar);
}),
Obx(() => IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
? null
: () {
setState(() => _showBar = !_showBar);
},
)),
],
),
);
}
bool get showCursorPaint =>
!gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded;
Widget getBodyForMobile() {
final keyboardIsVisible = keyboardVisibilityController.isVisible;
return Container(
@@ -411,7 +456,7 @@ class _RemotePageState extends State<RemotePage> {
),
),
];
if (!gFFI.canvasModel.cursorEmbedded) {
if (showCursorPaint) {
paints.add(CursorPaint());
}
return paints;
@@ -420,7 +465,7 @@ class _RemotePageState extends State<RemotePage> {
Widget getBodyForDesktopWithListener(bool keyboard) {
var paints = <Widget>[ImagePaint()];
if (!gFFI.canvasModel.cursorEmbedded) {
if (showCursorPaint) {
final cursor = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor');
if (keyboard || cursor) {
@@ -466,7 +511,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
bind.sessionPeerOption(
sessionId: sessionId, name: "touch", value: v);
sessionId: sessionId, name: "touch-mode", value: v);
})));
}
@@ -695,8 +740,8 @@ class CursorPaint extends StatelessWidget {
return CustomPaint(
painter: ImagePainter(
image: m.image ?? preDefaultCursor.image,
x: m.x * s - hotx * s + c.x,
y: m.y * s - hoty * s + c.y - adjust,
x: m.x * s - hotx + c.x,
y: m.y * s - hoty + c.y - adjust,
scale: 1),
);
}

View File

@@ -210,11 +210,216 @@ class ServiceNotRunningNotification extends StatelessWidget {
.marginOnly(bottom: 8),
ElevatedButton.icon(
icon: const Icon(Icons.play_arrow),
onPressed: serverModel.toggleService,
onPressed: () {
if (gFFI.userModel.userName.value.isEmpty && bind.mainGetLocalOption(key: "show-scam-warning") != "N") {
_showScamWarning(context, serverModel);
} else {
serverModel.toggleService();
}
},
label: Text(translate("Start Service")))
],
));
}
void _showScamWarning(BuildContext context, ServerModel serverModel) {
showDialog(
context: context,
builder: (BuildContext context) {
return ScamWarningDialog(serverModel: serverModel);
},
);
}
}
class ScamWarningDialog extends StatefulWidget {
final ServerModel serverModel;
ScamWarningDialog({required this.serverModel});
@override
_ScamWarningDialogState createState() => _ScamWarningDialogState();
}
class _ScamWarningDialogState extends State<ScamWarningDialog> {
int _countdown = 12;
bool show_warning = false;
late Timer _timer;
late ServerModel _serverModel;
@override
void initState() {
super.initState();
_serverModel = widget.serverModel;
startCountdown();
}
void startCountdown() {
const oneSecond = Duration(seconds: 1);
_timer = Timer.periodic(oneSecond, (timer) {
setState(() {
_countdown--;
if (_countdown <= 0) {
timer.cancel();
}
});
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isButtonLocked = _countdown > 0;
return AlertDialog(
content: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Color(0xffe242bc),
Color(0xfff4727c),
],
),
borderRadius: BorderRadius.circular(20.0),
),
padding: EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_amber_sharp,
color: Colors.white,
),
SizedBox(width: 10),
Text(
translate("Warning"),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20.0,
),
),
],
),
SizedBox(height: 20),
Center(
child: Image.asset('assets/scam.png',
width: 180,
),
),
SizedBox(height: 18),
Text(
translate("scam_title"),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 22.0,
),
),
SizedBox(height: 18),
SizedBox(
height: 220,
child: Scrollbar(
child: SingleChildScrollView(
child: Text(
translate("scam_text1")+"\n\n"
+translate("scam_text2")+"\n",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
),
),
),
Row(
children: <Widget>[
Checkbox(
value: show_warning,
onChanged: (value) {
setState((){
show_warning = value!;
});
},
),
Text(
translate("Don't show again"),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15.0,
),
),
],
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
constraints: BoxConstraints(maxWidth: 150),
child: ElevatedButton(
onPressed: isButtonLocked
? null
: () {
Navigator.of(context).pop();
_serverModel.toggleService();
if (show_warning) {
bind.mainSetLocalOption(key: "show-scam-warning", value: "N");
}
},
style: ElevatedButton.styleFrom(
primary: Colors.blueAccent,
),
child: Text(
isButtonLocked ? translate("I Agree")+" (${_countdown}s)" : translate("I Agree"),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
SizedBox(width: 15),
Container(
constraints: BoxConstraints(maxWidth: 150),
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
primary: Colors.blueAccent,
),
child: Text(
translate("Decline"),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
)])),
contentPadding: EdgeInsets.all(0.0),
);
}
}
class ServerInfo extends StatelessWidget {

View File

@@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:settings_ui/settings_ui.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
@@ -44,10 +45,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _enableDirectIPAccess = false;
var _enableRecordSession = false;
var _autoRecordIncomingSession = false;
var _allowAutoDisconnect = false;
var _localIP = "";
var _directAccessPort = "";
var _fingerprint = "";
var _buildDate = "";
var _autoDisconnectTimeout = "";
@override
void initState() {
@@ -150,6 +153,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_buildDate = buildDate;
}
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
await bind.mainGetOption(key: 'allow-auto-disconnect'));
if (allowAutoDisconnect != _allowAutoDisconnect) {
update = true;
_allowAutoDisconnect = allowAutoDisconnect;
}
final autoDisconnectTimeout =
await bind.mainGetOption(key: 'auto-disconnect-timeout');
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
update = true;
_autoDisconnectTimeout = autoDisconnectTimeout;
}
if (update) {
setState(() {});
}
@@ -305,6 +322,48 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
await bind.mainSetOption(key: 'direct-server', value: value);
setState(() {});
},
),
SettingsTile.switchTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("auto_disconnect_option_tip")),
Offstage(
offstage: !_allowAutoDisconnect,
child: Text(
'${_autoDisconnectTimeout.isEmpty ? '10' : _autoDisconnectTimeout} min',
style: Theme.of(context).textTheme.bodySmall,
)),
])),
Offstage(
offstage: !_allowAutoDisconnect,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.edit,
size: 20,
),
onPressed: () async {
final timeout = await changeAutoDisconnectTimeout(
_autoDisconnectTimeout);
setState(() {
_autoDisconnectTimeout = timeout;
});
}))
]),
initialValue: _allowAutoDisconnect,
onToggle: (_) async {
_allowAutoDisconnect = !_allowAutoDisconnect;
String value =
bool2option('allow-auto-disconnect', _allowAutoDisconnect);
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value);
setState(() {});
},
)
];
if (_hasIgnoreBattery) {
@@ -383,7 +442,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsSection(
title: Text(translate('Account')),
tiles: [
SettingsTile.navigation(
SettingsTile(
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
@@ -399,19 +458,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
],
),
SettingsSection(title: Text(translate("Settings")), tiles: [
SettingsTile.navigation(
SettingsTile(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings(gFFI.dialogManager);
}),
SettingsTile.navigation(
SettingsTile(
title: Text(translate('Language')),
leading: Icon(Icons.translate),
onPressed: (context) {
showLanguageSettings(gFFI.dialogManager);
}),
SettingsTile.navigation(
SettingsTile(
title: Text(translate(
Theme.of(context).brightness == Brightness.light
? 'Dark Theme'
@@ -424,45 +483,50 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
},
)
]),
SettingsSection(
title: Text(translate("Recording")),
tiles: [
SettingsTile.switchTile(
title: Text(translate('Automatically record incoming sessions')),
leading: Icon(Icons.videocam),
description: FutureBuilder(
builder: (ctx, data) => Offstage(
offstage: !data.hasData,
child: Text("${translate("Directory")}: ${data.data}")),
future: bind.mainDefaultVideoSaveDirectory()),
initialValue: _autoRecordIncomingSession,
onToggle: (v) async {
await bind.mainSetOption(
key: "allow-auto-record-incoming",
value: bool2option("allow-auto-record-incoming", v));
final newValue = option2bool(
'allow-auto-record-incoming',
await bind.mainGetOption(
key: 'allow-auto-record-incoming'));
setState(() {
_autoRecordIncomingSession = newValue;
});
},
),
],
),
SettingsSection(
title: Text(translate("Share Screen")),
tiles: shareScreenTiles,
),
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
),
if (isAndroid)
SettingsSection(
title: Text(translate("Recording")),
tiles: [
SettingsTile.switchTile(
title:
Text(translate('Automatically record incoming sessions')),
leading: Icon(Icons.videocam),
description: FutureBuilder(
builder: (ctx, data) => Offstage(
offstage: !data.hasData,
child: Text("${translate("Directory")}: ${data.data}")),
future: bind.mainDefaultVideoSaveDirectory()),
initialValue: _autoRecordIncomingSession,
onToggle: (v) async {
await bind.mainSetOption(
key: "allow-auto-record-incoming",
value: bool2option("allow-auto-record-incoming", v));
final newValue = option2bool(
'allow-auto-record-incoming',
await bind.mainGetOption(
key: 'allow-auto-record-incoming'));
setState(() {
_autoRecordIncomingSession = newValue;
});
},
),
],
),
if (isAndroid)
SettingsSection(
title: Text(translate("Share Screen")),
tiles: shareScreenTiles,
),
defaultDisplaySection(),
if (isAndroid)
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
),
SettingsSection(
title: Text(translate("About")),
tiles: [
SettingsTile.navigation(
SettingsTile(
onPressed: (context) async {
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
@@ -477,21 +541,28 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
)),
),
leading: Icon(Icons.info)),
SettingsTile.navigation(
SettingsTile(
title: Text(translate("Build Date")),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(_buildDate),
),
leading: Icon(Icons.query_builder)),
SettingsTile.navigation(
onPressed: (context) => onCopyFingerprint(_fingerprint),
title: Text(translate("Fingerprint")),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(_fingerprint),
),
leading: Icon(Icons.fingerprint)),
if (isAndroid)
SettingsTile(
onPressed: (context) => onCopyFingerprint(_fingerprint),
title: Text(translate("Fingerprint")),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(_fingerprint),
),
leading: Icon(Icons.fingerprint)),
SettingsTile(
title: Text(translate("Privacy Statement")),
onPressed: (context) =>
launchUrlString('https://rustdesk.com/privacy.html'),
leading: Icon(Icons.privacy_tip),
)
],
),
],
@@ -508,6 +579,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
}
return true;
}
defaultDisplaySection() {
return SettingsSection(
title: Text(translate("Display Settings")),
tiles: [
SettingsTile(
title: Text(translate('Display Settings')),
leading: Icon(Icons.desktop_windows_outlined),
trailing: Icon(Icons.arrow_forward_ios),
onPressed: (context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return _DisplayPage();
}));
})
],
);
}
}
void showServerSettings(OverlayDialogManager dialogManager) async {
@@ -618,3 +706,181 @@ class ScanButton extends StatelessWidget {
);
}
}
class _DisplayPage extends StatefulWidget {
const _DisplayPage({super.key});
@override
State<_DisplayPage> createState() => __DisplayPageState();
}
class __DisplayPageState extends State<_DisplayPage> {
@override
Widget build(BuildContext context) {
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
final h264 = codecsJson['h264'] ?? false;
final h265 = codecsJson['h265'] ?? false;
var codecList = [
_RadioEntry('Auto', 'auto'),
_RadioEntry('VP8', 'vp8'),
_RadioEntry('VP9', 'vp9'),
_RadioEntry('AV1', 'av1'),
if (h264) _RadioEntry('H264', 'h264'),
if (h265) _RadioEntry('H265', 'h265')
];
RxBool showCustomImageQuality = false.obs;
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.arrow_back_ios)),
title: Text(translate('Display Settings')),
centerTitle: true,
),
body: SettingsList(sections: [
SettingsSection(
tiles: [
_getPopupDialogRadioEntry(
title: 'Default View Style',
list: [
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
],
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
asyncSetter: (value) async {
await bind.mainSetUserDefaultOption(
key: 'view_style', value: value);
},
),
_getPopupDialogRadioEntry(
title: 'Default Image Quality',
list: [
_RadioEntry('Good image quality', kRemoteImageQualityBest),
_RadioEntry('Balanced', kRemoteImageQualityBalanced),
_RadioEntry('Optimize reaction time', kRemoteImageQualityLow),
_RadioEntry('Custom', kRemoteImageQualityCustom),
],
getter: () {
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
return v;
},
asyncSetter: (value) async {
await bind.mainSetUserDefaultOption(
key: 'image_quality', value: value);
showCustomImageQuality.value =
value == kRemoteImageQualityCustom;
},
tail: customImageQualitySetting(),
showTail: showCustomImageQuality,
notCloseValue: kRemoteImageQualityCustom,
),
_getPopupDialogRadioEntry(
title: 'Default Codec',
list: codecList,
getter: () =>
bind.mainGetUserDefaultOption(key: 'codec-preference'),
asyncSetter: (value) async {
await bind.mainSetUserDefaultOption(
key: 'codec-preference', value: value);
},
),
],
),
SettingsSection(
title: Text(translate('Other Default Options')),
tiles: [
otherRow('Show remote cursor', 'show_remote_cursor'),
otherRow('Show quality monitor', 'show_quality_monitor'),
otherRow('Mute', 'disable_audio'),
otherRow('Disable clipboard', 'disable_clipboard'),
otherRow('Lock after session end', 'lock_after_session_end'),
otherRow('Privacy mode', 'privacy_mode'),
otherRow('Touch mode', 'touch-mode'),
],
),
]),
);
}
otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
return SettingsTile.switchTile(
initialValue: value,
title: Text(translate(label)),
onToggle: (b) async {
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
setState(() {});
},
);
}
}
class _RadioEntry {
final String label;
final String value;
_RadioEntry(this.label, this.value);
}
typedef _RadioEntryGetter = String Function();
typedef _RadioEntrySetter = Future<void> Function(String);
_getPopupDialogRadioEntry({
required String title,
required List<_RadioEntry> list,
required _RadioEntryGetter getter,
required _RadioEntrySetter asyncSetter,
Widget? tail,
RxBool? showTail,
String? notCloseValue,
}) {
RxString groupValue = ''.obs;
RxString valueText = ''.obs;
init() {
groupValue.value = getter();
final e = list.firstWhereOrNull((e) => e.value == groupValue.value);
if (e != null) {
valueText.value = e.label;
}
}
init();
void showDialog() async {
gFFI.dialogManager.show((setState, close, context) {
onChanged(String? value) async {
if (value == null) return;
await asyncSetter(value);
init();
if (value != notCloseValue) {
close();
}
}
return CustomAlertDialog(
content: Obx(
() => Column(children: [
...list
.map((e) => getRadio(Text(translate(e.label)), e.value,
groupValue.value, (String? value) => onChanged(value)))
.toList(),
Offstage(
offstage:
!(tail != null && showTail != null && showTail.value == true),
child: tail,
),
]),
));
}, backDismiss: true, clickMaskDismiss: true);
}
return SettingsTile(
title: Text(translate(title)),
onPressed: (context) => showDialog(),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Obx(() => Text(translate(valueText.value))),
),
);
}

View File

@@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -147,59 +147,72 @@ void setTemporaryPasswordLengthDialog(
void showServerSettingsWithValue(
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
final oldCfg = ServerConfig.fromOptions(oldOptions);
var isInProgress = false;
final idCtrl = TextEditingController(text: serverConfig.idServer);
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
final apiCtrl = TextEditingController(text: serverConfig.apiServer);
final keyCtrl = TextEditingController(text: serverConfig.key);
String? idServerMsg;
String? relayServerMsg;
String? apiServerMsg;
RxString idServerMsg = ''.obs;
RxString relayServerMsg = ''.obs;
RxString apiServerMsg = ''.obs;
final controllers = [idCtrl, relayCtrl, apiCtrl, keyCtrl];
final errMsgs = [
idServerMsg,
relayServerMsg,
apiServerMsg,
];
dialogManager.show((setState, close, context) {
Future<bool> validate() async {
if (idCtrl.text != oldCfg.idServer) {
final res = await validateAsync(idCtrl.text);
setState(() => idServerMsg = res);
if (idServerMsg != null) return false;
}
if (relayCtrl.text != oldCfg.relayServer) {
relayServerMsg = await validateAsync(relayCtrl.text);
if (relayServerMsg != null) return false;
}
if (apiCtrl.text != oldCfg.apiServer) {
if (apiServerMsg != null) return false;
}
return true;
Future<bool> submit() async {
setState(() {
isInProgress = true;
});
bool ret = await setServerConfig(
controllers,
errMsgs,
ServerConfig(
idServer: idCtrl.text.trim(),
relayServer: relayCtrl.text.trim(),
apiServer: apiCtrl.text.trim(),
key: keyCtrl.text.trim()));
setState(() {
isInProgress = false;
});
return ret;
}
return CustomAlertDialog(
title: Text(translate('ID/Relay Server')),
title: Row(
children: [
Expanded(child: Text(translate('ID/Relay Server'))),
...ServerConfigImportExportWidgets(controllers, errMsgs),
],
),
content: Form(
child: Column(
child: Obx(() => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: idCtrl,
decoration: InputDecoration(
labelText: translate('ID Server'),
errorText: idServerMsg),
errorText: idServerMsg.value.isEmpty
? null
: idServerMsg.value),
)
] +
[
TextFormField(
controller: relayCtrl,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg.value.isEmpty
? null
: relayServerMsg.value),
)
] +
(isAndroid
? [
TextFormField(
controller: relayCtrl,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg),
)
]
: []) +
[
TextFormField(
controller: apiCtrl,
@@ -214,7 +227,7 @@ void showServerSettingsWithValue(
return translate("invalid_http");
}
}
return apiServerMsg;
return null;
},
),
TextFormField(
@@ -225,7 +238,7 @@ void showServerSettingsWithValue(
),
// NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(),
])),
]))),
actions: [
dialogButton('Cancel', onPressed: () {
close();
@@ -233,35 +246,12 @@ void showServerSettingsWithValue(
dialogButton(
'OK',
onPressed: () async {
setState(() {
idServerMsg = null;
relayServerMsg = null;
apiServerMsg = null;
isInProgress = true;
});
if (await validate()) {
if (idCtrl.text != oldCfg.idServer) {
if (oldCfg.idServer.isNotEmpty) {
await gFFI.userModel.logOut();
}
bind.mainSetOption(
key: "custom-rendezvous-server", value: idCtrl.text);
}
if (relayCtrl.text != oldCfg.relayServer) {
bind.mainSetOption(key: "relay-server", value: relayCtrl.text);
}
if (keyCtrl.text != oldCfg.key) {
bind.mainSetOption(key: "key", value: keyCtrl.text);
}
if (apiCtrl.text != oldCfg.apiServer) {
bind.mainSetOption(key: "api-server", value: apiCtrl.text);
}
if (await submit()) {
close();
showToast(translate('Successful'));
} else {
showToast(translate('Failed'));
}
setState(() {
isInProgress = false;
});
},
),
],

View File

@@ -3,9 +3,9 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
@@ -23,13 +23,20 @@ bool shouldSortTags() {
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
}
final filterAbTagOption = 'filter-ab-by-intersection';
bool filterAbTagByIntersection() {
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
}
class AbModel {
final abLoading = false.obs;
final pullError = "".obs;
final pushError = "".obs;
final tags = [].obs;
final RxMap<String, int> tagColors = Map<String, int>.fromEntries([]).obs;
final peers = List<Peer>.empty(growable: true).obs;
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
final retrying = false.obs;
bool get emtpy => peers.isEmpty && tags.isEmpty;
@@ -80,10 +87,11 @@ class AbModel {
if (resp.body.toLowerCase() == "null") {
// normal reply, emtpy ab return null
tags.clear();
tagColors.clear();
peers.clear();
} else if (resp.body.isNotEmpty) {
Map<String, dynamic> json =
_jsonDecode(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
} else if (json.containsKey('data')) {
@@ -93,26 +101,7 @@ class AbModel {
} catch (e) {}
final data = jsonDecode(json['data']);
if (data != null) {
final oldOnlineIDs =
peers.where((e) => e.online).map((e) => e.id).toList();
tags.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
if (isFull(false)) {
peers.removeRange(licensedDevices, peers.length);
}
// restore online
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
_deserialize(data);
_saveCache(); // save on success
}
}
@@ -121,9 +110,6 @@ class AbModel {
if (!quiet) {
pullError.value =
'${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) {
BotToast.showText(contentColor: Colors.red, text: pullError.value);
}
}
} finally {
abLoading.value = false;
@@ -132,9 +118,10 @@ class AbModel {
_timerCounter = 0;
if (pullError.isNotEmpty) {
if (statusCode == 401) {
gFFI.userModel.reset(clearAbCache: true);
gFFI.userModel.reset(resetOther: true);
}
}
platformFFI.tryHandle({'name': LoadEvent.addressBook});
}
}
@@ -147,6 +134,7 @@ class AbModel {
'alias': alias,
'tags': tags,
});
_mergePeerFromGroup(peer);
peers.add(peer);
}
@@ -242,10 +230,7 @@ class AbModel {
final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList();
final body = jsonEncode({
"data": jsonEncode({"tags": tags, "peers": peersJsonData})
});
final body = jsonEncode({"data": jsonEncode(_serialize())});
http.Response resp;
// support compression
if (licensedDevices > 0 && body.length > 1024) {
@@ -261,7 +246,8 @@ class AbModel {
ret = true;
_saveCache();
} else {
Map<String, dynamic> json = _jsonDecode(resp.body, resp.statusCode);
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
} else if (resp.statusCode == 200) {
@@ -318,6 +304,7 @@ class AbModel {
void deleteTag(String tag) {
gFFI.abModel.selectedTags.remove(tag);
tags.removeWhere((element) => element == tag);
tagColors.remove(tag);
for (var peer in peers) {
if (peer.tags.isEmpty) {
continue;
@@ -353,6 +340,11 @@ class AbModel {
}
}).toList();
}
int? oldColor = tagColors[oldTag];
if (oldColor != null) {
tagColors.remove(oldTag);
tagColors.addAll({newTag: oldColor});
}
}
void unsetSelectedTags() {
@@ -368,6 +360,20 @@ class AbModel {
}
}
Color getTagColor(String tag) {
int? colorValue = tagColors[tag];
if (colorValue != null) {
return Color(colorValue);
}
return str2color2(tag, existing: tagColors.values.toList());
}
setTagColor(String tag, Color color) {
if (tags.contains(tag)) {
tagColors[tag] = color.value;
}
}
void merge(Peer r, Peer p) {
p.hash = r.hash.isEmpty ? p.hash : r.hash;
p.username = r.username.isEmpty ? p.username : r.username;
@@ -467,43 +473,33 @@ class AbModel {
_saveCache() {
try {
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList();
final m = <String, dynamic>{
var m = _serialize();
m.addAll(<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'),
"peers": peersJsonData,
"tags": tags.map((e) => e.toString()).toList(),
};
});
bind.mainSaveAb(json: jsonEncode(m));
} catch (e) {
debugPrint('ab save:$e');
}
}
loadCache() async {
Future<void> loadCache() async {
try {
if (_cacheLoadOnceFlag || abLoading.value) return;
if (_cacheLoadOnceFlag || abLoading.value || initialized) return;
_cacheLoadOnceFlag = true;
final access_token = bind.mainGetLocalOption(key: 'access_token');
if (access_token.isEmpty) return;
final cache = await bind.mainLoadAb();
if (abLoading.value) return;
final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return;
tags.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
_deserialize(data);
} catch (e) {
debugPrint("load ab cache: $e");
}
}
Map<String, dynamic> _jsonDecode(String body, int statusCode) {
Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
try {
Map<String, dynamic> json = jsonDecode(body);
return json;
@@ -516,6 +512,50 @@ class AbModel {
}
}
Map<String, dynamic> _serialize() {
final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList();
final tagColorJsonData = jsonEncode(tagColors);
return {
"tags": tags,
"peers": peersJsonData,
"tag_colors": tagColorJsonData
};
}
_deserialize(dynamic data) {
if (data == null) return;
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
tags.clear();
tagColors.clear();
peers.clear();
if (data['tags'] is List) {
tags.value = data['tags'];
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
if (isFull(false)) {
peers.removeRange(licensedDevices, peers.length);
}
// restore online
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
if (data['tag_colors'] is String) {
Map<String, dynamic> map = jsonDecode(data['tag_colors']);
tagColors.value = Map<String, int>.from(map);
}
// add color to tag
final tagsWithoutColor =
tags.toList().where((e) => !tagColors.containsKey(e)).toList();
for (var t in tagsWithoutColor) {
tagColors[t] = str2color2(t, existing: tagColors.values.toList()).value;
}
}
reSyncToast(Future<bool> future) {
if (!shouldSyncAb()) return;
Future.delayed(Duration.zero, () async {
@@ -528,4 +568,26 @@ class AbModel {
}
});
}
reset() async {
pullError.value = '';
pushError.value = '';
tags.clear();
peers.clear();
await bind.mainClearAb();
}
_mergePeerFromGroup(Peer p) {
final g = gFFI.groupModel.peers.firstWhereOrNull((e) => p.id == e.id);
if (g == null) return;
if (p.username.isEmpty) {
p.username = g.username;
}
if (p.hostname.isEmpty) {
p.hostname = g.hostname;
}
if (p.platform.isEmpty) {
p.platform = g.platform;
}
}
}

View File

@@ -10,7 +10,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/mobile/pages/home_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
@@ -63,7 +62,7 @@ class ChatModel with ChangeNotifier {
bool isConnManager = false;
RxBool isWindowFocus = true.obs;
BlockableOverlayState? _blockableOverlayState;
BlockableOverlayState _blockableOverlayState = BlockableOverlayState();
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
@@ -72,6 +71,13 @@ class ChatModel with ChangeNotifier {
RxInt mobileUnreadSum = 0.obs;
MessageKey? latestReceivedKey;
Offset chatWindowPosition = Offset(20, 80);
void setChatWindowPosition(Offset position) {
chatWindowPosition = position;
notifyListeners();
}
@override
void dispose() {
textController.dispose();
@@ -86,13 +92,13 @@ class ChatModel with ChangeNotifier {
late final Map<MessageKey, MessageBody> _messages = {};
MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value
late bool _isShowCMChatPage = false;
late bool _isShowCMSidePage = false;
Map<MessageKey, MessageBody> get messages => _messages;
MessageKey get currentKey => _currentKey;
bool get isShowCMChatPage => _isShowCMChatPage;
bool get isShowCMSidePage => _isShowCMSidePage;
void setOverlayState(BlockableOverlayState blockableOverlayState) {
_blockableOverlayState = blockableOverlayState;
@@ -154,7 +160,7 @@ class ChatModel with ChangeNotifier {
}
}
final overlayState = _blockableOverlayState?.state;
final overlayState = _blockableOverlayState.state;
if (overlayState == null) return;
final overlay = OverlayEntry(builder: (context) {
@@ -210,7 +216,7 @@ class ChatModel with ChangeNotifier {
}
},
child: DraggableChatWindow(
position: chatInitPos ?? Offset(20, 80),
position: chatInitPos ?? chatWindowPosition,
width: 250,
height: 350,
chatModel: this));
@@ -255,7 +261,7 @@ class ChatModel with ChangeNotifier {
showChatPage(MessageKey key) async {
if (isDesktop) {
if (isConnManager) {
if (!_isShowCMChatPage) {
if (!_isShowCMSidePage) {
await toggleCMChatPage(key);
}
} else {
@@ -276,8 +282,15 @@ class ChatModel with ChangeNotifier {
if (gFFI.chatModel.currentKey != key) {
gFFI.chatModel.changeCurrentKey(key);
}
if (_isShowCMChatPage) {
_isShowCMChatPage = !_isShowCMChatPage;
await toggleCMSidePage();
}
var _togglingCMSidePage = false; // protect order for await
toggleCMSidePage() async {
if (_togglingCMSidePage) return false;
_togglingCMSidePage = true;
if (_isShowCMSidePage) {
_isShowCMSidePage = !_isShowCMSidePage;
notifyListeners();
await windowManager.show();
await windowManager.setSizeAlignment(
@@ -287,9 +300,10 @@ class ChatModel with ChangeNotifier {
await windowManager.show();
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeOpenChat, Alignment.topRight);
_isShowCMChatPage = !_isShowCMChatPage;
_isShowCMSidePage = !_isShowCMSidePage;
notifyListeners();
}
_togglingCMSidePage = false;
}
changeCurrentKey(MessageKey key) {
@@ -396,7 +410,7 @@ class ChatModel with ChangeNotifier {
parent.target?.serverModel.jumpTo(id);
}
} else {
if (HomePage.homeKey.currentState?.selectedIndex != 1 ||
if (HomePage.homeKey.currentState?.isChatPageCurrentTab != true ||
_currentKey != messagekey) {
client.unreadChatMessageCount.value += 1;
mobileUpdateUnreadSum();

View File

@@ -0,0 +1,142 @@
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:get/get.dart';
import 'file_model.dart';
class CmFileModel {
final WeakReference<FFI> parent;
final currentJobTable = RxList<JobProgress>();
final _jobTables = HashMap<int, RxList<JobProgress>>.fromEntries([]);
Stopwatch stopwatch = Stopwatch();
int _lastElapsed = 0;
CmFileModel(this.parent);
void updateCurrentClientId(int id) {
if (_jobTables[id] == null) {
_jobTables[id] = RxList<JobProgress>();
}
Future.delayed(Duration.zero, () {
currentJobTable.value = _jobTables[id]!;
});
}
onFileTransferLog(dynamic log) {
try {
dynamic d = jsonDecode(log);
if (!stopwatch.isRunning) stopwatch.start();
bool calcSpeed = stopwatch.elapsedMilliseconds - _lastElapsed >= 1000;
if (calcSpeed) {
_lastElapsed = stopwatch.elapsedMilliseconds;
}
if (d is List<dynamic>) {
for (var l in d) {
_dealOneJob(l, calcSpeed);
}
} else {
_dealOneJob(d, calcSpeed);
}
currentJobTable.refresh();
} catch (e) {
debugPrint("onFileTransferLog:$e");
}
}
_dealOneJob(dynamic l, bool calcSpeed) {
final data = TransferJobSerdeData.fromJson(l);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
return;
}
JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
if (job == null) {
job = JobProgress();
jobTable.add(job);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
}
job.id = data.id;
job.isRemoteToLocal = data.isRemote;
job.fileName = data.path;
job.totalSize = data.totalSize;
job.finishedSize = data.finishedSize;
if (job.finishedSize > data.totalSize) {
job.finishedSize = data.totalSize;
}
job.isRemoteToLocal = data.isRemote;
if (job.finishedSize > 0) {
if (job.finishedSize < job.totalSize) {
job.state = JobState.inProgress;
} else {
job.state = JobState.done;
}
}
if (data.done) {
job.state = JobState.done;
} else if (data.cancel || data.error == 'skipped') {
job.state = JobState.done;
job.err = 'skipped';
} else if (data.error.isNotEmpty) {
job.state = JobState.error;
job.err = data.error;
}
if (calcSpeed) {
job.speed = (data.transferred - job.lastTransferredSize) * 1.0;
job.lastTransferredSize = data.transferred;
}
jobTable.refresh();
}
}
class TransferJobSerdeData {
int connId;
int id;
String path;
bool isRemote;
int totalSize;
int finishedSize;
int transferred;
bool done;
bool cancel;
String error;
TransferJobSerdeData({
required this.connId,
required this.id,
required this.path,
required this.isRemote,
required this.totalSize,
required this.finishedSize,
required this.transferred,
required this.done,
required this.cancel,
required this.error,
});
TransferJobSerdeData.fromJson(dynamic d)
: this(
connId: d['connId'] ?? 0,
id: int.tryParse(d['id'].toString()) ?? 0,
path: d['path'] ?? '',
isRemote: d['isRemote'] ?? false,
totalSize: d['totalSize'] ?? 0,
finishedSize: d['finishedSize'] ?? 0,
transferred: d['transferred'] ?? 0,
done: d['done'] ?? false,
cancel: d['cancel'] ?? false,
error: d['error'] ?? '',
);
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
@@ -9,7 +8,7 @@ class RenderTexture {
final RxInt textureId = RxInt(-1);
int _textureKey = -1;
SessionID? _sessionId;
final useTextureRender = bind.mainUseTextureRender();
static final useTextureRender = bind.mainUseTextureRender();
final textureRenderer = TextureRgbaRenderer();
@@ -21,7 +20,6 @@ class RenderTexture {
_sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async {
debugPrint("id: $id, texture_key: $_textureKey");
if (id != -1) {
final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerTexture(sessionId, ptr);

View File

@@ -1029,6 +1029,7 @@ class JobProgress {
var to = "";
var showHidden = false;
var err = "";
int lastTransferredSize = 0;
clear() {
state = JobState.none;

View File

@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
@@ -11,57 +12,74 @@ import 'package:http/http.dart' as http;
class GroupModel {
final RxBool groupLoading = false.obs;
final RxString groupLoadError = "".obs;
final RxString groupId = ''.obs;
RxString groupName = ''.obs;
final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<Peer> peersShow = RxList.empty(growable: true);
final RxList<Peer> peers = RxList.empty(growable: true);
final RxString selectedUser = ''.obs;
final RxString searchUserText = ''.obs;
WeakReference<FFI> parent;
var initialized = false;
var _cacheLoadOnceFlag = false;
var _statusCode = 200;
bool get emtpy => users.isEmpty && peers.isEmpty;
GroupModel(this.parent);
reset() {
groupName.value = '';
groupId.value = '';
users.clear();
peersShow.clear();
initialized = false;
}
Future<void> pull({force = true, quiet = false}) async {
/*
if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (!force && initialized) return;
if (!quiet) {
groupLoading.value = true;
groupLoadError.value = "";
}
await _pull();
try {
await _pull();
} catch (_) {}
groupLoading.value = false;
initialized = true;
*/
platformFFI.tryHandle({'name': LoadEvent.group});
if (_statusCode == 401) {
gFFI.userModel.reset(resetOther: true);
} else {
_saveCache();
}
}
Future<void> _pull() async {
reset();
if (bind.mainGetLocalOption(key: 'access_token') == '') {
List<UserPayload> tmpUsers = List.empty(growable: true);
if (!await _getUsers(tmpUsers)) {
return;
}
try {
if (!await _getGroup()) {
reset();
return;
}
} catch (e) {
debugPrint('$e');
reset();
List<Peer> tmpPeers = List.empty(growable: true);
if (!await _getPeers(tmpPeers)) {
return;
}
// me first
var index = tmpUsers
.indexWhere((user) => user.name == gFFI.userModel.userName.value);
if (index != -1) {
var user = tmpUsers.removeAt(index);
tmpUsers.insert(0, user);
}
users.value = tmpUsers;
if (!users.any((u) => u.name == selectedUser.value)) {
selectedUser.value = '';
}
// recover online
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
peers.value = tmpPeers;
peers
.where((e) => oldOnlineIDs.contains(e.id))
.map((e) => e.online = true)
.toList();
groupLoadError.value = '';
}
Future<bool> _getUsers(List<UserPayload> tmpUsers) async {
final api = "${await bind.mainGetApiServer()}/api/users";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
final pageSize = 100;
var total = 0;
int current = 0;
do {
@@ -74,86 +92,68 @@ class GroupModel {
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
if (gFFI.userModel.isAdmin.isFalse) 'grp': groupId.value,
'accessible': '',
'status': '1',
});
final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(utf8.decode(resp.bodyBytes));
if (json.containsKey('error')) {
throw json['error'];
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) {
if (json['error'] == 'Admin required!' ||
json['error']
.toString()
.contains('ambiguous column name: status')) {
throw translate('upgrade_rustdesk_server_pro_to_{1.1.10}_tip');
} else {
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final user in data) {
final u = UserPayload.fromJson(user);
if (!users.any((e) => e.name == u.name)) {
users.add(u);
}
}
throw json['error'];
}
}
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final user in data) {
final u = UserPayload.fromJson(user);
int index = tmpUsers.indexWhere((e) => e.name == u.name);
if (index < 0) {
tmpUsers.add(u);
} else {
tmpUsers[index] = u;
}
}
}
}
}
} while (current * pageSize < total);
return true;
} catch (err) {
debugPrint('$err');
groupLoadError.value = err.toString();
} finally {
_pullUserPeers();
debugPrint('get accessible users: $err');
groupLoadError.value =
'${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
}
Future<bool> _getGroup() async {
final url = await bind.mainGetApiServer();
final body = {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid()
};
try {
final response = await http.post(Uri.parse('$url/api/currentGroup'),
headers: getHttpHeaders(), body: json.encode(body));
final status = response.statusCode;
if (status == 401 || status == 400) {
return false;
}
final data = json.decode(utf8.decode(response.bodyBytes));
final error = data['error'];
if (error != null) {
throw error;
}
groupName.value = data['name'] ?? '';
groupId.value = data['guid'] ?? '';
return groupId.value.isNotEmpty && groupName.isNotEmpty;
} catch (e) {
debugPrint('$e');
groupLoadError.value = e.toString();
} finally {}
return false;
}
Future<void> _pullUserPeers() async {
peersShow.clear();
final api = "${await bind.mainGetApiServer()}/api/peers";
Future<bool> _getPeers(List<Peer> tmpPeers) async {
try {
final api = "${await bind.mainGetApiServer()}/api/peers";
var uri0 = Uri.parse(api);
final pageSize =
20; // ????????????????????????????????????????????????????? stupid stupis, how about >20 peers
final pageSize = 100;
var total = 0;
int current = 0;
var queryParameters = {
'current': current.toString(),
'pageSize': pageSize.toString(),
};
if (!gFFI.userModel.isAdmin.value) {
queryParameters.addAll({'grp': groupId.value});
}
do {
current += 1;
var queryParameters = {
'current': current.toString(),
'pageSize': pageSize.toString(),
'accessible': '',
'status': '1',
};
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
@@ -161,32 +161,102 @@ class GroupModel {
port: uri0.port,
queryParameters: queryParameters);
final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(utf8.decode(resp.bodyBytes));
if (json.containsKey('error')) {
throw json['error'];
} else {
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final p in data) {
final peerPayload = PeerPayload.fromJson(p);
final peer = PeerPayload.toPeer(peerPayload);
if (!peersShow.any((e) => e.id == peer.id)) {
peersShow.add(peer);
}
}
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final p in data) {
final peerPayload = PeerPayload.fromJson(p);
final peer = PeerPayload.toPeer(peerPayload);
int index = tmpPeers.indexWhere((e) => e.id == peer.id);
if (index < 0) {
tmpPeers.add(peer);
} else {
tmpPeers[index] = peer;
}
}
}
}
}
} while (current * pageSize < total);
return true;
} catch (err) {
debugPrint('$err');
groupLoadError.value = err.toString();
} finally {}
debugPrint('get accessible peers: $err');
groupLoadError.value =
'${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
return false;
}
Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
try {
Map<String, dynamic> json = jsonDecode(body);
return json;
} catch (e) {
final err = body.isNotEmpty && body.length < 128 ? body : e.toString();
if (statusCode != 200) {
throw 'HTTP $statusCode, $err';
}
throw err;
}
}
void _saveCache() {
try {
final map = (<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'),
"users": users.map((e) => e.toGroupCacheJson()).toList(),
'peers': peers.map((e) => e.toGroupCacheJson()).toList()
});
bind.mainSaveGroup(json: jsonEncode(map));
} catch (e) {
debugPrint('group save:$e');
}
}
Future<void> loadCache() async {
try {
if (_cacheLoadOnceFlag || groupLoading.value || initialized) return;
_cacheLoadOnceFlag = true;
final access_token = bind.mainGetLocalOption(key: 'access_token');
if (access_token.isEmpty) return;
final cache = await bind.mainLoadGroup();
if (groupLoading.value) return;
final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return;
users.clear();
peers.clear();
if (data['users'] is List) {
for (var u in data['users']) {
users.add(UserPayload.fromJson(u));
}
}
if (data['peers'] is List) {
for (final peer in data['peers']) {
peers.add(Peer.fromJson(peer));
}
}
} catch (e) {
debugPrint("load group cache: $e");
}
}
reset() async {
groupLoadError.value = '';
users.clear();
peers.clear();
selectedUser.value = '';
await bind.mainClearGroup();
}
}

View File

@@ -35,6 +35,24 @@ extension ToString on MouseButtons {
}
}
class PointerEventToRust {
final String kind;
final String type;
final dynamic value;
PointerEventToRust(this.kind, this.type, this.value);
Map<String, dynamic> toJson() {
return {
'k': kind,
'v': {
't': type,
'v': value,
}
};
}
}
class InputModel {
final WeakReference<FFI> parent;
String keyboardMode = "legacy";
@@ -62,11 +80,11 @@ class InputModel {
int _lastButtons = 0;
Offset lastMousePos = Offset.zero;
get id => parent.target?.id ?? "";
late final SessionID sessionId;
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
InputModel(this.parent) {
sessionId = parent.target!.sessionId;
@@ -223,14 +241,8 @@ class InputModel {
command: command);
}
Map<String, dynamic> getEvent(PointerEvent evt, String type) {
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
out['x'] = evt.position.dx;
out['y'] = evt.position.dy;
if (alt) out['alt'] = 'true';
if (shift) out['shift'] = 'true';
if (ctrl) out['ctrl'] = 'true';
if (command) out['command'] = 'true';
// Check update event type and set buttons to be sent.
int buttons = _lastButtons;
@@ -260,7 +272,6 @@ class InputModel {
out['buttons'] = buttons;
out['type'] = type;
return out;
}
@@ -292,7 +303,7 @@ class InputModel {
}
/// Modify the given modifier map [evt] based on current modifier key status.
Map<String, String> modify(Map<String, String> evt) {
Map<String, dynamic> modify(Map<String, dynamic> evt) {
if (ctrl) evt['ctrl'] = 'true';
if (shift) evt['shift'] = 'true';
if (alt) evt['alt'] = 'true';
@@ -334,27 +345,33 @@ class InputModel {
isPhysicalMouse.value = true;
}
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventMove));
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
}
}
void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
_lastScale = 1.0;
_stopFling = true;
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position);
}
}
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale;
if (peerPlatform != kPeerPlatformAndroid) {
final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale;
if (scale != 0) {
bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode({
'touch': {'scale': scale}
}));
return;
if (scale != 0) {
bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
.toJson()));
return;
}
}
final delta = e.panDelta;
@@ -362,7 +379,7 @@ class InputModel {
var x = delta.dx.toInt();
var y = delta.dy.toInt();
if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) {
if (peerPlatform == kPeerPlatformLinux) {
_trackpadScrollUnsent += (delta * _trackpadSpeed);
x = _trackpadScrollUnsent.dx.truncate();
y = _trackpadScrollUnsent.dy.truncate();
@@ -378,9 +395,13 @@ class InputModel {
}
}
if (x != 0 || y != 0) {
bind.sessionSendMouse(
sessionId: sessionId,
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_update', Offset(x.toDouble(), y.toDouble()));
} else {
bind.sessionSendMouse(
sessionId: sessionId,
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
}
}
}
@@ -436,11 +457,15 @@ class InputModel {
}
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_end', e.position);
return;
}
bind.sessionSendPointer(
sessionId: sessionId,
msg: json.encode({
'touch': {'scale': 0}
}));
msg: json.encode(
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
waitLastFlingDone();
_stopFling = false;
@@ -465,21 +490,21 @@ class InputModel {
}
}
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventDown));
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
}
}
void onPointUpImage(PointerUpEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventUp));
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
}
}
void onPointMoveImage(PointerMoveEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, _kMouseEventMove));
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
}
}
@@ -504,19 +529,16 @@ class InputModel {
}
void refreshMousePos() => handleMouse({
'x': lastMousePos.dx,
'y': lastMousePos.dy,
'buttons': 0,
'type': _kMouseEventMove,
});
}, lastMousePos);
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
{
'x': pos.dx,
'y': pos.dy,
'buttons': 0,
'type': _kMouseEventMove,
},
pos,
onExit: true,
);
@@ -550,17 +572,49 @@ class InputModel {
return Offset(x, y);
}
void handleMouse(
Map<String, dynamic> evt, {
bool onExit = false,
}) {
double x = evt['x'];
double y = max(0.0, evt['y']);
final cursorModel = parent.target!.cursorModel;
void handlePointerEvent(String kind, String type, Offset offset) {
double x = offset.dx;
double y = offset.dy;
if (_checkPeerControlProtected(x, y)) {
return;
}
// Only touch events are handled for now. So we can just ignore buttons.
// to-do: handle mouse events
late final dynamic evtValue;
if (type == 'pan_update') {
evtValue = {
'x': x.toInt(),
'y': y.toInt(),
};
} else {
final isMoveTypes = ['pan_start', 'pan_end'];
final pos = handlePointerDevicePos(
kPointerEventKindTouch,
x,
y,
isMoveTypes.contains(type),
type,
);
if (pos == null) {
return;
}
evtValue = {
'x': pos.x,
'y': pos.y,
};
}
final evt = PointerEventToRust(kind, type, evtValue).toJson();
bind.sessionSendPointer(
sessionId: sessionId, msg: json.encode(modify(evt)));
}
bool _checkPeerControlProtected(double x, double y) {
final cursorModel = parent.target!.cursorModel;
if (cursorModel.isPeerControlProtected) {
lastMousePos = ui.Offset(x, y);
return;
return true;
}
if (!cursorModel.gotMouseControl) {
@@ -571,10 +625,23 @@ class InputModel {
cursorModel.gotMouseControl = true;
} else {
lastMousePos = ui.Offset(x, y);
return;
return true;
}
}
lastMousePos = ui.Offset(x, y);
return false;
}
void handleMouse(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
}) {
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
return;
}
var type = '';
var isMove = false;
@@ -592,17 +659,58 @@ class InputModel {
return;
}
evt['type'] = type;
final pos = handlePointerDevicePos(
kPointerEventKindMouse,
x,
y,
isMove,
type,
onExit: onExit,
buttons: evt['buttons'],
);
if (pos == null) {
return;
}
if (type != '') {
evt['x'] = '0';
evt['y'] = '0';
} else {
evt['x'] = '${pos.x}';
evt['y'] = '${pos.y}';
}
Map<int, String> mapButtons = {
kPrimaryMouseButton: 'left',
kSecondaryMouseButton: 'right',
kMiddleMouseButton: 'wheel',
kBackMouseButton: 'back',
kForwardMouseButton: 'forward'
};
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
}
Point? handlePointerDevicePos(
String kind,
double x,
double y,
bool isMove,
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel;
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final ffiModel = parent.target!.ffiModel;
if (isMove) {
canvasModel.moveDesktopMouse(x, y);
}
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final d = ffiModel.display;
final imageWidth = d.width * canvasModel.scale;
final imageHeight = d.height * canvasModel.scale;
@@ -650,7 +758,7 @@ class InputModel {
} catch (e) {
debugPrintStack(
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
return;
return null;
}
int minX = d.x.toInt();
@@ -659,40 +767,16 @@ class InputModel {
int maxY = (d.y + d.height).toInt() - 1;
evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
// If left mouse up, no early return.
if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
return;
if (kind == kPointerEventKindMouse) {
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
// If left mouse up, no early return.
if (!(buttons == kPrimaryMouseButton && evtType == 'up')) {
return null;
}
}
}
if (type != '') {
evtX = 0;
evtY = 0;
}
evt['x'] = '$evtX';
evt['y'] = '$evtY';
var buttons = '';
switch (evt['buttons']) {
case kPrimaryMouseButton:
buttons = 'left';
break;
case kSecondaryMouseButton:
buttons = 'right';
break;
case kMiddleMouseButton:
buttons = 'wheel';
break;
case kBackMouseButton:
buttons = 'back';
break;
case kForwardMouseButton:
buttons = 'forward';
break;
}
evt['buttons'] = buttons;
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(evt));
return Point(evtX, evtY);
}
/// Web only

View File

@@ -4,12 +4,14 @@ import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
@@ -37,11 +39,52 @@ import 'platform_model.dart';
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
final _waitForImageDialogShow = <UuidValue, bool>{};
final _waitForFirstImage = <UuidValue, bool>{};
final _constSessionId = Uuid().v4obj();
class CachedPeerData {
Map<String, dynamic> updatePrivacyMode = {};
Map<String, dynamic> peerInfo = {};
List<Map<String, dynamic>> cursorDataList = [];
Map<String, dynamic> lastCursorId = {};
bool secure = false;
bool direct = false;
CachedPeerData();
@override
String toString() {
return jsonEncode({
'updatePrivacyMode': updatePrivacyMode,
'peerInfo': peerInfo,
'cursorDataList': cursorDataList,
'lastCursorId': lastCursorId,
'secure': secure,
'direct': direct,
});
}
static CachedPeerData? fromString(String s) {
try {
final map = jsonDecode(s);
final data = CachedPeerData();
data.updatePrivacyMode = map['updatePrivacyMode'];
data.peerInfo = map['peerInfo'];
for (final cursorData in map['cursorDataList']) {
data.cursorDataList.add(cursorData);
}
data.lastCursorId = map['lastCursorId'];
data.secure = map['secure'];
data.direct = map['direct'];
return data;
} catch (e) {
debugPrint('Failed to parse CachedPeerData: $e');
return null;
}
}
}
class FfiModel with ChangeNotifier {
CachedPeerData cachedPeerData = CachedPeerData();
PeerInfo _pi = PeerInfo();
Display _display = Display();
@@ -56,6 +99,10 @@ class FfiModel with ChangeNotifier {
WeakReference<FFI> parent;
late final SessionID sessionId;
RxBool waitForImageDialogShow = true.obs;
Timer? waitForImageTimer;
RxBool waitForFirstImage = true.obs;
Map<String, bool> get permissions => _permissions;
Display get display => _display;
@@ -114,9 +161,12 @@ class FfiModel with ChangeNotifier {
_timer?.cancel();
_timer = null;
clearPermissions();
waitForImageTimer?.cancel();
}
setConnectionType(String peerId, bool secure, bool direct) {
cachedPeerData.secure = secure;
cachedPeerData.direct = direct;
_secure = secure;
_direct = direct;
try {
@@ -143,6 +193,24 @@ class FfiModel with ChangeNotifier {
_permissions.clear();
}
handleCachedPeerData(CachedPeerData data, String peerId) async {
handleMsgBox({
'type': 'success',
'title': 'Successful',
'text': 'Connected, waiting for image...',
'link': '',
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
await handlePeerInfo(data.peerInfo, peerId);
for (final element in data.cursorDataList) {
updateLastCursorId(element);
await handleCursorData(element);
}
updateLastCursorId(data.lastCursorId);
handleCursorId(data.lastCursorId);
}
// todo: why called by two position
StreamEventHandler startEventListener(SessionID sessionId, String peerId) {
return (evt) async {
@@ -159,9 +227,11 @@ class FfiModel with ChangeNotifier {
} else if (name == 'switch_display') {
handleSwitchDisplay(evt, sessionId, peerId);
} else if (name == 'cursor_data') {
await parent.target?.cursorModel.updateCursorData(evt);
updateLastCursorId(evt);
await handleCursorData(evt);
} else if (name == 'cursor_id') {
await parent.target?.cursorModel.updateCursorId(evt);
updateLastCursorId(evt);
handleCursorId(evt);
} else if (name == 'cursor_position') {
await parent.target?.cursorModel.updateCursorPosition(evt, peerId);
} else if (name == 'clipboard') {
@@ -199,8 +269,6 @@ class FfiModel with ChangeNotifier {
updateBlockInputState(evt, peerId);
} else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt, sessionId, peerId);
} else if (name == 'alias') {
handleAliasChanged(evt);
} else if (name == 'show_elevation') {
final show = evt['show'].toString() == 'true';
parent.target?.serverModel.setShowElevation(show);
@@ -252,6 +320,10 @@ class FfiModel with ChangeNotifier {
}
}
}
} else if (name == "cm_file_transfer_log") {
if (isDesktop) {
gFFI.cmFileModel.onFileTransferLog(evt['log']);
}
} else {
debugPrint('Unknown event name: $name');
}
@@ -282,13 +354,6 @@ class FfiModel with ChangeNotifier {
platformFFI.setEventCallback(startEventListener(sessionId, peerId));
}
handleAliasChanged(Map<String, dynamic> evt) {
final rxAlias = PeerStringOption.find(evt['id'], 'alias');
if (rxAlias.value != evt['alias']) {
rxAlias.value = evt['alias'];
}
}
_updateCurDisplay(SessionID sessionId, Display newDisplay) {
if (newDisplay != _display) {
if (newDisplay.x != _display.x || newDisplay.y != _display.y) {
@@ -436,7 +501,7 @@ class FfiModel with ChangeNotifier {
closeConnection();
}
if (_waitForFirstImage[sessionId] == false) return;
if (waitForFirstImage.isFalse) return;
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
@@ -447,7 +512,12 @@ class FfiModel with ChangeNotifier {
onCancel: onClose),
tag: '$sessionId-waiting-for-image',
);
_waitForImageDialogShow[sessionId] = true;
waitForImageDialogShow.value = true;
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
if (waitForFirstImage.isTrue) {
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
}
});
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
}
@@ -464,6 +534,8 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
cachedPeerData.peerInfo = evt;
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers();
@@ -482,23 +554,13 @@ class FfiModel with ChangeNotifier {
}
final connType = parent.target?.connType;
if (isPeerAndroid) {
_touchMode = true;
if (connType == ConnType.defaultConn &&
parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer(
const Duration(milliseconds: 100),
() => parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!));
}
} else {
_touchMode = await bind.sessionGetOption(
sessionId: sessionId, arg: 'touch-mode') !=
'';
}
if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady();
} else if (connType == ConnType.defaultConn) {
@@ -514,7 +576,7 @@ class FfiModel with ChangeNotifier {
}
if (displays.isNotEmpty) {
_reconnects = 1;
_waitForFirstImage[sessionId] = true;
waitForFirstImage.value = true;
}
Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1;
@@ -538,11 +600,25 @@ class FfiModel with ChangeNotifier {
}
}
_pi.isSet.value = true;
stateGlobal.resetLastResolutionGroupValues(peerId);
notifyListeners();
}
tryShowAndroidActionsOverlay({int delayMSecs = 10}) {
if (isPeerAndroid) {
if (parent.target?.connType == ConnType.defaultConn &&
parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer(
Duration(milliseconds: delayMSecs),
() => parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!));
}
}
}
handleResolutions(String id, dynamic resolutions) {
try {
final List<dynamic> dynamicArray = jsonDecode(resolutions as String);
@@ -579,9 +655,24 @@ class FfiModel with ChangeNotifier {
return d;
}
updateLastCursorId(Map<String, dynamic> evt) {
parent.target?.cursorModel.id = int.parse(evt['id']);
}
handleCursorId(Map<String, dynamic> evt) {
cachedPeerData.lastCursorId = evt;
parent.target?.cursorModel.updateCursorId(evt);
}
handleCursorData(Map<String, dynamic> evt) async {
cachedPeerData.cursorDataList.add(evt);
await parent.target?.cursorModel.updateCursorData(evt);
}
/// Handle the peer info synchronization event based on [evt].
handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async {
if (evt['displays'] != null) {
cachedPeerData.peerInfo['displays'] = evt['displays'];
List<dynamic> displays = json.decode(evt['displays']);
List<Display> newDisplays = [];
for (int i = 0; i < displays.length; ++i) {
@@ -1196,6 +1287,7 @@ class CursorModel with ChangeNotifier {
final _cacheKeys = <String>{};
double _x = -10000;
double _y = -10000;
int _id = -1;
double _hotx = 0;
double _hoty = 0;
double _displayOriginX = 0;
@@ -1204,7 +1296,7 @@ class CursorModel with ChangeNotifier {
bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String id = '';
String peerId = '';
WeakReference<FFI> parent;
ui.Image? get image => _image;
@@ -1218,6 +1310,8 @@ class CursorModel with ChangeNotifier {
double get hotx => _hotx;
double get hoty => _hoty;
set id(int id) => _id = id;
bool get isPeerControlProtected =>
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec;
@@ -1356,32 +1450,33 @@ class CursorModel with ChangeNotifier {
}
updateCursorData(Map<String, dynamic> evt) async {
var id = int.parse(evt['id']);
_hotx = double.parse(evt['hotx']);
_hoty = double.parse(evt['hoty']);
var width = int.parse(evt['width']);
var height = int.parse(evt['height']);
final id = int.parse(evt['id']);
final hotx = double.parse(evt['hotx']);
final hoty = double.parse(evt['hoty']);
final width = int.parse(evt['width']);
final height = int.parse(evt['height']);
List<dynamic> colors = json.decode(evt['colors']);
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
final image = await img.decodeImageFromPixels(
rgba, width, height, ui.PixelFormat.rgba8888);
_image = image;
if (await _updateCache(rgba, image, id, width, height)) {
_images[id] = Tuple3(image, _hotx, _hoty);
} else {
_hotx = 0;
_hoty = 0;
}
try {
// my throw exception, because the listener maybe already dispose
notifyListeners();
} catch (e) {
debugPrint('WARNING: updateCursorId $id, without notifyListeners(). $e');
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
_images[id] = Tuple3(image, hotx, hoty);
}
// Update last cursor data.
// Do not use the previous `image` and `id`, because `_id` may be changed.
_updateCurData();
}
Future<bool> _updateCache(
Uint8List rgba, ui.Image image, int id, int w, int h) async {
Uint8List rgba,
ui.Image image,
int id,
double hotx,
double hoty,
int w,
int h,
) async {
Uint8List? data;
img2.Image imgOrigin = img2.Image.fromBytes(
width: w, height: h, bytes: rgba.buffer, order: img2.ChannelOrder.rgba);
@@ -1395,33 +1490,45 @@ class CursorModel with ChangeNotifier {
}
data = imgBytes.buffer.asUint8List();
}
_cache = CursorData(
peerId: this.id,
final cache = CursorData(
peerId: peerId,
id: id,
image: imgOrigin,
scale: 1.0,
data: data,
hotxOrigin: _hotx,
hotyOrigin: _hoty,
hotxOrigin: hotx,
hotyOrigin: hoty,
width: w,
height: h,
);
_cacheMap[id] = _cache!;
_cacheMap[id] = cache;
return true;
}
updateCursorId(Map<String, dynamic> evt) async {
final id = int.parse(evt['id']);
_cache = _cacheMap[id];
final tmp = _images[id];
bool _updateCurData() {
_cache = _cacheMap[_id];
final tmp = _images[_id];
if (tmp != null) {
_image = tmp.item1;
_hotx = tmp.item2;
_hoty = tmp.item3;
notifyListeners();
try {
// may throw exception, because the listener maybe already dispose
notifyListeners();
} catch (e) {
debugPrint(
'WARNING: updateCursorId $_id, without notifyListeners(). $e');
}
return true;
} else {
return false;
}
}
updateCursorId(Map<String, dynamic> evt) {
if (!_updateCurData()) {
debugPrint(
'WARNING: updateCursorId $id, cache is ${_cache == null ? "null" : "not null"}. without notifyListeners()');
'WARNING: updateCursorId $_id, cache is ${_cache == null ? "null" : "not null"}. without notifyListeners()');
}
}
@@ -1576,6 +1683,7 @@ class ElevationModel with ChangeNotifier {
bool get showRequestMenu => _canElevate && !_running;
onPeerInfo(PeerInfo pi) {
_canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false;
_running = false;
}
onPortableServiceRunning(Map<String, dynamic> evt) {
@@ -1596,7 +1704,6 @@ class FFI {
/// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager();
late final bool isSessionAdded;
late final SessionID sessionId;
late final ImageModel imageModel; // session
late final FfiModel ffiModel; // session
@@ -1613,9 +1720,9 @@ class FFI {
late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session
late final ElevationModel elevationModel; // session
late final CmFileModel cmFileModel; // cm
FFI(SessionID? sId) {
isSessionAdded = sId != null;
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
imageModel = ImageModel(WeakReference(this));
ffiModel = FfiModel(WeakReference(this));
@@ -1632,6 +1739,15 @@ class FFI {
recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this));
elevationModel = ElevationModel(WeakReference(this));
cmFileModel = CmFileModel(WeakReference(this));
}
/// Mobile reuse FFI
void mobileReset() {
ffiModel.waitForFirstImage.value = true;
ffiModel.waitForImageDialogShow.value = true;
ffiModel.waitForImageTimer?.cancel();
ffiModel.waitForImageTimer = null;
}
/// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
@@ -1641,9 +1757,11 @@ class FFI {
bool isRdp = false,
String? switchUuid,
String? password,
bool? forceRelay}) {
bool? forceRelay,
int? tabWindowId}) {
closed = false;
auditNote = '';
if (isMobile) mobileReset();
assert(!(isFileTransfer && isPortForward), 'more than one connect type');
if (isFileTransfer) {
connType = ConnType.fileTransfer;
@@ -1654,9 +1772,11 @@ class FFI {
connType = ConnType.defaultConn;
canvasModel.id = id;
imageModel.id = id;
cursorModel.id = id;
cursorModel.peerId = id;
}
if (!isSessionAdded) {
// If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one.
if (tabWindowId == null) {
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
sessionId: sessionId,
@@ -1677,8 +1797,25 @@ class FFI {
// Preserved for the rgba data.
stream.listen((message) {
if (closed) return;
if (isSessionAdded && !isToNewWindowNotified.value) {
bind.sessionReadyToNewWindow(sessionId: sessionId);
if (tabWindowId != null && !isToNewWindowNotified.value) {
// Session is read to be moved to a new window.
// Get the cached data and handle the cached data.
Future.delayed(Duration.zero, () async {
final cachedData = await DesktopMultiWindow.invokeMethod(
tabWindowId, kWindowEventGetCachedSessionData, id);
if (cachedData == null) {
// unreachable
debugPrint('Unreachable, the cached data is empty.');
return;
}
final data = CachedPeerData.fromString(cachedData);
if (data == null) {
debugPrint('Unreachable, the cached data cannot be decoded.');
return;
}
await ffiModel.handleCachedPeerData(data, id);
await bind.sessionRefresh(sessionId: sessionId);
});
isToNewWindowNotified.value = true;
}
() async {
@@ -1704,7 +1841,7 @@ class FFI {
} else {
// Fetch the image buffer from rust codes.
final sz = platformFFI.getRgbaSize(sessionId);
if (sz == null || sz == 0) {
if (sz == 0) {
return;
}
final rgba = platformFFI.getRgba(sessionId, sz);
@@ -1721,12 +1858,13 @@ class FFI {
}
void onEvent2UIRgba() async {
if (_waitForImageDialogShow[sessionId] == true) {
_waitForImageDialogShow[sessionId] = false;
if (ffiModel.waitForImageDialogShow.isTrue) {
ffiModel.waitForImageDialogShow.value = false;
ffiModel.waitForImageTimer?.cancel();
clearWaitingForImage(dialogManager, sessionId);
}
if (_waitForFirstImage[sessionId] == true) {
_waitForFirstImage[sessionId] = false;
if (ffiModel.waitForFirstImage.value == true) {
ffiModel.waitForFirstImage.value = false;
dialogManager.dismissAll();
await canvasModel.updateViewStyle();
await canvasModel.updateScrollStyle();
@@ -1841,7 +1979,7 @@ class Features {
bool privacyMode = false;
}
class PeerInfo {
class PeerInfo with ChangeNotifier {
String version = '';
String username = '';
String hostname = '';
@@ -1853,6 +1991,8 @@ class PeerInfo {
List<Resolution> resolutions = [];
Map<String, dynamic> platform_additions = {};
RxBool isSet = false.obs;
bool get is_wayland => platform_additions['is_wayland'] == true;
bool get is_headless => platform_additions['headless'] == true;
}

View File

@@ -21,16 +21,8 @@ class RgbaFrame extends Struct {
external Pointer<Uint8> data;
}
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>);
typedef F4 = Uint64 Function(Pointer<Utf8>);
typedef F4Dart = int Function(Pointer<Utf8>);
typedef F5 = Void Function(Pointer<Utf8>);
typedef F5Dart = void Function(Pointer<Utf8>);
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
// pub fn session_register_texture(id: *const char, ptr: usize)
typedef F6 = Void Function(Pointer<Utf8>, Uint64);
typedef F6Dart = void Function(Pointer<Utf8>, int);
/// FFI wrapper around the native Rust core.
/// Hides the platform differences.
@@ -38,7 +30,6 @@ class PlatformFFI {
String _dir = '';
// _homeDir is only needed for Android and IOS.
String _homeDir = '';
F2? _translate;
final _eventHandlers = <String, Map<String, HandleEvent>>{};
late RustdeskImpl _ffiBind;
late String _appType;
@@ -51,9 +42,6 @@ class PlatformFFI {
RustdeskImpl get ffiBind => _ffiBind;
F3? _session_get_rgba;
F4Dart? _session_get_rgba_size;
F5Dart? _session_next_rgba;
F6Dart? _session_register_texture;
static get localeName => Platform.localeName;
@@ -89,18 +77,8 @@ class PlatformFFI {
}
}
String translate(String name, String locale) {
if (_translate == null) return name;
var a = name.toNativeUtf8();
var b = locale.toNativeUtf8();
var p = _translate!(a, b);
assert(p != nullptr);
final res = p.toDartString();
calloc.free(p);
calloc.free(a);
calloc.free(b);
return res;
}
String translate(String name, String locale) =>
_ffiBind.translate(name: name, locale: locale);
Uint8List? getRgba(SessionID sessionId, int bufSize) {
if (_session_get_rgba == null) return null;
@@ -118,30 +96,12 @@ class PlatformFFI {
}
}
int? getRgbaSize(SessionID sessionId) {
if (_session_get_rgba_size == null) return null;
final sessionIdStr = sessionId.toString();
var a = sessionIdStr.toNativeUtf8();
final bufferSize = _session_get_rgba_size!(a);
malloc.free(a);
return bufferSize;
}
void nextRgba(SessionID sessionId) {
if (_session_next_rgba == null) return;
final sessionIdStr = sessionId.toString();
final a = sessionIdStr.toNativeUtf8();
_session_next_rgba!(a);
malloc.free(a);
}
void registerTexture(SessionID sessionId, int ptr) {
if (_session_register_texture == null) return;
final sessionIdStr = sessionId.toString();
final a = sessionIdStr.toNativeUtf8();
_session_register_texture!(a, ptr);
malloc.free(a);
}
int getRgbaSize(SessionID sessionId) =>
_ffiBind.sessionGetRgbaSize(sessionId: sessionId);
void nextRgba(SessionID sessionId) =>
_ffiBind.sessionNextRgba(sessionId: sessionId);
void registerTexture(SessionID sessionId, int ptr) =>
_ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
/// Init the FFI class, loads the native Rust core library.
Future<void> init(String appType) async {
@@ -157,14 +117,7 @@ class PlatformFFI {
: DynamicLibrary.process();
debugPrint('initializing FFI $_appType');
try {
_translate = dylib.lookupFunction<F2, F2>('translate');
_session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba");
_session_get_rgba_size =
dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
_session_next_rgba =
dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
_session_register_texture =
dylib.lookupFunction<F6, F6Dart>("session_register_texture");
try {
// SYSTEM user failed
_dir = (await getApplicationDocumentsDirectory()).path;
@@ -246,7 +199,7 @@ class PlatformFFI {
version = await getVersion();
}
Future<bool> _tryHandle(Map<String, dynamic> evt) async {
Future<bool> tryHandle(Map<String, dynamic> evt) async {
final name = evt['name'];
if (name != null) {
final handlers = _eventHandlers[name];
@@ -264,14 +217,15 @@ class PlatformFFI {
/// Start listening to the Rust core's events and frames.
void _startListenEvent(RustdeskImpl rustdeskImpl) {
final appType = _appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType;
final appType =
_appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType;
var sink = rustdeskImpl.startGlobalEventStream(appType: appType);
sink.listen((message) {
() async {
try {
Map<String, dynamic> event = json.decode(message);
// _tryHandle here may be more flexible than _eventCallback
if (!await _tryHandle(event)) {
if (!await tryHandle(event)) {
if (_eventCallback != null) {
await _eventCallback!(event);
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'platform_model.dart';
// ignore: depend_on_referenced_packages
import 'package:collection/collection.dart';
@@ -7,7 +8,7 @@ import 'package:collection/collection.dart';
class Peer {
final String id;
String hash;
String username;
String username; // pc username
String hostname;
String platform;
String alias;
@@ -16,6 +17,7 @@ class Peer {
String rdpPort;
String rdpUsername;
bool online = false;
String loginName; //login username
String getId() {
if (alias != '') {
@@ -34,7 +36,8 @@ class Peer {
tags = json['tags'] ?? [],
forceAlwaysRelay = json['forceAlwaysRelay'] == 'true',
rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? '';
rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '';
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -48,6 +51,7 @@ class Peer {
"forceAlwaysRelay": forceAlwaysRelay.toString(),
"rdpPort": rdpPort,
"rdpUsername": rdpUsername,
'loginName': loginName,
};
}
@@ -63,6 +67,16 @@ class Peer {
};
}
Map<String, dynamic> toGroupCacheJson() {
return <String, dynamic>{
"id": id,
"username": username,
"hostname": hostname,
"platform": platform,
"login_name": loginName,
};
}
Peer({
required this.id,
required this.hash,
@@ -74,6 +88,7 @@ class Peer {
required this.forceAlwaysRelay,
required this.rdpPort,
required this.rdpUsername,
required this.loginName,
});
Peer.loading()
@@ -88,6 +103,7 @@ class Peer {
forceAlwaysRelay: false,
rdpPort: '',
rdpUsername: '',
loginName: '',
);
bool equal(Peer other) {
return id == other.id &&
@@ -99,21 +115,24 @@ class Peer {
tags.equals(other.tags) &&
forceAlwaysRelay == other.forceAlwaysRelay &&
rdpPort == other.rdpPort &&
rdpUsername == other.rdpUsername;
rdpUsername == other.rdpUsername &&
loginName == other.loginName;
}
Peer.copy(Peer other)
: this(
id: other.id,
hash: other.hash,
username: other.username,
hostname: other.hostname,
platform: other.platform,
alias: other.alias,
tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername);
id: other.id,
hash: other.hash,
username: other.username,
hostname: other.hostname,
platform: other.platform,
alias: other.alias,
tags: other.tags.toList(),
forceAlwaysRelay: other.forceAlwaysRelay,
rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername,
loginName: other.loginName,
);
}
enum UpdateEvent { online, load }
@@ -121,11 +140,14 @@ enum UpdateEvent { online, load }
class Peers extends ChangeNotifier {
final String name;
final String loadEvent;
List<Peer> peers;
List<Peer> peers = List.empty(growable: true);
final RxList<Peer>? initPeers;
UpdateEvent event = UpdateEvent.load;
static const _cbQueryOnlines = 'callback_query_onlines';
Peers({required this.name, required this.peers, required this.loadEvent}) {
Peers(
{required this.name, required this.initPeers, required this.loadEvent}) {
peers = initPeers ?? [];
platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async {
_updateOnlineState(evt);
});
@@ -176,7 +198,11 @@ class Peers extends ChangeNotifier {
void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates();
peers = _decodePeers(evt['peers']);
if (initPeers != null) {
peers = initPeers!;
} else {
peers = _decodePeers(evt['peers']);
}
for (var peer in peers) {
final state = onlineStates[peer.id];
peer.online = state != null && state != false;

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
@@ -16,8 +17,6 @@ enum PeerTabIndex {
group,
}
const String defaultGroupTabname = 'Group';
class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent;
int get currentTab => _currentTab;
@@ -27,7 +26,7 @@ class PeerTabModel with ChangeNotifier {
'Favorites',
'Discovered',
'Address Book',
//defaultGroupTabname,
'Group',
];
final List<IconData> icons = [
Icons.access_time_filled,
@@ -36,7 +35,10 @@ class PeerTabModel with ChangeNotifier {
IconFont.addressBook,
Icons.group,
];
final List<bool> _isVisible = List.filled(5, true, growable: false);
List<bool> get isVisible => _isVisible;
List<int> get indexs => List.generate(tabNames.length, (index) => index);
List<int> get visibleIndexs => indexs.where((e) => _isVisible[e]).toList();
List<Peer> _selectedPeers = List.empty(growable: true);
List<Peer> get selectedPeers => _selectedPeers;
bool _multiSelectionMode = false;
@@ -49,12 +51,29 @@ class PeerTabModel with ChangeNotifier {
String get lastId => _lastId;
PeerTabModel(this.parent) {
// visible
try {
final option = bind.getLocalFlutterOption(k: 'peer-tab-visible');
if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == _isVisible.length) {
for (int i = 0; i < _isVisible.length; i++) {
if (decodeList[i] is bool) {
_isVisible[i] = decodeList[i];
}
}
}
}
} catch (e) {
debugPrint("failed to get peer tab visible list:$e");
}
// init currentTab
_currentTab =
int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0;
if (_currentTab < 0 || _currentTab >= tabNames.length) {
_currentTab = 0;
}
_trySetCurrentTabToFirstVisible();
}
setCurrentTab(int index) {
@@ -64,17 +83,9 @@ class PeerTabModel with ChangeNotifier {
}
}
String tabTooltip(int index, String groupName) {
String tabTooltip(int index) {
if (index >= 0 && index < tabNames.length) {
if (index == PeerTabIndex.group.index) {
if (gFFI.userModel.isAdmin.value || groupName.isEmpty) {
return translate(defaultGroupTabname);
} else {
return '${translate('Group')}: $groupName';
}
} else {
return translate(tabNames[index]);
}
return translate(tabNames[index]);
}
assert(false);
return index.toString();
@@ -158,4 +169,31 @@ class PeerTabModel with ChangeNotifier {
}
}
}
setTabVisible(int index, bool visible) {
if (index >= 0 && index < _isVisible.length) {
if (_isVisible[index] != visible) {
_isVisible[index] = visible;
if (index == _currentTab && !visible) {
_trySetCurrentTabToFirstVisible();
} else if (visible && visibleIndexs.length == 1) {
_currentTab = index;
}
try {
bind.setLocalFlutterOption(
k: 'peer-tab-visible', v: jsonEncode(_isVisible));
} catch (_) {}
notifyListeners();
}
}
}
_trySetCurrentTabToFirstVisible() {
if (!_isVisible[_currentTab]) {
int firstVisible = _isVisible.indexWhere((e) => e);
if (firstVisible >= 0) {
_currentTab = firstVisible;
}
}
}
}

View File

@@ -31,11 +31,12 @@ class ServerModel with ChangeNotifier {
bool _audioOk = false;
bool _fileOk = false;
bool _showElevation = false;
bool _hideCm = false;
bool hideCm = false;
int _connectStatus = 0; // Rendezvous Server status
String _verificationMethod = "";
String _temporaryPasswordLength = "";
String _approveMode = "";
int _zeroClientLengthCounter = 0;
late String _emptyIdShow;
late final IDTextEditingController _serverId;
@@ -60,8 +61,6 @@ class ServerModel with ChangeNotifier {
bool get showElevation => _showElevation;
bool get hideCm => _hideCm;
int get connectStatus => _connectStatus;
String get verificationMethod {
@@ -120,6 +119,19 @@ class ServerModel with ChangeNotifier {
_emptyIdShow = translate("Generating ...");
_serverId = IDTextEditingController(text: _emptyIdShow);
/*
// initital _hideCm at startup
final verificationMethod =
bind.mainGetOptionSync(key: "verification-method");
final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
_hideCm = option2bool(
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
if (!(approveMode == 'password' &&
verificationMethod == kUsePermanentPassword)) {
_hideCm = false;
}
*/
timerCallback() async {
final connectionStatus =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
@@ -134,6 +146,17 @@ class ServerModel with ChangeNotifier {
if (res != null) {
debugPrint("clients not match!");
updateClientState(res);
} else {
if (_clients.isEmpty) {
hideCmWindow();
if (_zeroClientLengthCounter++ == 12) {
// 6 second
windowManager.close();
}
} else {
_zeroClientLengthCounter = 0;
if (!hideCm) showCmWindow();
}
}
}
@@ -187,12 +210,14 @@ class ServerModel with ChangeNotifier {
final temporaryPasswordLength =
await bind.mainGetOption(key: "temporary-password-length");
final approveMode = await bind.mainGetOption(key: 'approve-mode');
/*
var hideCm = option2bool(
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
if (!(approveMode == 'password' &&
verificationMethod == kUsePermanentPassword)) {
hideCm = false;
}
*/
if (_approveMode != approveMode) {
_approveMode = approveMode;
update = true;
@@ -224,6 +249,7 @@ class ServerModel with ChangeNotifier {
_temporaryPasswordLength = temporaryPasswordLength;
update = true;
}
/*
if (_hideCm != hideCm) {
_hideCm = hideCm;
if (desktopType == DesktopType.cm) {
@@ -235,6 +261,7 @@ class ServerModel with ChangeNotifier {
}
update = true;
}
*/
if (update) {
notifyListeners();
}
@@ -422,6 +449,7 @@ class ServerModel with ChangeNotifier {
return;
}
final oldClientLenght = _clients.length;
_clients.clear();
tabController.state.value.tabs.clear();
@@ -434,6 +462,16 @@ class ServerModel with ChangeNotifier {
debugPrint("Failed to decode clientJson '$clientJson', error $e");
}
}
if (desktopType == DesktopType.cm) {
if (_clients.isEmpty) {
hideCmWindow();
} else if (!hideCm) {
showCmWindow();
}
}
if (_clients.length != oldClientLenght) {
notifyListeners();
}
}
void addConnection(Map<String, dynamic> evt) {
@@ -461,6 +499,9 @@ class ServerModel with ChangeNotifier {
_clients.removeAt(index_disconnected);
tabController.remove(index_disconnected);
}
if (desktopType == DesktopType.cm && !hideCm) {
showCmWindow();
}
scrollToBottom();
notifyListeners();
if (isAndroid && !client.authorized) showLoginDialog(client);
@@ -581,6 +622,9 @@ class ServerModel with ChangeNotifier {
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
parent.target?.invokeMethod("cancel_notification", id);
}
if (desktopType == DesktopType.cm && _clients.isEmpty) {
hideCmWindow();
}
notifyListeners();
} catch (e) {
debugPrint("onClientRemove failed,error:$e");

View File

@@ -20,6 +20,8 @@ class StateGlobal {
final RxBool showRemoteToolBar = false.obs;
final RxInt displaysCount = 0.obs;
final svcStatus = SvcStatus.notReady.obs;
// Only used for macOS
bool closeOnFullscreen = false;
// Use for desktop -> remote toolbar -> resolution
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
@@ -64,7 +66,7 @@ class StateGlobal {
setMinimized(bool v) => _isMinimized = v;
setFullscreen(bool v) {
setFullscreen(bool v, {bool procWnd = true}) {
if (_fullscreen != v) {
_fullscreen = v;
_showTabBar.value = !_fullscreen;
@@ -76,20 +78,22 @@ class StateGlobal {
print(
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
_windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth;
WindowController.fromWindowId(windowId)
.setFullscreen(_fullscreen)
.then((_) {
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
if (Platform.isWindows && !v) {
Future.delayed(Duration.zero, () async {
final frame =
await WindowController.fromWindowId(windowId).getFrame();
final newRect = Rect.fromLTWH(
frame.left, frame.top, frame.width + 1, frame.height + 1);
await WindowController.fromWindowId(windowId).setFrame(newRect);
});
}
});
if (procWnd) {
WindowController.fromWindowId(windowId)
.setFullscreen(_fullscreen)
.then((_) {
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
if (Platform.isWindows && !v) {
Future.delayed(Duration.zero, () async {
final frame =
await WindowController.fromWindowId(windowId).getFrame();
final newRect = Rect.fromLTWH(
frame.left, frame.top, frame.width + 1, frame.height + 1);
await WindowController.fromWindowId(windowId).setFrame(newRect);
});
}
});
}
}
}

View File

@@ -45,7 +45,7 @@ class UserModel {
refreshingUser = false;
final status = response.statusCode;
if (status == 401 || status == 400) {
reset(clearAbCache: status == 401);
reset(resetOther: status == 401);
return;
}
final data = json.decode(utf8.decode(response.bodyBytes));
@@ -84,11 +84,13 @@ class UserModel {
}
}
Future<void> reset({bool clearAbCache = false}) async {
Future<void> reset({bool resetOther = false}) async {
await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
if (clearAbCache) await bind.mainClearAb();
await gFFI.groupModel.reset();
if (resetOther) {
await gFFI.abModel.reset();
await gFFI.groupModel.reset();
}
userName.value = '';
}
@@ -120,7 +122,7 @@ class UserModel {
} catch (e) {
debugPrint("request /api/logout failed: err=$e");
} finally {
await reset(clearAbCache: true);
await reset(resetOther: true);
gFFI.dialogManager.dismissByTag(tag);
}
}

View File

@@ -28,6 +28,13 @@ extension Index on int {
}
}
class MultiWindowCallResult {
int windowId;
dynamic result;
MultiWindowCallResult(this.windowId, this.result);
}
/// Window Manager
/// mainly use it in `Main Window`
/// use it in sub window is not recommended
@@ -47,6 +54,7 @@ class RustDeskMultiWindowManager {
var params = {
'type': WindowType.RemoteDesktop.index,
'id': peerId,
'tab_window_id': windowId,
'session_id': sessionId,
};
await _newSession(
@@ -57,17 +65,15 @@ class RustDeskMultiWindowManager {
_remoteDesktopWindows,
jsonEncode(params),
);
await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventCloseForSeparateWindow, peerId);
}
newSessionWindow(
Future<int> newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
final windowId = windowController.windowId;
windowController
..setFrame(const Offset(0, 0) &
Size(1280 + windowController.windowId * 20,
720 + windowController.windowId * 20))
..setFrame(
const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20))
..center()
..setTitle(getWindowNameWithId(
remoteId,
@@ -76,11 +82,12 @@ class RustDeskMultiWindowManager {
if (Platform.isMacOS) {
Future.microtask(() => windowController.show());
}
registerActiveWindow(windowController.windowId);
windows.add(windowController.windowId);
registerActiveWindow(windowId);
windows.add(windowId);
return windowId;
}
_newSession(
Future<MultiWindowCallResult> _newSession(
bool openInTabs,
WindowType type,
String methodName,
@@ -90,9 +97,10 @@ class RustDeskMultiWindowManager {
) async {
if (openInTabs) {
if (windows.isEmpty) {
await newSessionWindow(type, remoteId, msg, windows);
final windowId = await newSessionWindow(type, remoteId, msg, windows);
return MultiWindowCallResult(windowId, null);
} else {
call(type, methodName, msg);
return call(type, methodName, msg);
}
} else {
if (_inactiveWindows.isNotEmpty) {
@@ -103,15 +111,16 @@ class RustDeskMultiWindowManager {
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
WindowController.fromWindowId(windowId).show();
registerActiveWindow(windowId);
return;
return MultiWindowCallResult(windowId, null);
}
}
}
await newSessionWindow(type, remoteId, msg, windows);
final windowId = await newSessionWindow(type, remoteId, msg, windows);
return MultiWindowCallResult(windowId, null);
}
}
Future<dynamic> newSession(
Future<MultiWindowCallResult> newSession(
WindowType type,
String methodName,
String remoteId,
@@ -143,15 +152,15 @@ class RustDeskMultiWindowManager {
for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return;
return MultiWindowCallResult(windowId, null);
}
}
}
await _newSession(openInTabs, type, methodName, remoteId, windows, msg);
return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
}
Future<dynamic> newRemoteDesktop(
Future<MultiWindowCallResult> newRemoteDesktop(
String remoteId, {
String? password,
String? switchUuid,
@@ -168,7 +177,7 @@ class RustDeskMultiWindowManager {
);
}
Future<dynamic> newFileTransfer(String remoteId,
Future<MultiWindowCallResult> newFileTransfer(String remoteId,
{String? password, bool? forceRelay}) async {
return await newSession(
WindowType.FileTransfer,
@@ -180,7 +189,7 @@ class RustDeskMultiWindowManager {
);
}
Future<dynamic> newPortForward(String remoteId, bool isRDP,
Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
{String? password, bool? forceRelay}) async {
return await newSession(
WindowType.PortForward,
@@ -193,18 +202,22 @@ class RustDeskMultiWindowManager {
);
}
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
Future<MultiWindowCallResult> call(
WindowType type, String methodName, dynamic args) async {
final wnds = _findWindowsByType(type);
if (wnds.isEmpty) {
return;
return MultiWindowCallResult(kInvalidWindowId, null);
}
for (final windowId in wnds) {
if (_activeWindows.contains(windowId)) {
return await DesktopMultiWindow.invokeMethod(
windowId, methodName, args);
final res =
await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
return MultiWindowCallResult(windowId, res);
}
}
return await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
final res =
await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
return MultiWindowCallResult(wnds[0], res);
}
List<int> _findWindowsByType(WindowType type) {

View File

@@ -37,8 +37,6 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>

View File

@@ -237,10 +237,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.17.1"
colorize:
dependency: transitive
description:
@@ -300,11 +300,12 @@ packages:
dash_chat_2:
dependency: "direct main"
description:
name: dash_chat_2
sha256: e9e08b2a030d340d60f7adbeb977d3d6481db1f172b51440bfa02488b92fa19c
url: "https://pub.dev"
source: hosted
version: "0.0.17"
path: "."
ref: HEAD
resolved-ref: bd6b5b41254e57c5bcece202ebfb234de63e6487
url: "https://github.com/rustdesk-org/Dash-Chat-2"
source: git
version: "0.0.18"
debounce_throttle:
dependency: "direct main"
description:
@@ -327,7 +328,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "6c4181330f4ed80c1cb5670bd61aa75115f9f748"
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@@ -395,6 +396,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
dynamic_layouts:
dependency: "direct main"
description:
path: "."
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
url: "https://github.com/21pages/dynamic_layouts.git"
source: git
version: "0.0.1+1"
event_bus:
dependency: transitive
description:
@@ -451,6 +461,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
url: "https://pub.dev"
source: hosted
version: "3.3.0"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
flutter:
dependency: "direct main"
description: flutter
@@ -743,10 +769,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.18.0"
io:
dependency: transitive
description:
@@ -799,10 +825,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.2.0"
meta:
dependency: transitive
description:
@@ -1019,6 +1045,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
pull_down_button:
dependency: "direct main"
description:
name: pull_down_button
sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f"
url: "https://pub.dev"
source: hosted
version: "0.9.3"
puppeteer:
dependency: transitive
description:
@@ -1054,12 +1088,11 @@ packages:
screen_retriever:
dependency: transitive
description:
path: "."
ref: "406b9b0"
resolved-ref: "406b9b038b2c1d779f1e7bf609c8c248be247372"
url: "https://github.com/Kingtous/rustdesk_screen_retriever.git"
source: git
version: "0.1.2"
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
scroll_pos:
dependency: "direct main"
description:
@@ -1382,10 +1415,10 @@ packages:
dependency: transitive
description:
name: video_player
sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28"
sha256: d3910a8cefc0de8a432a4411dcf85030e885d8fef3ddea291f162253a05dbf01
url: "https://pub.dev"
source: hosted
version: "2.5.1"
version: "2.7.1"
video_player_android:
dependency: transitive
description:
@@ -1406,10 +1439,10 @@ packages:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae"
sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a
url: "https://pub.dev"
source: hosted
version: "6.0.1"
version: "6.2.1"
video_player_web:
dependency: transitive
description:
@@ -1474,14 +1507,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel:
dependency: transitive
description:
@@ -1511,10 +1536,10 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "2c4b242e668acf4e652b09b13f650bcfbbaa3871"
resolved-ref: f19acdb008645366339444a359a45c3257c8b32e
url: "https://github.com/rustdesk-org/window_manager"
source: git
version: "0.3.4"
version: "0.3.6"
window_size:
dependency: "direct main"
description:
@@ -1565,5 +1590,5 @@ packages:
source: hosted
version: "0.2.0"
sdks:
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.7.0-0"
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.10.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.2.2
version: 1.2.3+39
environment:
sdk: ">=2.17.0"
@@ -39,7 +39,9 @@ dependencies:
package_info_plus: ^3.1.2
url_launcher: ^6.0.9
toggle_switch: ^2.1.0
dash_chat_2: ^0.0.17
dash_chat_2:
git:
url: https://github.com/rustdesk-org/Dash-Chat-2
draggable_float_widget: ^0.0.2
settings_ui: ^2.0.2
flutter_breadcrumb: ^1.0.1
@@ -97,6 +99,12 @@ dependencies:
dropdown_button2: ^2.0.0
uuid: ^3.0.7
auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0
dynamic_layouts:
git:
url: https://github.com/21pages/dynamic_layouts.git
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3
dev_dependencies:
icons_launcher: ^2.0.4
@@ -149,9 +157,6 @@ flutter:
- family: AddressBook
fonts:
- asset: assets/address_book.ttf
- family: CheckBox
fonts:
- asset: assets/checkbox.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.

View File

@@ -16,7 +16,7 @@ final testClients = [
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
];
/// flutter run -d {platform} -t lib/cm_test.dart to test cm
/// flutter run -d {platform} -t test/cm_test.dart to test cm
void main(List<String> args) async {
isTest = true;
WidgetsFlutterBinding.ensureInitialized();

View File

@@ -93,7 +93,7 @@ BEGIN
VALUE "FileDescription", "rustdesk" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "rustdesk" "\0"
VALUE "LegalCopyright", "Copyright (C) 2022 com.carriez. All rights reserved." "\0"
VALUE "LegalCopyright", "Copyright (C) 2023 com.carriez. All rights reserved." "\0"
VALUE "OriginalFilename", "rustdesk.exe" "\0"
VALUE "ProductName", "rustdesk" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"

View File

@@ -70,6 +70,8 @@ pub use win::ENIGO_INPUT_EXTRA_VALUE;
mod macos;
#[cfg(target_os = "macos")]
pub use macos::Enigo;
#[cfg(target_os = "macos")]
pub use macos::ENIGO_INPUT_EXTRA_VALUE;
#[cfg(target_os = "linux")]
mod linux;

View File

@@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3;
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
const BUF_LEN: usize = 4;
/// The event source user data value of cgevent.
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
#[allow(improper_ctypes)]
#[allow(non_snake_case)]
#[link(name = "ApplicationServices", kind = "framework")]
@@ -131,6 +134,7 @@ impl Enigo {
fn post(&self, event: CGEvent) {
event.set_flags(self.flags);
event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE);
event.post(CGEventTapLocation::HID);
}
}

View File

@@ -1,4 +1,4 @@
mod macos_impl;
pub mod keycodes;
pub use self::macos_impl::Enigo;
pub use self::macos_impl::{Enigo, ENIGO_INPUT_EXTRA_VALUE};

View File

@@ -1,5 +1,4 @@
mod win_impl;
pub mod keycodes;
pub use self::win_impl::Enigo;
pub use self::win_impl::ENIGO_INPUT_EXTRA_VALUE;
pub use self::win_impl::{Enigo, ENIGO_INPUT_EXTRA_VALUE};

View File

@@ -118,9 +118,29 @@ message TouchScaleUpdate {
int32 scale = 1;
}
message TouchPanStart {
int32 x = 1;
int32 y = 2;
}
message TouchPanUpdate {
// The delta x position relative to the previous position.
int32 x = 1;
// The delta y position relative to the previous position.
int32 y = 2;
}
message TouchPanEnd {
int32 x = 1;
int32 y = 2;
}
message TouchEvent {
oneof union {
TouchScaleUpdate scale_update = 1;
TouchPanStart pan_start = 2;
TouchPanUpdate pan_update = 3;
TouchPanEnd pan_end = 4;
}
}
@@ -377,6 +397,7 @@ message FileTransferReceiveRequest {
string path = 2; // path written to
repeated FileEntry files = 3;
int32 file_num = 4;
uint64 total_size = 5;
}
message FileRemoveDir {
@@ -603,6 +624,8 @@ message BackNotification {
PrivacyModeState privacy_mode_state = 1;
BlockInputState block_input_state = 2;
}
// Supplementary message, for "PrvOnFailed" and "PrvOffFailed"
string details = 3;
}
message ElevationRequestWithLogon {

View File

@@ -214,7 +214,7 @@ pub struct Resolution {
pub h: i32,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PeerConfig {
#[serde(default, deserialize_with = "deserialize_vec_u8")]
pub password: Vec<u8>,
@@ -230,6 +230,7 @@ pub struct PeerConfig {
skip_serializing_if = "String::is_empty"
)]
pub view_style: String,
// Image scroll style, scrollbar or scroll auto
#[serde(
default = "PeerConfig::default_scroll_style",
deserialize_with = "PeerConfig::deserialize_scroll_style",
@@ -276,6 +277,13 @@ pub struct PeerConfig {
pub keyboard_mode: String,
#[serde(flatten)]
pub view_only: ViewOnly,
// Mouse wheel or touchpad scroll mode
#[serde(
default = "PeerConfig::default_reverse_mouse_wheel",
deserialize_with = "PeerConfig::deserialize_reverse_mouse_wheel",
skip_serializing_if = "String::is_empty"
)]
pub reverse_mouse_wheel: String,
#[serde(
default,
@@ -296,6 +304,39 @@ pub struct PeerConfig {
pub transfer: TransferSerde,
}
impl Default for PeerConfig {
fn default() -> Self {
Self {
password: Default::default(),
size: Default::default(),
size_ft: Default::default(),
size_pf: Default::default(),
view_style: Self::default_view_style(),
scroll_style: Self::default_scroll_style(),
image_quality: Self::default_image_quality(),
custom_image_quality: Self::default_custom_image_quality(),
show_remote_cursor: Default::default(),
lock_after_session_end: Default::default(),
privacy_mode: Default::default(),
allow_swap_key: Default::default(),
port_forwards: Default::default(),
direct_failures: Default::default(),
disable_audio: Default::default(),
disable_clipboard: Default::default(),
enable_file_transfer: Default::default(),
show_quality_monitor: Default::default(),
keyboard_mode: Default::default(),
view_only: Default::default(),
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
custom_resolutions: Default::default(),
options: Self::default_options(),
ui_flutter: Default::default(),
info: Default::default(),
transfer: Default::default(),
}
}
}
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
pub struct PeerInfoSerde {
#[serde(default, deserialize_with = "deserialize_string")]
@@ -1098,6 +1139,11 @@ impl PeerConfig {
deserialize_image_quality,
UserDefaultConfig::read().get("image_quality")
);
serde_field_string!(
default_reverse_mouse_wheel,
deserialize_reverse_mouse_wheel,
UserDefaultConfig::read().get("reverse_mouse_wheel")
);
fn default_custom_image_quality() -> Vec<i32> {
let f: f64 = UserDefaultConfig::read()
@@ -1124,6 +1170,17 @@ impl PeerConfig {
D: de::Deserializer<'de>,
{
let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?;
Self::insert_default_options(&mut mp);
Ok(mp)
}
fn default_options() -> HashMap<String, String> {
let mut mp: HashMap<String, String> = Default::default();
Self::insert_default_options(&mut mp);
return mp;
}
fn insert_default_options(mp: &mut HashMap<String, String>) {
let mut key = "codec-preference";
if !mp.contains_key(key) {
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
@@ -1136,7 +1193,10 @@ impl PeerConfig {
if !mp.contains_key(key) {
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
}
Ok(mp)
key = "touch-mode";
if !mp.contains_key(key) {
mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
}
}
}
@@ -1437,7 +1497,11 @@ impl UserDefaultConfig {
}
pub fn set(&mut self, key: String, value: String) {
self.options.insert(key, value);
if value.is_empty() {
self.options.remove(&key);
} else {
self.options.insert(key, value);
}
self.store();
}
@@ -1525,6 +1589,12 @@ pub struct Ab {
pub peers: Vec<AbPeer>,
#[serde(default, deserialize_with = "deserialize_vec_string")]
pub tags: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub tag_colors: String,
}
impl Ab {
@@ -1580,6 +1650,106 @@ macro_rules! deserialize_default {
};
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct GroupPeer {
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub id: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub username: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub hostname: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub platform: String,
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub login_name: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct GroupUser {
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub name: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Group {
#[serde(
default,
deserialize_with = "deserialize_string",
skip_serializing_if = "String::is_empty"
)]
pub access_token: String,
#[serde(default, deserialize_with = "deserialize_vec_groupuser")]
pub users: Vec<GroupUser>,
#[serde(default, deserialize_with = "deserialize_vec_grouppeer")]
pub peers: Vec<GroupPeer>,
}
impl Group {
fn path() -> PathBuf {
let filename = format!("{}_group", APP_NAME.read().unwrap().clone());
Config::path(filename)
}
pub fn store(json: String) {
if let Ok(mut file) = std::fs::File::create(Self::path()) {
let data = compress(json.as_bytes());
let max_len = 64 * 1024 * 1024;
if data.len() > max_len {
// maxlen of function decompress
return;
}
if let Ok(data) = symmetric_crypt(&data, true) {
file.write_all(&data).ok();
}
};
}
pub fn load() -> Self {
if let Ok(mut file) = std::fs::File::open(Self::path()) {
let mut data = vec![];
if file.read_to_end(&mut data).is_ok() {
if let Ok(data) = symmetric_crypt(&data, false) {
let data = decompress(&data);
if let Ok(group) = serde_json::from_str::<Self>(&String::from_utf8_lossy(&data))
{
return group;
}
}
}
};
Self::remove();
Self::default()
}
pub fn remove() {
std::fs::remove_file(Self::path()).ok();
}
}
deserialize_default!(deserialize_string, String);
deserialize_default!(deserialize_bool, bool);
deserialize_default!(deserialize_i32, i32);
@@ -1588,6 +1758,8 @@ deserialize_default!(deserialize_vec_string, Vec<String>);
deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>);
deserialize_default!(deserialize_vec_discoverypeer, Vec<DiscoveryPeer>);
deserialize_default!(deserialize_vec_abpeer, Vec<AbPeer>);
deserialize_default!(deserialize_vec_groupuser, Vec<GroupUser>);
deserialize_default!(deserialize_vec_grouppeer, Vec<GroupPeer>);
deserialize_default!(deserialize_keypair, KeyPair);
deserialize_default!(deserialize_size, Size);
deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>);

View File

@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use tokio::{fs::File, io::*};
use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
@@ -194,7 +195,8 @@ pub fn can_enable_overwrite_detection(version: i64) -> bool {
version >= get_version_number("1.1.10")
}
#[derive(Default)]
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransferJob {
pub id: i32,
pub remote: String,
@@ -203,10 +205,13 @@ pub struct TransferJob {
pub is_remote: bool,
pub is_last_job: bool,
pub file_num: i32,
#[serde(skip_serializing)]
pub files: Vec<FileEntry>,
pub conn_id: i32, // server only
#[serde(skip_serializing)]
file: Option<File>,
total_size: u64,
pub total_size: u64,
finished_size: u64,
transferred: u64,
enable_overwrite_detection: bool,
@@ -695,13 +700,20 @@ pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
}
#[inline]
pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message {
pub fn new_receive(
id: i32,
path: String,
file_num: i32,
files: Vec<FileEntry>,
total_size: u64,
) -> Message {
let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest {
id,
path,
files,
file_num,
total_size,
..Default::default()
});
let mut msg_out = Message::new();
@@ -748,10 +760,16 @@ pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
jobs.iter_mut().find(|x| x.id() == id)
}
#[inline]
pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
jobs.iter().find(|x| x.id() == id)
}
pub async fn handle_read_jobs(
jobs: &mut Vec<TransferJob>,
stream: &mut crate::Stream,
) -> ResultType<()> {
) -> ResultType<String> {
let mut job_log = Default::default();
let mut finished = Vec::new();
for job in jobs.iter_mut() {
if job.is_last_job {
@@ -768,9 +786,11 @@ pub async fn handle_read_jobs(
}
Ok(None) => {
if job.job_completed() {
job_log = serialize_transfer_job(job, true, false, "");
finished.push(job.id());
match job.job_error() {
Some(err) => {
job_log = serialize_transfer_job(job, false, false, &err);
stream
.send(&new_error(job.id(), err, job.file_num()))
.await?
@@ -786,7 +806,7 @@ pub async fn handle_read_jobs(
for id in finished {
remove_job(id, jobs);
}
Ok(())
Ok(job_log)
}
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
@@ -861,3 +881,20 @@ pub fn is_write_need_confirmation(
Ok(DigestCheckResult::NoSuchFile)
}
}
pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
let mut v = vec![];
for job in jobs {
let value = serde_json::to_value(job).unwrap_or_default();
v.push(value);
}
serde_json::to_string(&v).unwrap_or_default()
}
pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
let mut value = serde_json::to_value(job).unwrap_or_default();
value["done"] = json!(done);
value["cancel"] = json!(cancel);
value["error"] = json!(error);
serde_json::to_string(&value).unwrap_or_default()
}

View File

@@ -103,15 +103,16 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String
// String: password
// bool: whether decryption is successful
// bool: whether should store to re-encrypt when load
// note: s.len() return length in bytes, s.chars().count() return char count
// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars
pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
if s.len() > VERSION_LEN {
let version = &s[..VERSION_LEN];
if version == "00" {
if s.starts_with("00") {
if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
return (
String::from_utf8_lossy(&v).to_string(),
true,
version != current_version,
"00" != current_version,
);
}
}
@@ -198,7 +199,7 @@ mod test {
let max_len = 128;
println!("test str");
let data = "Hello World";
let data = "1ü1111";
let encrypted = encrypt_str_or_original(data, version, max_len);
let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
println!("data: {data}");
@@ -217,7 +218,7 @@ mod test {
);
println!("test vec");
let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6];
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
let encrypted = encrypt_vec_or_original(&data, version, max_len);
let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
println!("data: {data:?}");
@@ -253,6 +254,10 @@ mod test {
let (_, succ, store) = decrypt_vec_or_original(&[], version);
assert!(!store);
assert!(!succ);
let data = "1ü1111";
assert_eq!(decrypt_str_or_original(data, version).0, data);
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
assert_eq!(decrypt_vec_or_original(&data, version).0, data);
println!("test speed");
let test_speed = |len: usize, name: &str| {

View File

@@ -183,6 +183,15 @@ pub fn is_active(sid: &str) -> bool {
}
}
pub fn is_active_and_seat0(sid: &str) -> bool {
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
String::from_utf8_lossy(&output.stdout).contains("State=active")
&& String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
} else {
false
}
}
pub fn run_cmds(cmds: &str) -> ResultType<String> {
let output = std::process::Command::new("sh")
.args(vec!["-c", cmds])

View File

@@ -1,5 +1,10 @@
extern crate embed_resource;
use std::fs;
fn main() {
embed_resource::compile("icon.rc", embed_resource::NONE);
let runner_res_path = "Runner.res";
match fs::metadata(runner_res_path) {
Ok(_) => println!("cargo:rustc-link-lib=dylib:+verbatim=./libs/portable/Runner.res"),
Err(_) => embed_resource::compile("icon.rc", embed_resource::NONE),
}
}

View File

@@ -154,17 +154,18 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init(
}
}
pub fn call_main_service_mouse_input(mask: i32, x: i32, y: i32) -> JniResult<()> {
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let kind = env.new_string(kind)?;
env.call_method(
ctx,
"rustMouseInput",
"(III)V",
&[JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
"rustPointerInput",
"(Ljava/lang/String;III)V",
&[JValue::Object(&JObject::from(kind)), JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
)?;
return Ok(());
} else {

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