Compare commits

..

213 Commits
ios ... 1.4.3

Author SHA1 Message Date
fufesou
6a0da9cf09 fix: custom scale, dpi (#13197)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-18 00:35:43 +08:00
rustdesk
d55974c352 --terminal command line 2025-10-17 19:22:46 +08:00
Gyuris Gellért
a898c22f4b Translation: Review and update hu.rs (#13169)
Some translation conventions, linguistic correctness, and spelling review.
2025-10-17 16:10:23 +08:00
solokot
b82e8bedfc Update ru.rs (#13168) 2025-10-17 16:09:55 +08:00
VenusGirl❤
7453cefd94 Update ko.rs (#13152)
Update Korean
2025-10-17 16:09:30 +08:00
Lynilia
1ed6b958cb Update fr.rs (#13151) 2025-10-17 16:09:16 +08:00
Alex Rijckaert
57896ab176 Update nl.rs (#13150) 2025-10-17 16:09:02 +08:00
Mr-Update
5c370b3914 Update de.rs (#13149)
* Update de.rs

* Update de.rs
2025-10-17 16:08:37 +08:00
rustdesk
182e35adc7 1.4.3 2025-10-17 13:58:08 +08:00
fufesou
d0a360fd80 refact: option, touch mode, move to local (#13055)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-17 13:36:46 +08:00
21pages
2fbc0625de fix macos low fps after installation (#13185)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-16 17:27:23 +08:00
fufesou
d3d20a4e20 fix: Wayland, cpu 100, workaround (#13179)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-15 21:59:48 +08:00
21pages
2c088d3504 fix can't run from cmd on win7 (#13160)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-14 12:11:05 +08:00
bovirus
6f9728f2d4 Italian language update (#13148) 2025-10-13 20:43:07 +08:00
21pages
30552fd202 show peer note (#13140)
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-10-12 14:59:42 +08:00
Mahdi Rahimi
9826c4e943 Update Arabic translation in ar.rs (#13144) 2025-10-12 14:55:51 +08:00
Mahdi Rahimi
bb9445bd0f Updated Persian translations in fa.rs (#13143) 2025-10-12 14:55:37 +08:00
Mr-Update
1f7e66f4cb Update de.rs (#13138) 2025-10-12 14:55:23 +08:00
bovirus
2a34e918a0 Italian language update (#13136) 2025-10-12 14:55:01 +08:00
VenusGirl❤
21c0d924ab Update ko.rs (#13134) 2025-10-12 14:54:40 +08:00
Lynilia
c8d5ee6565 Update fr.rs (#13132) 2025-10-12 14:51:37 +08:00
fufesou
3d8fc7ca7b fix: uninstall, idd (#13142)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-12 09:14:21 +08:00
Jonathan Gilbert
246b5b93f8 Centralize debounce of save window pos and save window pos on close (#12987)
* Added method equals to class LastWindowPosition to compare the contents of instances.
Added storage to common.dart for remembering what window position data has previously been written.
Factored the actual save code from saveWindowPosition to _saveWindowPositionActual and updated saveWindowPosition to call it through a debouncer, and only if the window position data has actually changed since the last call in the same instance.
Added named parameter 'flush' to saveWindowPosition in common.dart, and to _saveFrame in tabbar_widget.dart, and updated the onWindowClosed handler in tabbar_widget.dart to call _saveFrame with flush: true, forcing an immediate save on close.
Removed the _saveFrame debouncer from tabbar_widget.dart.

* saveWindowPosition: don't reschedule debounce if it's already in flight

* Reworked the logic in saveWindowPosition to collapse a rapid series of updates into one save at the end.
2025-10-11 16:11:56 +08:00
dependabot[bot]
2183c0980b Git submodule: Bump libs/hbb_common from 7ea8686 to 5ed0afd (#13122)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `7ea8686` to `5ed0afd`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](7ea868612d...5ed0afde08)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 5ed0afde0841659e2fb37ae7acaddc005fa1a8d3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-10 17:44:59 +08:00
rustdesk
4ae301710d upload x86 windows 2025-10-10 00:23:48 +08:00
Re*Index. (ot_inc)
5f9390c210 Update Japanese Language (#13123) 2025-10-09 17:51:03 +08:00
fufesou
0f3a03aab7 feat: mobile, virtual mouse (#12911)
* feat: mobile, virtual mouse

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

* feat: mobile, virtual mouse, mouse mode

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

* refact: mobile, virtual mouse, mouse mode

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

* feat: mobile, virtual mouse mode

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

* feat: mobile virtual mouse, options

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-10-09 08:23:55 +08:00
summoner
02f455b0cc Translation: Update hu.rs (#13115)
Translate new strings
2025-10-09 08:21:44 +08:00
bovirus
ffddf60184 Update Italian language (#13117) 2025-10-09 08:21:30 +08:00
Alessandro De Blasis
482840b8bb feat(ui): custom scale mode with inline controls and live apply (#13045)
* feat(ui): custom scale mode with inline controls and live apply

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/models/model.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(dialog): remove unused showCustomScaleDialog function

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* feat(ui): enhance custom scale controls with live updates and improved UI

- Introduced a reactive custom scale percentage using RxInt.
- Added initialization of custom scale from stored options after the widget builds.
- Updated viewStyle method to conditionally display custom controls based on selection.
- Implemented a debouncer for smoother scale adjustments.
- Enhanced slider UI with custom thumb shape and improved button interactions.

This update improves user experience by allowing real-time adjustments to the custom scale settings.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(remote_toolbar): improve widget lifecycle management and enhance slider dimensions

- Moved initialization of custom scale percentage to initState for better lifecycle handling.
- Updated slider thumb dimensions and layout for improved UI consistency.
- Added dispose method to clean up resources in custom scale controls.

These changes enhance the overall performance and user experience of the remote toolbar.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* feat(remote_toolbar): enhance scroll behavior and improve slider thumb rendering

- Introduced a new state variable to manage scroll enablement based on canvas model changes.
- Updated the return value of the viewStyle method to include the scroll enablement status.
- Refactored the slider thumb shape for better performance and visual consistency.
- Improved the initialization of image overflow detection in the CanvasModel.

These changes enhance the user experience by providing dynamic scroll control and a more responsive UI.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(scale): introduce utility functions for custom scale management for DRY

- Added a new file `scale.dart` containing utility functions to clamp, parse, and compute custom scale percentages.
- Refactored the `CanvasModel` and `_DisplayMenuState` to utilize the new utility functions for fetching and applying custom scale settings.
- Improved code readability and maintainability by centralizing scale-related logic.

These changes enhance the handling of custom scale settings across the application.

Signed-off-by: Alessandro De Blasis alex@deblasis.net
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/utils/scale.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/models/model.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/models/model.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: Remove unused import of 'uuid' in scale.dart

Signed-off-by: Alessandro De Blasis alex@deblasis.net
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* feat(remote_toolbar): implement nonlinear mapping for custom scale slider

- Added piecewise mapping functions to convert normalized slider positions to custom scale percentages and vice versa.
- Introduced snapping behavior for the slider to enhance user experience.
- Updated the slider's minimum and maximum values to align with the new mapping logic.
- Adjusted the clamping function to ensure the minimum percentage is 10.

These changes improve the precision and usability of the custom scale slider in the remote toolbar.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* fix(scale): update minimum scale percentage to 5

- Adjusted the minimum scale percentage in both the remote toolbar and the clamping function to improve consistency and usability.
- This change aligns the clamping logic with the updated minimum value for the custom scale slider.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor(scale): centralize custom scale constants in consts.dart

- Moved piecewise mapping constants for the custom scale slider from the remote toolbar to consts.dart for better organization and maintainability.
- Introduced additional constants related to custom scale behavior, including minimum, pivot, and maximum percentages, as well as debounce duration.
- Updated the remote toolbar to reference these centralized constants, improving code clarity and reducing duplication.

These changes enhance the structure and readability of the custom scale implementation.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(consts): remove duplicate custom scale percent key definition

- Eliminated redundant declaration of the custom scale percent key in consts.dart, ensuring a single source of truth for this constant.
- This change improves code clarity and maintainability by reducing duplication.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(scale): update clamping logic to use centralized constants

- Modified the clamping function to utilize the newly defined constants for minimum and maximum scale percentages, enhancing code maintainability and clarity.
- This change ensures consistency across the application by referencing a single source for scale limits.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Enhance RdoMenuButton behavior for custom scale selection

- Updated the RdoMenuButton to include a new `closeOnActivate` parameter, allowing the submenu to remain open when selecting custom scale options.
- Modified the onChanged callback to conditionally trigger a rebuild when entering custom mode, improving user experience by immediately displaying the slider controls.

These changes streamline the interaction with the custom scale feature in the remote toolbar.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(toolbar):  _DisplayMenuState to simplify scroll handling

- Removed the _scrollEnabled state variable and its associated logic, streamlining the component's state management.
- Updated the RdoMenuButton onChanged callbacks to directly reference the canvasModel's imageOverflow value, enhancing responsiveness and reducing complexity.

These changes improve code clarity and maintainability in the remote toolbar's display menu.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* feat(lang): Add translations for custom scale features in multiple languages

- Introduced new entries for "Scale custom", "Custom scale slider", "Decrease", and "Increase" in various language files to support the custom scale functionality.
- This update enhances the localization of the application, ensuring users can interact with the custom scale features in their preferred language.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* feat(lang): Add translations for custom scale features in Catalan and Romanian

- Updated language files for Catalan and Romanian to include translations for "Custom scale slider", "Decrease", and "Increase".
- This enhancement improves the localization of the application, allowing users to interact with custom scale features in their native languages.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* fix(model): Correct error logging in getSessionCustomScale method

- Updated the error logging statement in the getSessionCustomScale method to properly interpolate the exception message, improving debugging clarity.
- This change ensures that error messages are more informative, aiding in troubleshooting issues related to session scaling.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(scale): Simplify clamping logic for custom scale percent

- Updated the clampCustomScalePercent function to use the built-in clamp method, improving code readability and maintainability.
- This change ensures consistent clamping behavior across the application by centralizing the logic for valid scale ranges.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(scale): Remove unused import for web bridge

- Eliminated the conditional import of the web bridge from scale.dart, as it is no longer necessary. This change helps to clean up the code and improve maintainability by removing unused dependencies.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* chore(model): typo

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore(toolbar): Clarify precision for scale adjustments in remote toolbar

- Added comments to clarify the use of a wide range of divisions for the scale slider, allowing for ~1% precision increments. This change improves user experience by enabling more precise scale value settings, reducing the need for fine-tuning with +/- buttons.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/models/model.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(model): Enhance error logging in getSessionCustomScale method

- Improved error logging by adding stack trace output to debugPrintStack, enhancing debugging capabilities for session scaling issues.
- This change provides clearer insights into errors encountered during scale retrieval, aiding in troubleshooting.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* refactor(toolbar): Simplify custom scale percent retrieval in remote toolbar

- Replaced the previous method of retrieving the custom scale percent with a new function, getSessionCustomScalePercent, enhancing code clarity and maintainability.
- This change streamlines the process of obtaining the scale value, ensuring a more efficient and readable implementation.

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>

* Update flutter/lib/desktop/widgets/remote_toolbar.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
Signed-off-by: Alessandro De Blasis alex@deblasis.net
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-10-08 14:40:20 +08:00
flusheDData
a3637cf2b6 Update es.rs (#13104)
New terms added
2025-10-07 23:31:08 +08:00
fufesou
48669cdb34 fix: alarm audit number, ipv6 prefix attempts (#13097)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-06 22:10:54 +08:00
Michael Bacarella
a953845ba7 feat: Add IPv6 prefix-based rate limiting on login failures (#13070)
Enhance security by implementing rate limiting on IPv6 prefixes (/64, /56, /48)
to prevent brute force attacks that exploit cheap IPv6 address generation.

* Add private get_ipv6_prefixes() to calculate network prefixes
* Implement private check_failure_ipv6_prefix() for prefix-specific limits
  on IPv6 addresses
* Refactor check_failure() and update_failure() to support both IPs and prefixes
* Add ExceedIPv6PrefixAttempts to AlarmAuditType enum

Signed-off-by: Michael Bacarella <m@bacarella.com>
2025-10-05 23:43:29 +08:00
summoner
8d71534839 Translation: Update hu.rs (#13089)
Translate new strings
Fix translation
2025-10-03 22:41:53 +08:00
loako
d110118961 fix: Update Swedish translations that were missing (#13081) 2025-10-02 20:33:20 +08:00
Ibnul Mutaki
fa1ed2bc0c fix: Update Indonesian translations for consistency and clarity (#13077) 2025-10-01 22:59:00 +08:00
ysr9029
3f28978dad fix: Correct Japanese translations and typos in lang file (#13029) 2025-09-30 17:42:49 +08:00
dependabot[bot]
02cd121465 Git submodule: Bump libs/hbb_common from 1df14d9 to 7ea8686 (#13062)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `1df14d9` to `7ea8686`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](1df14d90c9...7ea868612d)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 7ea868612dfee7954facb9a7857d65ef875076eb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:37:54 +08:00
21pages
5481c300b2 more assign from cli and devices.py (#13050)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-27 16:55:08 +08:00
Berk Efe Keskin
7b75257a4a Fixed translation errors on README-TR.md (#12976) 2025-09-26 15:51:36 +08:00
Alt
c02e5cad73 refactor: update lang id.rs (#13026) 2025-09-25 23:30:51 +08:00
fufesou
dee03c0f9f fix: Center the main window on first run. (#13003)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-24 14:47:29 +08:00
21pages
d1159764f6 add ab.py and audits.py (#12989)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-23 17:13:13 +08:00
Nathan Saslavsky
eacb07988d Add Wayland multi-monitor screen capture functionality (#12900)
* Add Wayland multi-monitor screen capture functionality

* fix wayland capture issues by reverting to CapturerPtr, the problem was that calling Display::all in get_capturer_for_display was dropping the pipewire capturer and causing the video to freeze.

* If running as AppImage or flatpak, ignore the 'multiple' argument

* Comment out warning log with unclear purpose Comment out warning log with unclear purpose

---------

Co-authored-by: fufesou <13586388+fufesou@users.noreply.github.com>
2025-09-22 21:53:14 +08:00
21pages
a375766ac2 disable iconv on android (#13001)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-22 21:49:58 +08:00
21pages
9b9276e752 fix crash on android armv7 (#12997)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-22 17:02:53 +08:00
Jonathan Gilbert
753a2ab2b7 Fixed super call in onWindowResized in tabbar_widget.dart. (#12979) 2025-09-22 11:26:19 +08:00
rustdesk
0cef5f79ee remove can't save option 2025-09-20 14:03:48 +08:00
fufesou
b11a8dfe54 fix: build (#12968)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-19 17:20:53 +08:00
Jonathan Gilbert
2d1c94f1ef Fix window positioning on Windows when the taskbar is on the top or left (#12933)
* Added win32_desktop.cpp/.h defining a method Win32Desktop::GetWorkArea.
Added code to wWinMain in main.cpp to position the window relative to the work area, which may not be at (0, 0) depending on the user's configuration.

* Corrected the constraint on the size value calculated by main.cpp.

* Fixed references to min to use std::min.

* Reworked GetWorkArea in win32_desktop.cpp to treat the supplied origin and size as containing an existing window rectangle, and to find the monitor that contains or is closest to that window.
Added function FitToWorkArea to win32_desktop.cpp/.h.
Updated main.cpp to use Win32Desktop::FitToWorkArea instead of explicitly constraining the size.
2025-09-19 16:11:26 +08:00
luzpaz
e14e850e10 fix: typos in src/ and subdirectories (#11727)
Found via codespell
2025-09-17 13:37:44 +08:00
21pages
3176391693 fix websocket reconnect (#12903)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-15 14:31:57 +08:00
VenusGirl❤
5277300943 Update README-KR.md (#12899) 2025-09-12 15:59:39 +08:00
VenusGirl❤
878e1ff290 Update README-KR.md (#12874) 2025-09-10 12:44:21 +08:00
fufesou
8d453010a4 fix: port forward, invalid msg (#12881)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-09 21:20:58 +08:00
Daniel
e2f6030590 Create CODE_OF_CONDUCT-DE.md (#12414)
Create a German Version of the CoC
2025-09-09 14:27:34 +08:00
VenusGirl❤
bf3f8706f8 Add CODE_OF_CONDUCT-KR.md (#12330) 2025-09-08 17:35:45 +08:00
21pages
5c9b4abab2 default shared password (#12868)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-07 16:47:35 +08:00
Kleofass
9fb4862a45 Update lv.rs (#12863) 2025-09-07 16:07:38 +08:00
Alex Rijckaert
65df6897a6 Update nl.rs (#12815) 2025-09-07 16:07:21 +08:00
XLion
529810f2f4 Update tw.rs (#12814) 2025-09-07 16:07:10 +08:00
fufesou
df0ff4f134 feat: cursor, linux, Xwayland (#12859)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-06 20:35:51 +08:00
fufesou
6c949a9602 feat: cursor, linux (#12822)
* feat: cursor, linux

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

* refact: cursor, text, white background

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-06 12:11:43 +08:00
Mahdi Rahimi
f933f46283 Updated Persian translations in fa.rs (#12802) 2025-09-06 12:09:58 +08:00
21pages
4080907d2b android mediacodec encode align 64 (#12852)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-06 12:09:21 +08:00
Mr-Update
ed5cd21cb6 Update de.rs (#12783) 2025-09-05 16:49:54 +08:00
solokot
aa8278e1d5 Update ru.rs (#12778) 2025-09-05 16:49:39 +08:00
fufesou
0f526fce6c refact: http, rust side, log errror (#12820)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-04 15:04:53 +08:00
IwantHappiness
15d471e520 Remove needless macros format! (#11456) 2025-09-03 22:17:11 +08:00
Mahdi Rahimi
c47e94813d Update Arabic translation in ar.rs (#12773) 2025-09-02 23:00:46 +08:00
rustdesk
c979cbcac7 disable-discovery-pane 2025-09-01 17:07:29 +08:00
21pages
6b2a1dfd84 update vcpkg, aom, vpx (#12795)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-01 15:35:27 +08:00
fufesou
7948d3144a fix: cursor, macos, text (#12794)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-01 15:34:48 +08:00
fufesou
d499098c4f Fix/cursor macos multi displays (#12791)
* fix: cursor, whiteboard, pos

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

* fix: whiteboard, macos, multi displays

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-09-01 13:02:06 +08:00
21pages
42be442385 fix ci (#12789)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-09-01 12:50:38 +08:00
fufesou
e2ec6a5be8 feat: whiteboard, macos (#12780)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-30 22:16:35 +08:00
bovirus
438cef8cf9 Italian language update (#12762) 2025-08-30 12:08:56 +08:00
Lynilia
7bacf7cdc9 Update fr.rs (#12758) 2025-08-30 12:08:44 +08:00
VenusGirl❤
c5e76972aa Update ko.rs (#12757)
Update Korean
2025-08-29 17:10:04 +08:00
fufesou
7ca8e0d437 refact: show my cursor (#12765)
1. Show not supported on Win7.
2. Enabling "Show my cursor" automatically enables "View mode".

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-29 01:06:37 +08:00
fufesou
a98852e279 fix: mouse event, is in current window (#12760)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-29 01:06:05 +08:00
fufesou
d0e9c6dc57 feat: show my cursor (#12745)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-28 15:20:01 +08:00
fufesou
ac70f380a6 fix: file transfer, resume, path and finished size (#12739)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-26 17:59:39 +08:00
Dmitry Beskov
34cf9d6181 Enhance .desktop File with New Keywords for Improved App Discoverability (#12599)
* linux keywords in a desktop entry

* Update rustdesk.desktop

* Update rustdesk.desktop
2025-08-26 15:31:31 +08:00
fufesou
db4296533a feat: advanced option, main window, always on top (#12731)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-26 00:15:55 +08:00
fufesou
6381f43f01 feat: clipboard files, audit (#12730)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-25 22:29:53 +08:00
fufesou
f4fb31d7a1 feat: file transfer, resume (#12626)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-25 14:34:03 +08:00
Luke Bermingham
9e22f9639a Fix audio delay: added pulse audio and pipewire configuration for RustDesk service in Linux (#12724) 2025-08-25 14:33:37 +08:00
Mahdi Rahimi
9b854d3034 Update Arabic translation in ar.rs (#12714) 2025-08-24 14:16:10 +08:00
Re*Index. (ot_inc)
2c88a44a53 Update & Fix Japanese translate. (#12702) 2025-08-23 22:47:02 +08:00
RustDesk
0c2b86c8e7 Revert "Create Hi.rs (#12482)" (#12700)
This reverts commit 74752bbd2f.
2025-08-21 12:31:11 +08:00
Leo Louis
74752bbd2f Create Hi.rs (#12482)
* Create Hi.rs

Added hindi translation file

* Create Gu.rs

Added Gujarati translation file

* Create Ml.rs

Added Malayalam translation file

* Update lang.rs

* Rename Gu.rs to gu.rs

* Rename Ml.rs to ml.rs

changed name to correct format

* Rename Hi.rs to hi.rs

changed name to correct format
2025-08-21 12:20:48 +08:00
Mahdi Rahimi
ad396b4155 Updated Persian translations in fa.rs (#12697) 2025-08-21 12:20:05 +08:00
21pages
5ff1740b5b set allowMalformed to true when decode utf8 (#12693)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-20 14:55:52 +08:00
bovirus
e0ab3f0c92 Italian language update (#12679) 2025-08-20 14:17:02 +08:00
BigRetroMike
9b77e91d79 Update pl.rs (#12618)
Added missing translation and small correction
2025-08-19 12:14:19 +08:00
rustdesk
d187121645 simply remove it in case password log 2025-08-19 00:23:17 +08:00
fufesou
a22f2108c6 refact: suppress warns on macos (#12449)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-18 15:09:11 +08:00
rustdesk
bf24869c6a fix bundle id 2025-08-17 15:37:12 +08:00
Alex Rijckaert
4e9a370ff6 Update nl.rs (#12617)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-08-17 15:29:17 +08:00
rustdesk
1aed6f3c2e compile warn 2025-08-17 10:08:12 +08:00
rustdesk
6367d50d76 fix myself 2025-08-17 10:04:40 +08:00
John Fowler
f33ed27419 Update hu.rs (#12610)
Add and translate a new string.
2025-08-16 12:09:15 +08:00
DeDuplicate
870c8cb158 Update he.rs (#12601) 2025-08-15 15:00:30 +08:00
fufesou
0b9d7925b5 fix: ios, file transfer, home dir (#12657)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-15 00:00:05 +08:00
rustdesk
16b625f8b4 fix ci 2025-08-14 18:58:26 +08:00
rustdesk
16d301a783 try AssociatedBundleIdentifiers per https://developer.apple.com/documentation/servicemanagement/updating-helper-executables-from-earlier-versions-of-macos#Respond-to-changes-in-System-Settings 2025-08-14 18:47:52 +08:00
Mr-Update
212bbaf44c Update de.rs (#12600) 2025-08-14 18:08:39 +08:00
rustdesk
1d6037003a new badge 2025-08-13 19:24:00 +08:00
Mahdi Rahimi
6f4b23b40b Updated Persian translations in fa.rs (#12589) 2025-08-13 12:26:27 +08:00
Mahdi Rahimi
4e82766ba4 Update Arabic translation in ar.rs (#12588) 2025-08-13 12:26:17 +08:00
Lynilia
dc86db5206 Update fr.rs (#12582) 2025-08-13 12:25:52 +08:00
solokot
5a75ea723b Update ru.rs (#12594) 2025-08-13 12:25:30 +08:00
Alex Rijckaert
d59f216c26 Update nl.rs (#12592) 2025-08-13 12:25:19 +08:00
VenusGirl❤
160edcc1cd Update ko.rs (#12590)
* Update ko.rs

* Update ko.rs

Update Korean
2025-08-13 12:25:09 +08:00
21pages
59d597de8a show direct connection for IPv6 via RelayResponse (#12634)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-13 10:08:23 +08:00
21pages
806351b6c1 fix remote tab tooltip (#12632)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-12 20:29:48 +08:00
21pages
e7909a0dbd opt update of direct/direct_failures (#12627)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-12 17:48:20 +08:00
rustdesk
d6d44be1b7 temporrarily revert file transfer resume 2025-08-11 23:28:19 +08:00
RustDesk
53efaf125c Revert "Feat: file transfer, resume (#12557)" (#12620)
This reverts commit 43ec57c769.
2025-08-11 23:25:41 +08:00
21pages
1fb0123ed7 remove skip udp punch if udp nat port is 0 (#12615)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-11 20:41:46 +08:00
21pages
a0659a277a show TCP/UDP/IPv6 in tooltip (#12613)
* add punch type log

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

* show TCP/UDP/IPv6 in tooltip

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

* Skip udp punch if udp nat port is 0

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-11 16:13:31 +08:00
rustdesk
77064cc2f8 fix ci 2025-08-10 17:50:25 +08:00
RustDesk
1954790808 try tcp and udp both 2025-08-10 17:44:36 +08:00
rustdesk
4263643200 macos-14 for arm 2025-08-10 00:04:00 +08:00
fufesou
43ec57c769 Feat: file transfer, resume (#12557)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-09 23:47:19 +08:00
rustdesk
302dad2016 update hbb_common 2025-08-09 23:46:51 +08:00
rustdesk
fdb8b498cb all use macos-13 2025-08-09 23:27:56 +08:00
rustdesk
f6af59b044 remove useless selfhost job 2025-08-09 23:26:33 +08:00
fufesou
ad1ed132d1 fix: file transfer, web (#12565)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-09 15:54:00 +08:00
rustdesk
466d456760 fix https://github.com/rustdesk/rustdesk/issues/12587 2025-08-09 10:25:21 +08:00
fufesou
6bc3b38b56 refact: macos, update, preparing for installation (#12581)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-08-08 14:25:22 +08:00
fufesou
39b91911cb fix: update macos (#12578)
* fix: update macos

1. Use `ditto` instead of `cp -r`.
2. Add prompt for extracting dmg.

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

* fix: error to err

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

* Refact: Remove "Extracting"

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-08-07 23:31:31 +08:00
SALİH ÖZKARA
e85989e9d9 Fix Turkish localization (#12555) 2025-08-07 20:16:14 +08:00
Alex Rijckaert
e7f672899b Update nl.rs (#12525) 2025-08-07 20:15:59 +08:00
VenusGirl❤
9538eba64e Update ko.rs (#12523)
Because it is button-shaped, even a short phrase such as “upgrade” can convey meaning in Korean.
2025-08-07 20:15:47 +08:00
rustdesk
b37b271fce add team to osx 2025-08-07 18:09:09 +08:00
21pages
77be752ff1 sciter hide cm (#12570)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-08-07 13:29:21 +08:00
Andrzej Rudnik
725a47268e Updated Polish translation (#12521)
* Update pl.rs

* Update pl.rs
2025-08-06 23:16:35 +08:00
asereze
2ba215a6d7 Update sc.rs (#12517) 2025-08-06 01:45:24 +08:00
H3XÐΛΞMѲИ
6533a1b98d i18n(tw): Fix translations and address inconsistencies (#12490) 2025-08-04 17:48:17 +08:00
tschettervictor
1f2f5a41d4 typo: openbad > openbsd (#12484) 2025-08-03 16:00:52 +08:00
VenusGirl❤
4e7680e322 Update ko.rs (#12480)
* Update ko.rs

* Update ko.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-08-02 12:05:19 +08:00
Mahdi Rahimi
f32591c3d1 Update Arabic translation in ar.rs (#12451) 2025-08-01 17:18:49 +08:00
fufesou
6ec217263d fix: nokhwa, win, infinite loop (#12489)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-31 16:58:00 +08:00
fufesou
8899b90725 fix: build (#12483)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-31 00:27:55 +08:00
rustdesk
7ece7e730a fix https://github.com/rustdesk/rustdesk/issues/12481 2025-07-30 21:06:09 +08:00
Mahdi Rahimi
d55b98b187 Updated Persian translations in fa.rs (#12450) 2025-07-30 13:13:28 +08:00
dependabot[bot]
d9674a2d77 Git submodule: Bump libs/hbb_common from f91459c to 57c8a23 (#12459)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `f91459c` to `57c8a23`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](f91459c4ab...57c8a23ab9)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 57c8a23ab970587ea6380943b04dc354020bbe7c
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 16:03:07 +08:00
rustdesk
26e5f7bbeb show websocket option on desktop 2025-07-29 11:53:45 +08:00
21pages
7a3e67e1d3 fix connect timeout of udp_nat_connect and udp_nat_listen (#12447)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-07-28 20:06:30 +08:00
fufesou
af53b1e8c9 fix: rendezvous server timeout (#12443)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-28 12:14:07 +08:00
Lynilia
9db7217cab Update fr.rs (#12438) 2025-07-28 12:12:44 +08:00
fufesou
d0651e32c5 fix: printer, printable area (#12442)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-28 11:42:30 +08:00
rustdesk
0646a5b313 try to fix reboot not working because retry too slow 2025-07-28 11:16:04 +08:00
RustDesk
e9692b94ca Revert "Fix/printer printable area (#12433)" (#12441)
This reverts commit 6e62c10fa0.
2025-07-28 10:38:19 +08:00
fufesou
6e62c10fa0 Fix/printer printable area (#12433)
* fix: printer, printable area

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

* refact: windows, sc config RustDesk --start= delayed-auto

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-27 19:47:23 +08:00
Mr-Update
52bfc02eea Update de.rs (#12424) 2025-07-26 18:42:19 +08:00
21pages
2282c8e308 opt assert for debug (#12420)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-07-26 18:41:57 +08:00
21pages
9409912344 update kcp-sys (#12419)
1. Update kcp-sys to send KCP in frames to avoid potential crashes.
2. Fix the issue when the controling side is closed, the kcp connection close is not immediately recognized by the controlled end.
  * Unless the controling side receives the close reason, force the sending of the close reason to the controlled end when using KCP, and delay for 30ms to ensure the message is sent successfully.
  * Move the CloseReason receiving forward, as this message needs to be received when unauthorized, especially for kcp.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-07-25 13:22:52 +08:00
XLion
2afd538cf1 Update tw.rs (#12412) 2025-07-25 13:13:31 +08:00
John Fowler
ab48f10f25 Update hu.rs (#12403)
Translate new string(s).
2025-07-24 17:43:06 +08:00
TheBitBrine
1b40d146ee Fix retry button blocked by overly broad "exist" filter (#12397)
The retry logic was blocking retry buttons for errors containing "exist", 
which incorrectly filtered out "An existing connection was forcibly closed" 
network errors. Changed to "not exist" to only block "ID does not exist" 
type errors while allowing legitimate network disconnection errors to show 
retry buttons.

Fixes issue where users couldn't retry after network disconnections.
2025-07-24 08:51:25 +08:00
fufesou
b4e13706bd refact: active terminal on conn the same remote (#12392)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-23 22:44:05 +08:00
21pages
f2473974b8 fix ci (#12387)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-07-23 17:10:26 +08:00
rustdesk
50fc6d691f 1.4.1 2025-07-23 15:51:44 +08:00
fufesou
247f0b7eb1 fix: terminal, check service_id (#12384)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-23 15:43:55 +08:00
fufesou
80c4a83a39 fix: build (#12385)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-23 13:53:04 +08:00
solokot
3fb3d51567 Update ru.rs (#12374) 2025-07-23 11:13:36 +08:00
VenusGirl❤
596e7b33db Update ko.rs (#12348) 2025-07-23 11:13:20 +08:00
bovirus
c01bbeea78 Italian language update (#12347) 2025-07-23 11:12:56 +08:00
flusheDData
47886c4068 Update es.rs (#12339)
New terms added
2025-07-23 11:12:16 +08:00
fufesou
348c477f75 fix: terminal, web, fonts (#12376)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-22 23:42:05 +08:00
fufesou
61194182eb fix: debug, terminal web (#12375)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-22 19:26:50 +08:00
fufesou
9bca5ac000 refact: terminal, save window pos on close (#12370)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-22 15:16:13 +08:00
fufesou
b65ef36049 fix: terminal, restore, multi-sessions, msgs (#12364)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-22 09:59:20 +08:00
fufesou
391ef70007 fix: terminal, persistent (#12357)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-21 17:15:02 +08:00
VenusGirl❤
9bcfe9d148 Update README-KR.md (#12329)
Update
2025-07-20 21:59:26 +08:00
rustdesk
94e23a6cd0 remove devcontainer.md 2025-07-19 14:26:11 +08:00
VenusGirl❤
55ddb9751a Create DEVCONTAINER-KR.md (#12331) 2025-07-19 14:25:47 +08:00
rustdesk
9d82ef1a22 remove terminal.md 2025-07-19 14:23:22 +08:00
fufesou
555bb66668 fix: terminal, handle newline (#12342)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-19 11:14:14 +08:00
21pages
1581272104 opt hint of elevation username (#12338)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-07-18 18:40:43 +08:00
bovirus
a37f4d79db Italian language update (#12321) 2025-07-18 18:16:59 +08:00
XLion
4723d07215 Update tw.rs (#12327)
* Update tw.rs

* Update tw.rs
2025-07-18 18:16:28 +08:00
Mr-Update
3177786219 Update de.rs (#12324) 2025-07-18 18:16:00 +08:00
solokot
061dc9962d Update ru.rs (#12332) 2025-07-18 18:15:56 +08:00
VenusGirl❤
0a62103ccd Update ko.rs (#12316) 2025-07-18 18:15:01 +08:00
John Fowler
2e2b4ac2fe Update hu.rs (#12323)
Translate new strings.
2025-07-18 18:14:47 +08:00
fufesou
e91f4fc104 fix: terminal, restore, cross users (#12335)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-18 16:25:53 +08:00
fufesou
bdd3bb946e refact: restore terminals (#12334)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-18 11:51:53 +08:00
VenusGirl❤
398b0d8d8b Update SECURITY-KR.md (#12308) 2025-07-17 21:11:58 +08:00
VenusGirl❤
dc41495566 Update CONTRIBUTING-KR.md (#12302) 2025-07-17 20:58:21 +08:00
VenusGirl❤
effbb45eb7 Update README-KR.md (#12301)
Translation Update
2025-07-17 20:53:15 +08:00
WC3D
4d960c3c8c Potential fix for code scanning alert no. 29: Workflow does not contain permissions (#12326)
If a GitHub Actions job or workflow has no explicit permissions set, then the repository permissions are used. Repositories created under an organization inherit the organization's permissions. Organizations or repositories created before February 2023 have default permissions set to read-write. Often, these permissions do not adhere to the principle of least privilege and can be reduced to read-only, leaving write permission only for specific types, such as issues (write) or pull requests (write).

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-17 08:54:53 +08:00
fufesou
475bef63d7 fix: linux, env TERM (#12325)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-17 08:46:32 +08:00
fufesou
e711f73451 fix: macos, defunct process (#12315)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-16 14:17:16 +08:00
fufesou
661be6ae36 fix: build (#12313)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-16 09:28:24 +08:00
fufesou
e31b04b6a7 fix: new translation message (#12312)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-16 09:25:47 +08:00
fufesou
d5eb87ee8b fix: try to fix stuck on read (#12310)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-15 23:36:16 +08:00
fufesou
65c721e088 fix: terminal connection on Linux and MacOS (#12307)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-15 23:09:04 +08:00
21pages
69af5f2fa6 update hwcodec (#12303)
* Test necessary codecs in single thread
* Terminate test process with parent process

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-07-15 18:49:45 +08:00
fufesou
abb7748ee9 refact: terminal, win, run as admin (#12300)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-15 16:32:14 +08:00
VenusGirl❤
8d559725d5 Update ko.rs (#12298)
* Update ko.rs

* Update ko.rs

* Update ko.rs
2025-07-15 15:42:19 +08:00
Mahdi Rahimi
8c68b83265 Update Arabic translation in ar.rs (#12284) 2025-07-15 15:42:07 +08:00
Mahdi Rahimi
ae255c83ee Updated Persian translations in fa.rs (#12283) 2025-07-14 15:28:01 +08:00
LittleFishYu2008
856362006a Update cn.rs (#12281)
* Update cn.rs

* Update cn.rs

* Update cn.rs

* Update cn.rs
2025-07-13 16:08:41 +08:00
Kleofass
331b624cd6 Update lv.rs (#12270) 2025-07-12 13:40:45 +08:00
rustdesk
0117e94e6f format 2025-07-11 22:33:35 +08:00
John Fowler
aa680533ae Update hu.rs (#12267)
Translate new strings.
2025-07-11 22:32:14 +08:00
dependabot[bot]
94e76c3b6f Git submodule: Bump libs/hbb_common from f850a16 to 25e761f (#12264)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `f850a16` to `25e761f`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](f850a167ac...25e761f467)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 25e761f46778b567061770bc64d66332a4503332
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 16:21:19 +08:00
Alex Rijckaert
0258b9adca Update nl.rs (#12216) 2025-07-08 16:19:49 +08:00
Mr-Update
f15b9f05fb Update de.rs (#12215) 2025-07-07 16:57:04 +08:00
248 changed files with 12521 additions and 5087 deletions

View File

@@ -5,7 +5,7 @@ env:
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
on:
workflow_dispatch:

View File

@@ -23,7 +23,7 @@ env:
MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
CARGO_NDK_VERSION: "3.1.2"
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
SCITER_NASM_DEBVERSION: "2.14-1"
SCITER_NASM_DEBVERSION: "2.15.05-1"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.24.5"
@@ -31,14 +31,15 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2025.01.13
# vcpkg version: 2025.08.27
# If we change the `VCPKG COMMIT_ID`, please remember:
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
# Or we may face build issue like
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
VERSION: "1.4.0"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.3"
NDK_VERSION: "r27c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -177,24 +178,24 @@ jobs:
# Download printer driver files and extract them to ./rustdesk
try {
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums
# Check and move the files
$checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value
$downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256
$checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value
$downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) {
Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file."
Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath .
$checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value
$downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256
$checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value
$downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) {
Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file."
Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath .
mkdir ./rustdesk/drivers
mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver
mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver
Expand-Archive printer_driver_adapter.zip -DestinationPath .
mv -Force .\printer_driver_adapter.dll ./rustdesk
} elseif ($checksum_driver -ne $downloadsum_driver.Hash) {
Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file."
Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file."
} else {
Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file."
}
@@ -391,6 +392,13 @@ jobs:
ls -l ./libs/portable/Runner.res;
fi
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: Release
- name: Sign rustdesk files
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
shell: bash
@@ -424,80 +432,6 @@ jobs:
files: |
./SignOutput/rustdesk-*.exe
build-for-macOS-arm64-selfhost:
# use build-for-macOS instead
if: false
runs-on: [self-hosted, macOS, ARM64]
needs: [generate-bridge]
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Build rustdesk
run: |
./build.py --flutter --hwcodec --unix-file-copy-paste
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
run: |
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-arm64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-macos-arm64
path: rustdesk-${{ env.VERSION }}-arm64.dmg # can not upload the directory directly or tar.gz file, which destroy the link structure, causing the codesign failed
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
run: |
# Patch create-dmg to give more attempts to unmount image
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ~/.p12/api-key.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
if: env.UPLOAD_ARTIFACT == 'true'
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-aarch64.dmg"
done
- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-aarch64.dmg
build-rustdesk-ios:
if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa
@@ -617,63 +551,6 @@ jobs:
# files: |
# flutter/build/ios/ipa/*.ipa
build-rustdesk-ios-selfhost:
#if: ${{ inputs.upload-artifact }}
if: false
runs-on: [self-hosted, macOS, ARM64]
needs: [generate-bridge]
strategy:
fail-fast: false
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
- name: Restore bridge files
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Build rustdesk lib
run: |
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
- name: Build rustdesk
# ios sdk not installed on this machine, I will install it later after I am back home
if: false
shell: bash
run: |
pushd flutter
# 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'
# uses: actions/upload-artifact@master
# with:
# name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
# path: flutter/build/ios/ipa/*.ipa
# - name: Publish ipa package
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
# uses: softprops/action-gh-release@v1
# with:
# prerelease: true
# tag_name: ${{ env.TAG_NAME }}
# files: |
# flutter/build/ios/ipa/*.ipa
build-for-macOS:
name: ${{ matrix.job.target }}
@@ -692,7 +569,7 @@ jobs:
}
- {
target: aarch64-apple-darwin,
os: macos-latest,
os: macos-14,
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
extra-build-args: "--screencapturekit",
arch: aarch64,
@@ -746,7 +623,7 @@ jobs:
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja
brew install llvm create-dmg nasm
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
if command -v pkg-config &>/dev/null; then
echo "pkg-config is already installed"
@@ -886,6 +763,7 @@ jobs:
needs:
- build-for-macOS
- build-for-windows-flutter
- build-for-windows-sciter
runs-on: ubuntu-latest
if: ${{ inputs.upload-artifact }}
steps:
@@ -907,9 +785,15 @@ jobs:
name: rustdesk-unsigned-windows-x86_64
path: ./windows-x86_64/
- name: Download Artifacts
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-windows-x86
path: ./windows-x86/
- name: Combine unsigned app
run: |
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
- name: Publish unsigned app
uses: softprops/action-gh-release@v1
@@ -1768,6 +1652,14 @@ jobs:
with:
submodules: recursive
- name: Modify vcpkg.json for armv7
if: matrix.job.vcpkg-triplet == 'arm-linux'
run: |
# Replace the baseline in vcpkg.json with ARMV7_VCPKG_COMMIT_ID for armv7 builds
sed -i 's/"baseline": ".*"/"baseline": "${{ env.ARMV7_VCPKG_COMMIT_ID }}"/' vcpkg.json
echo "Modified vcpkg.json for armv7 build:"
grep -A 2 -B 2 '"baseline"' vcpkg.json
- name: Free Space
run: |
df -h
@@ -1853,11 +1745,12 @@ jobs:
rm -rf vcpkg
git clone https://github.com/microsoft/vcpkg
pushd vcpkg
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
# build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
git reset --hard ${{ env.ARMV7_VCPKG_COMMIT_ID }}
CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics
else
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
sh bootstrap-vcpkg.sh -disableMetrics
fi
popd
@@ -1978,11 +1871,8 @@ jobs:
# https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo apt-get install -y libarchive-tools libfuse2
# set-up appimage-builder
pushd /tmp
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
popd
# https://github.com/AppImage/AppImageKit/issues/1395
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
# run appimage-builder
pushd appimage
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
@@ -2009,14 +1899,15 @@ jobs:
job:
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
# https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981
distro: ubuntu22.04,
on: ubuntu-22.04,
arch: x86_64,
suffix: "",
}
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
distro: ubuntu22.04,
on: ubuntu-22.04,
arch: x86_64,
suffix: "-sciter",
@@ -2084,6 +1975,8 @@ jobs:
if: False
name: build-rustdesk-web
runs-on: ubuntu-22.04
permissions:
contents: read
strategy:
fail-fast: false
env:

View File

@@ -16,8 +16,8 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
VERSION: "1.4.0"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.3"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -126,7 +126,7 @@ jobs:
- name: Install build runtime
run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
brew install llvm create-dmg nasm pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2

View File

@@ -10,6 +10,6 @@ jobs:
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: RustDesk.RustDesk
version: "1.4.0"
release-tag: "1.4.0"
version: "1.4.3"
release-tag: "1.4.3"
token: ${{ secrets.WINGET_TOKEN }}

961
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.4.0"
version = "1.4.3"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -98,7 +98,7 @@ ctrlc = "3.2"
# arboard = { version = "3.4", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
portable-pty = "0.8.1" # higher version not work on rustc 1.75
portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" }
system_shutdown = "4.0"
qrcode-generator = "4.1"
@@ -144,6 +144,10 @@ core-graphics = "0.22"
include_dir = "0.7"
fruitbasket = "0.10"
objc_id = "0.1"
# If we use piet "0.7" here, we must also update core-graphics to "0.24".
piet = "0.6"
piet-coregraphics = "0.6"
foreign-types = "0.3"
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
@@ -155,6 +159,11 @@ keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
tiny-skia = "0.11"
softbuffer = "0.4"
fontdb = "0.23"
bytemuck = "1.23"
ttf-parser = "0.25"
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
@@ -180,6 +189,8 @@ once_cell = {version = "1.18", optional = true}
nix = { version = "0.29", features = ["term", "process"]}
gtk = "0.18"
termios = "0.3"
terminfo = "0.8"
winit = "0.30"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13"

View File

@@ -15,7 +15,7 @@
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Advanced%20Features-blue)](https://rustdesk.com/pricing.html)
Yet another remote desktop solution, written in Rust. Works out of the box with 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

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.0
version: 1.4.3
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
@@ -99,3 +99,4 @@ AppDir:
AppImage:
arch: aarch64
update-information: guess
comp: gzip

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.0
version: 1.4.3
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:
@@ -102,3 +102,4 @@ AppDir:
AppImage:
arch: x86_64
update-information: guess
comp: gzip

View File

@@ -68,11 +68,8 @@ fn install_android_deps() {
}
path.push(target);
println!(
"{}",
format!(
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
)
"cargo:rustc-link-search={}",
path.join("lib").to_str().unwrap()
);
println!("cargo:rustc-link-lib=ndk_compat");
println!("cargo:rustc-link-lib=oboe");

137
docs/CODE_OF_CONDUCT-DE.md Normal file
View File

@@ -0,0 +1,137 @@
# Verhaltenskodex (Code of Conduct) für Mitwirkende
## Unsere Verpflichtung
Wir als Mitglieder, Mitwirkende und Führungskräfte verpflichten uns,
die Teilnahme unserer Community zu einer Erfahrung zu machen,
die für alle frei von Belästigungen ist, unabhängig von Alter, Körpergröße,
sichtbarer oder unsichtbarer Behinderung, ethnischer Zugehörigkeit,
Geschlechtsmerkmalen, Geschlechtsidentität und -ausdruck, Erfahrungsniveau,
Bildung, sozioökonomischem Status, Nationalität, persönlichem Erscheinungsbild,
Rasse, Religion oder sexueller Identität und Orientierung.
Wir verpflichten uns, so zu handeln und zu interagieren, dass wir zu einer offenen,
einladenden, vielfältigen, integrativen und lebendigen Gemeinschaft beitragen.
## Unsere Standards
Beispiele für Verhaltensweisen, die zu einem positiven Umfeld für unsere
Gemeinschaft beitragen, sind:
* Empathie und Freundlichkeit gegenüber anderen Menschen zu zeigen
* Respektvoll gegenüber anderen Meinungen, Sichtweisen und Erfahrungen zu sein
* Das Vergeben von sowie das großzügige Empfangen von konstruktivem Feedback
* Verantwortung übernehmen, sich bei den Betroffenen entschuldigen
und aus den Erfahrungen lernen
* Nicht darauf zu achten, was das Beste für sich selbst,
sondern zu Achten, was das Beste für die gesamte Community ist
Beispiele für nicht akzeptables Verhalten sind:
* Die Verwendung sexualisierter bzw. anstößiger Sprache oder Bilder
sowie sexuelle Aufmerksamkeit oder Annäherungsversuche jeglicher Art
* Trolling, beleidigende oder herabwürdigende Kommentare
sowie persönliche oder politische Angriffe
* Öffentliche sowie private Belästigung
* Das Teilen privater Informationen anderer Leute ohne deren explizite Zustimmung,
wie bspw. die physische oder die E-Mail-Adresse
* Anderes Verhalten, das in einem professionellen Umfeld begründeter Weise als
unangemessen angesehen werden könnte
## Durchsetzungsbefugnisse
Die Leiter der Community sind dafür verantwortlich, unsere Standards für
akzeptables Verhalten zu klären und durchzusetzen und werden angemessene
und faire Korrekturmaßnahmen ergreifen, wenn sie ein Verhalten als unangemessen,
bedrohlich, beleidigend oder schädlich erachten.
Die Leiter der Community haben das Recht und die Pflicht, Kommentare, Commits,
Code, Wiki-Bearbeitungen, Issues und andere Beiträge, die nicht mit dem
Verhaltenskodex vereinbar sind, zu entfernen, zu bearbeiten oder abzulehnen.
Sie werden, falls angebracht, die Gründe für Moderationsentscheidungen mitteilen.
## Geltungsbereich
Dieser Verhaltenskodex gilt in allen Community-Bereichen und auch dann, wenn
eine Person die Community offiziell in öffentlichen Bereichen vertritt.
Beispiele für die Vertretung unserer Community sind die Verwendung einer
offiziellen E-Mail-Adresse, das Posten über einen offiziellen
Social-Media-Account oder die Tätigkeit als ernannter
Vertreter bei einer Online- oder Präsenzveranstaltung.
## Geltendmachung
Fälle von missbräuchlichem, belästigendem oder anderweitig inakzeptablem Verhalten können
den für die Durchsetzung zuständigen Community-Leitern
unter [info@rustdesk.com](mailto:info@rustdesk.com) gemeldet werden.
Jeder Fall wird umgehend und fair geprüft und untersucht.
## Richtlinien zur Geltendmachung
Die Community-Leiter werden die folgenden Community-Auswirkungsrichtlinien befolgen,
um die Konsequenzen für jede Handlung zu bestimmen, die sie als Verstoß gegen diesen
Verhaltenskodex ansehen:
### 1. Korrektur
**Auswirkungen auf die Community**: Verwendung unangemessener Sprache oder anderes
Verhalten, welches als unprofessionell oder in der Community unerwünscht angesehen wird.
**Konsequenz**: Eine private, schriftliche Verwarnung durch die Leiter der Community,
in der die Art des Verstoßes klar dargelegt und erklärt wird, warum das
Verhalten unangemessen war. Eine öffentliche Entschuldigung kann verlangt werden.
### 2. Warnung
**Auswirkungen auf die Community**: Ein Verstoß durch einen einzelnen Vorfall
oder eine Reihe von Handlungen.
**Konsequenz**: Eine Verwarnung mit Konsequenzen für das weitere Verhalten. Keine
Interaktion mit den beteiligten Personen, einschließlich unaufgeforderter Interaktion mit
denjenigen, die den Verhaltenskodex durchsetzen, für einen bestimmten Zeitraum. Dies
schließt die Vermeidung von Interaktionen in Gemeinschaftsräumen sowie externen Kanälen
wie sozialen Medien ein. Ein Verstoß gegen diese Bedingungen kann zu einer vorübergehenden oder
dauerhaften Sperrung führen.
### 3. Temporärer Sperrung
**Auswirkungen auf die Community**: Ein schwerwiegender Verstoß gegen die Community-Standards,
einschließlich anhaltend unangemessenem Verhalten.
**Konsequenz**: Eine vorübergehende Sperrung jeglicher Art von Interaktion oder öffentlicher
Kommunikation mit der Community für einen bestimmten Zeitraum. Während dieses Zeitraums sind
keine öffentlichen oder privaten Interaktionen mit den betroffenen Personen,
einschließlich unaufgeforderter Interaktionen mit denjenigen,
die den Verhaltenskodex durchsetzen, erlaubt.
Ein Verstoß gegen diese Bedingungen kann zu einer dauerhaften Sperrung führen.
### 4. Dauerhafte Sperrung
**Auswirkungen auf die Community**: Wiederholte Verstöße gegen die Community-Standards,
einschließlich anhaltend unangemessenem Verhalten, Belästigung einer
Person oder Aggression gegenüber oder Herabwürdigung von Personengruppen.
**Konsequenz**: Ein dauerhafter Ausschluss von jeglicher öffentlicher
Interaktion innerhalb der Community.
## Quellenangabe
Dieser Verhaltenskodex ist eine Adaption des [Contributor Covenant][homepage],
Version 2.0, verfügbar unter
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Die Richtlinien zu den Auswirkungen auf die Gemeinschaft wurden inspiriert von
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
Für Antworten auf häufig gestellte Fragen zu diesem Verhaltenskodex siehe die
häufig gestellten Fragen (FAQ) unter
[https://www.contributor-covenant.org/faq][FAQ]. Übersetzungen sind verfügbar
unter [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

133
docs/CODE_OF_CONDUCT-KR.md Normal file
View File

@@ -0,0 +1,133 @@
# 기여자 계약 행동 강령
## 우리의 서약
회원, 기여자, 리더로서 우리는 나이, 신체 크기, 눈에
보이거나 보이지 않는 장애, 민족, 성 특성, 성 정체성 및
표현, 경험 수준, 교육, 사회 경제적 지위, 국적, 외모,
인종, 종교, 성적 정체성 및 지향에 관계없이 모든 사람이
괴롭힘 없이 커뮤니티에 참여할 수 있도록 할 것을
서약합니다.
우리는 개방적이고 환영하며 다양하고 포용적이며 건강한 커뮤니티에
기여하는 방식으로 행동하고 교류할 것을 약속합니다.
## 우리의 표준
커뮤니티의 긍정적인 환경에 기여하는 행동의 예는
다음과 같습니다:
* 다른 사람들에게 공감과 친절을 보여주기
* 다양한 의견, 관점, 경험을 존중하기
* 건설적인 피드백을 제공하고 우아하게 받아들이기
* 우리의 실수로 인해 영향을 받은 사람들에게 책임을 받아들이고 사과하며
그 경험을 통해 배우기
* 우리 개인뿐만 아니라 전체 커뮤니티에 가장 좋은 것이 무엇인지
집중하기
용납할 수 없는 행동의 예는 다음과 같습니다:
* 성적인 언어 또는 이미지의 사용, 모든 종류의 성적 관심 또는
접근 행위
* 트롤링, 모욕적이거나 경멸적인 댓글, 개인적 또는 정치적 공격
* 공개적 또는 사적인 괴롭힘
* 명시적인 허가 없이 타인의 실제 주소 또는 이메일 주소와 같은
개인정보를 게시하는 행위
* 직업적 환경에서 합리적으로 부적절하다고 간주될 수 있는
기타 행위
## 시행 책임
커뮤니티 리더는 허용되는 행동의 기준을 명확히 하고 시행할
책임이 있으며 부적절하거나 위협적이거나 모욕적이거나
유해하다고 판단되는 행동에 대해 적절하고 공정한 시정 조치를
취합니다.
커뮤니티 리더는 본 행동 강령에 부합하지 않는 댓글, 커밋,
코드, 위키 편집, 이슈 및 기타 기여를 삭제, 편집 또는 거부할
권한과 책임이 있으며, 적절한 경우 중재 결정의 이유를
전달합니다.
## 범위
본 행동 강령은 모든 커뮤니티 공간에서 적용되며, 개인이 공개
공간에서 커뮤니티를 공식적으로 대표하는 경우에도 적용됩니다.
커뮤니티를 대표하는 예로는 공식 이메일 주소 사용, 공식 소셜 미디어
계정을 통한 게시, 온라인 또는 오프라인 이벤트에서 지정된 대표자로
활동하는 것 등이 있습니다.
## 시행
모욕적, 괴롭힘 또는 기타 용납할 수 없는 행동은
[info@rustdesk.com](mailto:info@rustdesk.com)으로 법 집행을 담당하는 커뮤니티 리더에게
신고하실 수 있습니다.
모든 불만 사항은 신속하고 공정하게 검토 및 조사됩니다.
모든 커뮤니티 리더는 모든 사건 신고자의 사생활과 보안을 존중할 의무가
있습니다.
## 시행 지침
커뮤니티 리더는 이 행동 강령을 위반한 것으로 간주되는 모든 행동에 대한
결과를 결정할 때 다음 커뮤니티 영향 지침을 따릅니다:
### 1. 수정
**커뮤니티 영향**: 커뮤니티에서 비전문적이거나 환영받지 못하는
것으로 간주되는 부적절한 언어 사용이나 기타 행위입니다.
**결과**: 커뮤니티 리더의 비공개 서면 경고. 위반 사항의 성격과
해당 행동이 부적절했던 이유를 명확히 설명해야 합니다.
공개 사과를 요청할 수도 있습니다.
### 2. 경고
**커뮤니티 영향**: 단일 사건 또는 일련의 행위를 통한
위반입니다.
**결과**: 지속적인 행동에 대한 경고 및 결과. 행동 강령 시행 담당자와의
원치 않는 상호작용을 포함하여 관련자와의 상호작용은 일정
기간 동안 금지됩니다. 여기에는 공동 공간 및 소셜 미디어와
같은 외부 채널에서의 상호작용 금지가 포함됩니다. 이러한
조건을 위반할 경우 일시적 또는 영구적으로 이용이 금지될 수
있습니다.
### 3. 일시 금지
**커뮤니티 영향**: 지속적인 부적절한 행동을 포함하여
커뮤니티 기준을 심각하게 위반한 경우입니다.
**결과**: 일정 기간 동안 커뮤니티와의 모든 상호작용이나 공개적인 소통이
일시적으로 금지됩니다. 이 기간 동안에는 행동 강령을 시행하는
사람들과의 원치 않는 상호작용을 포함하여 관련자들과의 공개적 또는
사적인 상호작용이 허용되지 않습니다.
이러한 조건을 위반할 경우 영구적으로 이용이 금지될 수 있습니다.
### 4. 영구 금지
**커뮤니티 영향**: 지속적인 부적절한 행동, 특정 개인에 대한 괴롭힘,
특정 계층에 대한 공격성 또는 비하 등 공동체 기준을 위반하는
행동을 보이는 경우입니다.
**결과**: 공동체 내 모든 종류의 공개적인 상호작용이 영구적으로
금지됩니다.
## 귀속
본 행동 강령은 [Contributor Covenant][homepage] 버전 2.0을 바탕으로 작성되었으며
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]에서
확인하실 수 있습니다.
커뮤니티 영향 지침은
[Mozilla's code of conduct enforcement ladder][Mozilla CoC]에서 영감을 받았습니다.
본 행동 강령에 대한 일반적인 질문은 [https://www.contributor-covenant.org/faq][FAQ]에서 FAQ를
참조하세요. 번역은 [https://www.contributor-covenant.org/translations][translations]에서
확인하실 수 있습니다.
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,40 +1,46 @@
# RustDesk 기여하기
# RustDesk 기여하기
RustDesk는 모든 분들의 여를 환영합니다. RustDesk에 기여하고 싶으시다면 아래 가이드를 참고해 주세요:
RustDesk는 모든 분들의 여를 환영합니다. 저희를 도와주실 생각이 있으시다면
다음 지침을 따르세요:
## 기여 방법
## 기여
RustDesk 프로젝트 또는 관련 라이브러리에 대한 기여는 GitHub 풀 리퀘스트(Pull Request) 형태로 이루어져야 합니다.
각 풀 리퀘스트는 핵심 기여자(패치 적용 권한이 있는 사람)가 검토하며,
메인 브랜치에 통합되거나 필요한 변경 사항에 대한 피드백을 받게 됩니다.
핵심 기여자를 포함 모든 기여자는 이 형식을 따라야 합니다.
RustDesk 또는 그 종속성에 대한 기여는 GitHub 풀 리퀘스트 형태로
이루어져야 합니다. 각 풀 리퀘스트는 핵심 기여자 (패치 적용 권한이
있는 사람)가 검토하여 메인 트리에 추가하거나 필요한 변경 사항에
대한 피드백을 제공합니다. 핵심 기여자의 기여를 포함하여 모든 기여
이 형식을 따라야 합니다.
특정 이슈에 대해 작업하고 싶다면, 먼저 해당 GitHub 이슈에 댓글을 달아 작업 의사를 알려주세요.
이는 여러 기여자가 동일한 이슈에 대해 중복으로 작업하는 것을 방지하기 위함입니다.
이슈에 대해 작업하고 싶으시면 먼저 해당 이슈에 대해 작업하고 싶다는
댓글을 달아 해당 이슈를 요청하세요. 이는 동일한 이슈에 대한 기여자의
중복된 노력을 방지하기 위한 것입니다.
## 풀 리퀘스트 체크리스트
- master 브랜치에서 브랜치를 만들고, 필요한 경우 Pull Request를 제출하기 전에 현재 master
브랜치로 리베이스하세요. master 브랜치와 깔끔하게 병합(merge)되지 않으면 변경 사항을
리베이스하도록 요청받을 수 있습니다.
- Master 브랜치에서 브랜치를 만들고, 필요한 경우 풀 리퀘스트를 제출하기
전에 현재 마스터 브랜치로 리베이스하세요. 마스터 브랜치와 깔끔하게
병합되지 않으면 변경 사항을 리베이스하라는 요청받을 수 있습니다.
- 커밋(commit)은 가능한 한 작게 유지하고, 각 커밋이 독립적으로 올바른지 (즉, 각 커밋이 컴파일되고 테스트를 통과하는지) 확인해야 합니다.
- 커밋은 가능한 한 작아야 하지만, 각 커밋이 독립적으로 올바른지 확인
해야 합니다 (즉, 각 커밋은 컴파일되어 테스트를 통과해야 함).
- 커밋에는 개발자 원본 증명서(DCO, Developer Certificate of Origin - http://developercertificate.org) 서명이 포함되어야 합니다. 이는 기여자(해당하는 경우
기여자의 고용주 포함)가 [프로젝트 라이선스](../LICENCE) 조건에 동의함을 의미합니다.
Git에서는 `git commit` 명령어에 `-s` 옵션을 사용합니다.
- 커밋에는 개발자 출처 증명서 (http://developercertificate.org)
서명이 첨부되어야 하며, 이는 귀하 (및 해당되는 경우 고용주)가
[프로젝트 라이선스](../LICENCE). 조건에 구속되는 데 동의한다는 것을 나타냅니다.
git에서는 `git commit``-s` 옵션입니다
- 패치가 검토되지 않거나 특정 리뷰어의 검토가 필요하다면, 풀 리퀘스트나 댓글에서
@멘션으로 리뷰어에게 알리거나 [이메일](mailto:info@rustdesk.com)로 검토를 요청할 수 있습니다.
- 패치가 검토되지 않거나 특정인이 검토해야 하는 경우, 풀 리퀘스트나
댓글에서 검토자에게 @-답글을 보내 검토를 요청하거나
[이메일](mailto:info@rustdesk.com)을 통해 검토를 요청할 수 있습니다.
- 수정 버그나 추가한 기능과 관련된 테스트 코드를 포함해 주세요.
- 수정 버그 또는 새 기능과 관련된 테스트를 추가합니다.
Git 사용에 대한 자세한 내용은 [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow) 문서를하세요.
구체적인 git 지침은, [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)하세요.
## 기여자 행동 강령
## 행동 강령
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## 소통 채널
## 커뮤니케이션
RustDesk 기여자들은 주로 [Discord](https://discord.gg/nDceKgxnkV)에서 소통합니다.
RustDesk 기여자들은 [Discord](https://discord.gg/nDceKgxnkV)에서 활동하고 있습니다.

View File

@@ -1,14 +0,0 @@
Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Bin<69>rprogramm im Debug-Modus erstellt.
Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an.
Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgef<65>hrt werden m<>ssen, um bestimmte Builds zu erstellen.
Kommando|Build-Typ|Modus
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@@ -1,14 +0,0 @@
Dopo l'avvio di devcontainer nel contenitore docker, viene creato un binario linux in modalità debug.
Attualmente devcontainer consente creazione build Linux e Android sia in modalità debug che in modalità rilascio.
Di seguito è riportata la tabella dei comandi da eseguire dalla root del progetto per la creazione di build specifiche.
Comando|Tipo build|Modo
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@@ -1,14 +0,0 @@
docker コンテナで devcontainer を起動すると、デバッグモードの linux バイナリが作成されます。
現在 devcontainer では、Linux と android のビルドをデバッグモードとリリースモードの両方で提供しています。
以下は、特定のビルドを作成するためにプロジェクトのルートから実行するコマンドの表になります。
コマンド|ビルド タイプ|モード
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@@ -1,15 +0,0 @@
Na de start van devcontainer in docker container wordt een linux binaire in foutmodus aangemaakt.
Momenteel biedt devcontainer linux en android builds in zowel foutopsporing- als uitgave modus.
Hieronder staat de tabel met commando's die vanuit de root van het project moeten worden
uitgevoerd om specifieke builds te maken.
Commando|Build Type|Modus
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|debug

View File

@@ -1,14 +0,0 @@
Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget.
Nå tilbyr devcontainer linux og android builds i både debug og release modus.
Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds.
Kommando|Build Type|Modus
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@@ -1,14 +0,0 @@
Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania.
Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej.
Poniżej tabela poleceń do uruchomienia z głównego folderu do tworzenia wybranych kompilacji.
Polecenie|Typ kompilacji|Tryb
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|debug

View File

@@ -1,12 +0,0 @@
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

@@ -1,14 +0,0 @@
After the start of devcontainer in docker container, a linux binary in debug mode is created.
Currently devcontainer offers linux and android builds in both debug and release mode.
Below is the table on commands to run from root of the project for creating specific builds.
Command|Build Type|Mode
-|-|-|
`.devcontainer/build.sh --debug linux`|Linux|debug
`.devcontainer/build.sh --release linux`|Linux|release
`.devcontainer/build.sh --debug android`|android-arm64|debug
`.devcontainer/build.sh --release android`|android-arm64|release

View File

@@ -11,7 +11,7 @@
[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) :تواصل معنا عبر
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA%20%D8%A7%D9%84%D9%85%D8%AA%D9%82%D8%AF%D9%85%D8%A9-blue)](https://rustdesk.com/pricing.html)
.Rustبرنامج آخر لسطح المكتب عن بعد، مكتوب بـ
يعمل خارج الصندوق، لا حاجة إلى إعدادات. لديك سيطرة كاملة على بياناتك، دون مخاوف بشأن الأمن. يمكنك استخدام خادم

View File

@@ -12,7 +12,7 @@
Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Pokro%C4%8Dil%C3%A9%20Funkce-blue)](https://rustdesk.com/pricing.html)
Zase další software pro přístup k ploše na dálku, naprogramovaný v jazyce Rust. Funguje hned tak, jak je není třeba žádného nastavování. Svá data máte ve svých rukách, bez obav o zabezpečení. Je možné používat námi poskytovaný propojovací/předávací (relay) server, [vytvořit si svůj vlastní](https://rustdesk.com/server), nebo [si dokonce svůj vlastní naprogramovat](https://github.com/rustdesk/rustdesk-server-demo), budete-li chtít.

View File

@@ -11,7 +11,7 @@
Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Avancerede%20Funktioner-blue)](https://rustdesk.com/pricing.html)
Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo).

View File

@@ -16,7 +16,7 @@
Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Erweiterte%20Funktionen-blue)](https://rustdesk.com/pricing.html)
RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Sie haben die volle Kontrolle über Ihre Daten und müssen sich keine Sorgen um die Sicherheit machen. Sie können unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Altnivela%20Funkcioj-blue)](https://rustdesk.com/pricing.html)
Denove alia fora labortabla programo, skribita en Rust. Ĝi funkcias elskatole, ne bezonas konfiguraĵon. Vi havas la tutan kontrolon sur viaj datumoj, sen zorgo pri sekureco. Vi povas uzi nian servilon rendezvous/relajsan, [agordi vian propran](https://rustdesk.com/server), aŭ [skribi vian propran servilon rendezvous/relajsan](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -15,7 +15,7 @@
Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Caracter%C3%ADsticas%20Avanzadas-blue)](https://rustdesk.com/pricing.html)
Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -12,7 +12,7 @@
با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D9%88%DB%8C%DA%98%DA%AF%DB%8C%E2%80%8C%D9%87%D8%A7%DB%8C%20%D9%BE%DB%8C%D8%B4%D8%B1%D9%81%D8%AA%D9%87-blue)](https://rustdesk.com/pricing.html)
راست‌دسک (RustDesk) نرم‌افزاری برای کارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید.

View File

@@ -11,7 +11,7 @@
Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Edistyneet%20Ominaisuudet-blue)](https://rustdesk.com/pricing.html)
Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetusta. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/server), tai [kirjoittaa oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Fonctionnalit%C3%A9s%20Avanc%C3%A9es-blue)](https://rustdesk.com/pricing.html)
Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/server), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%CE%A0%CF%81%CE%BF%CE%B7%CE%B3%CE%BC%CE%AD%CE%BD%CE%B5%CF%82%20%CE%94%CF%85%CE%BD%CE%B1%CF%84%CF%8C%CF%84%CE%B7%CF%84%CE%B5%CF%82-blue)](https://rustdesk.com/pricing.html)
Ένα λογισμικό απομακρυσμένης επιφάνειας εργασίας, γραμμένο σε γλώσσα Rust. Δεν χρειάζεται κάποια παραμετροποίηση, λειτουργεί αμέσως μετά την εγκατάσταση. Έχετε τον πλήρη έλεγχο των δεδομένων σας, χωρίς να ανησυχείτε για την ασφάλειά τους. Μπορείτε να χρησιμοποιήσετε τους προκαθορισμένους διακομιστές rendezvous/αναμετάδοσης, [να εγκαταστήσετε τον δικό σας διακομιστή](https://rustdesk.com/server), ή [να αναπτύξετε ένα δικό σας διακομιστή rendezvous/αναμετάδοσης](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Speci%C3%A1lis%20Funkci%C3%B3k-blue)](https://rustdesk.com/pricing.html)
A RustDesk egy távoli elérésű asztali szoftver, Rust-ban írva. Működik mindenféle konfiguráció nélkül, feltelepítéssel, vagy anélkül. Az adataidat teljesen te kezeled, nincs szükség aggódásra a harmadik felek miatt. Használhatod a RustDesk punblikus randevú/relay szervereit, [hostolhatsz sajátot](https://rustdesk.com/server), vagy akár [írhatsz is egyet](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Fitur%20Lanjutan-blue)](https://rustdesk.com/pricing.html)
[![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)

View File

@@ -11,7 +11,7 @@
Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Funzionalit%C3%A0%20Avanzate-blue)](https://rustdesk.com/pricing.html)
[![Bounties aperti](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)

View File

@@ -11,7 +11,7 @@
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%E9%AB%98%E5%BA%A6%E3%81%AA%E6%A9%9F%E8%83%BD-blue)](https://rustdesk.com/pricing.html)
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます。

View File

@@ -1,44 +1,44 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>]<br>
<b>이 README <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 여러분의 모국어로 번역하는 데 도움이 필요합니다.</b>
<a href="#빌드를 위한 원시 단계">빌드</a> •
<a href="#Docker로 빌드하는 방법">Docker</a> •
<a href="#파일 구조">구조</a> •
<a href="#스크린샷">스냇샷</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>]<br>
<b>이 README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 귀하의 모국어로 번역하는 데 도움이 필요합니다</b>
</p>
> [!Caution]
> **오용 관련 면책 조항:** <br>
> RustDesk 개발자는 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용납하거나 지원하지 않습니다. 무단 액세스, 제어 또는 사생활 침해와 같은 오용은 당사의 가이드라인에 엄격히 위배됩니다. 개발자는 애플리케이션의 오용에 대해 책임을 지지 않습니다.
채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
> **오용 면책 조항:** <br>
> RustDesk 개발자는 이 소프트웨어의 비윤리적 또는 불법적인 사용을 묵인하거나 지원하지 않습니다. 무단 액세스, 제어 또는 개인정보 침해와 같은 오용은 엄격하게 당사의 지침에 위배됩니다. 작성자는 응용 프로그램의 오용에 대해 책임을 지지 않습니다.
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
Rust로 작성되었고, 설정 없이 바로 사용할 수 있는 원격 데스크톱 소프트웨어입니다. 자신의 데이터를 완전히 제어할 수 있고, 보안 염려도 없습니다. 저희 rendezvous/relay 서버를 사용하거나, [직접 설정](https://rustdesk.com/server)하거나 [자체 rendezvous/relay 서버를 구축](https://github.com/rustdesk/rustdesk-server-demo)할 수도 있습니다.
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%EA%B3%A0%EA%B8%89%20%EA%B8%B0%EB%8A%A5-blue)](https://rustdesk.com/pricing.html)
또 하나의 원격 데스크톱 솔루션으로, Rust로 작성되었습니다. 별도의 설정 없이 바로 사용할 수 있습니다. 데이터에 대한 완전한 통제권을 가지며 보안에 대한 걱정이 없습니다. 저희 랑데부/릴레이 서버를 사용하거나, [직접 설정](https://rustdesk.com/server)하거나, [자신만의 랑데부/릴레이 서버를 작성](https://github.com/rustdesk/rustdesk-server-demo)할 수 있습니다.
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk는 모든 기여를 환영합니다. 기여하고 싶다면 [`CONTRIBUTING-KR.md`](CONTRIBUTING-KR.md)를 참고해 주세요.
RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움이 필요하면 [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조하세요.
[**자주 묻는 질문 (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**자주 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**바이너리 다운로드**](https://github.com/rustdesk/rustdesk/releases)
[**나이틀리 빌드**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[**개발자 빌드**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="F-Droid에서 다운로드"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
[<img src="https://flathub.org/api/badge?svg&locale=en"
alt="Flathub에서 다운로드"
alt="Get it on Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
## 의존
## 종속
데스크톱 버전은 GUI Flutter 또는 Sciter (지원 중단됨)를 사용합니다. 이 튜토리얼은 Sciter 전용이며, 시작하기 더 쉽고 친숙하기 때문입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) 확인하세요.
데스크톱 버전은 GUI Flutter 또는 Sciter (더 이상 지원되지 않음)를 사용하며, 이 자습서는 시작하기 더 쉽고 친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) 확인하세요.
Sciter 동적 라이브러리를 직접 다운로드하세요.
@@ -46,20 +46,20 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## 기본 빌드 방법
## 빌드를 위한 원시 단계
- Rust 개발 환경과 C++ 빌드 환경을 준비하세요.
- Rust 개발 환경과 C++ 빌드 환경을 준비합니다
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경변수를 정확히 설정하세요.
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다
- 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
- Linux/macOS: vcpkg install libvpx libyuv opus aom
- `cargo run` 실행합니다.
- `cargo run` 실행
## [빌드](https://rustdesk.com/docs/en/dev/build/)
## Linux에서 빌드 방법
## Linux에서 빌드하는 방법
### Ubuntu 18 (Debian 10)
@@ -99,7 +99,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### libvpx 수정 (For Fedora용)
### libvpx 수정 (Fedora용)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -136,41 +136,41 @@ git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
그 다음, 애플리케이션을 빌드하려면 다음 명령을 실행하세요:
다음 응용 프로그램을 빌드해야 할 때마다 다음 명령을 실행합니다:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
빌드 시에는 의존성이 캐시되느라 시간이 걸릴 수 있지만, 그 이후 빌드부터는 더 빨라집니다. 빌드 명령에 다른 인수를 추가하고 싶다면, 명령 끝의 `<OPTIONAL-ARGS>` 부분에 지정하세요. 예를 들어, 최적화된 릴리 버전을 빌드하고 싶다면 위 명령 뒤에 `--release`붙여 실행합니다. 결과 실행 파일은 시스템의 target 폴더에 생성되며, 다음 명령으로 실행할 수 있습니다:
번째 빌드는 종속성이 캐시되기까지 시간이 오래 걸릴 수 있으며, 이후 빌드는 더 빨라집니다. 또한 빌드 명령에 다른 인수를 지정해야 하는 경우 명령 끝의 `<OPTIONAL-ARGS>` 위치에 인수를 지정할 수 있습니다. 예를 들어 최적화된 릴리 버전을 빌드하면 위 명령 뒤에 `--release`추가하면 됩니다. 결과 실행 파일은 시스템의 대상 폴더에서 사용할 수 있으며 실행할 수 있습니다::
```sh
target/debug/rustdesk
```
또는, 릴리 실행 파일을 실행하는 경우:
또는 릴리 실행 파일을 실행하는 경우:
```sh
target/release/rustdesk
```
이 명령들은 RustDesk 리포지토리의 루트 디렉토리에서 실행해야 합니다. 그렇지 않으면 애플리케이션이 필요한 리소스를 찾지 못할 수 있습니다. 또한, `install` 또는 `run`과 같은 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방식은 지원되지 않습니다. 이 점에 유의해 주세요.
RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의세요.
## 파일 구조
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, TCP/UDP 래퍼, Protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫폼별 키보드/마우스 제어
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows, Linux, macOS용 파일 복사 및 붙여넣기 구현
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 더 이상 사용되지 않는 Sciter UI (지원 중단)
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 더 이상 사용되지 않는 Sciter UI (지원 중단)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오/클립보드/입력/비디오 서비스 및 네트워크 연결
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 연결 시작
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, Remote Direct (TCP Hole Punching) 또는 Relayed Connection 대기
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 클라이언트용 JavaScript
## 스
## 스크린
![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)

View File

@@ -11,7 +11,7 @@
ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%E0%B4%B5%E0%B4%BF%E0%B4%95%E0%B4%B8%E0%B4%BF%E0%B4%A4%20%E0%B4%B8%E0%B4%B5%E0%B4%BF%E0%B4%B6%E0%B5%87%E0%B4%B7%E0%B4%A4%E0%B4%95%E0%B5%BE-blue)](https://rustdesk.com/pricing.html)
റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ. ബോക്‌സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/server), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Geavanceerde%20Functies-blue)](https://rustdesk.com/pricing.html)
Alweer een andere programma voor -bureaublad op afstand-, geschreven in Rust. Werkt -out of the box-, geen configuratie nodig. U heeft volledige controle over uw gegevens, en hoeft zich geen zorgen te maken over de beveiliging. U kunt onze rendez-vous/relay server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Avanserte%20Funksjoner-blue)](https://rustdesk.com/pricing.html)
Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Zaawansowane%20Funkcje-blue)](https://rustdesk.com/pricing.html)
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -11,7 +11,7 @@
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Recursos%20Avan%C3%A7ados-blue)](https://rustdesk.com/pricing.html)
Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -15,7 +15,7 @@
Общение с нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D0%A0%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%92%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8-blue)](https://rustdesk.com/pricing.html)
Ещё одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, настройки не требует. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -7,34 +7,37 @@
<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>
<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 Dökümantasyonu</a>'nu ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
</p>
> [!Dikkat]
> **Yanlış Kullanım Uyarısı:** <br>
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Geli%C5%9Fmi%C5%9F%20%C3%96zellikler-blue)](https://rustdesk.com/pricing.html)
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).
Rust dilinde yazılmış, başka bir uzak masaüstü yazılımı daha. Hiçbir yapılandırma gerekmeksizin, hemen kullanıma hazır. Güvenlik konusunda hiçbir endişe duymadan, verileriniz üzerinde tam kontrole sahip olun. Kendi rendezvous/relay sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi rendezvous/relay 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](CONTRIBUTING-TR.md) belgesine göz atın.
RustDesk, herkesin katkısına açıktır. Başlamak için [CONTRIBUTING.md](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)
[**BINARY İNDİR**](https://github.com/rustdesk/rustdesk/releases)
[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[**NIGHTLY 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)
## Bağımlılıklar
## Gereksinimler
Masaüstü sürümleri GUI için
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
Masaüstü sürümleri GUI için; [Sciter](https://sciter.com/)(kaldırılacak) veya Flutter kullanır. Sciter daha kolay ve başlamak için daha dostcanlısı, bundan dolayı bu kılavuz sadece Sciter içindir. Flutter sürümünü derlemek için [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)'ımıza bakın.
Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
@@ -46,7 +49,7 @@ Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
- 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.
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` ortam 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
@@ -123,7 +126,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
## Docker ile Derleme Nasıl Yapılır
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
Önce repository'i klonlayın ve Docker container'ını oluşturun.
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -131,44 +134,40 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
Ardından, uygulamayı her derlemeniz gerektiğinde 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:
Bilin ki ilk derlemeniz gereksinimlerin önbelleği yüklenmesinden ötürü uzun sürebilir, sonraki derlemeleriniz daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu komutun sonunda ki `<OPTIONAL-ARGS>` yerine yazabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan çalıştırılabilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir olacaktır:
```sh
target/debug/rustdesk
```
Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
Veya, yayım çalıştırılabilir dosyası için:
```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.
Lütfen bu komutları RustDesk reposunun root klasöründe ç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, 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/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, dosya transferi için fs fonksiyonları 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
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: platforma özgü kopyala/yapıştır implementasyonları.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Eski Sciter UI (kaldırılacak)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pano/input/video servisleri ve ağ bağlantıları
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Eşli bağlantı başlat
- **[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şime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
- **[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
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript
> [!Dikkat]
> **Yanlış Kullanım Uyarısı:** <br>
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
## Ekran Görüntüleri

View File

@@ -11,7 +11,7 @@
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D0%A0%D0%BE%D0%B7%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D1%96%20%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D1%96%D1%97-blue)](https://rustdesk.com/pricing.html)
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -13,7 +13,7 @@
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-T%C3%ADnh%20N%C4%83ng%20N%C3%A2ng%20Cao-blue)](https://rustdesk.com/pricing.html)
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -14,7 +14,7 @@
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%E9%AB%98%E7%BA%A7%E5%8A%9F%E8%83%BD-blue)](https://rustdesk.com/pricing.html)
远程桌面软件,开箱即用,无需任何配置。您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器,
或者[自己设置](https://rustdesk.com/server)

View File

@@ -2,6 +2,6 @@
## 취약점 보고
저희는 프로젝트의 보안을 매우 중요하게 생각합니다. 모든 사용자가 발견한 취약점을 저희에게 보고할 것을 권장합니다. RustDesk 프로젝트에서 보안 취약점이 발견되면 info@rustdesk.com 로 이메일을 보내 책임감 있게 보고해 주시기 바랍니다.
저희는 프로젝트의 보안을 매우 중요하게 생각합니다. 모든 사용자가 발견한 취약점을 저희에게 보고할 것을 권장합니다. RustDesk 프로젝트에서 보안 취약점이 발견되면 info@rustdesk.com로 이메일을 보내 책임감 있게 보고해 주시기 바랍니다.
현재로서는 버그 현상금 프로그램이 없습니다. 저희는 큰 문제를 해결하기 위해 노력하는 소규모 팀입니다. 전체 커뮤니티를 위한 안전한 애플리케이션을 계속 구축할 수 있도록 취약점을 책임감 있게 신고해 주시기 바랍니다.
현재로서는 버그 현상금 프로그램이 없습니다. 저희는 큰 문제를 해결하기 위해 노력하는 소규모 팀입니다. 전체 커뮤니티를 위한 안전한 응용 프로그램을 계속 구축할 수 있도록 취약점을 책임감 있게 신고해 주시기 바랍니다.

View File

@@ -1,88 +0,0 @@
# iOS Audio Capture Implementation
## Overview
RustDesk iOS audio capture is implemented following the existing audio service pattern, capturing app audio by default and sending it to peers using the Opus codec.
## Architecture
### Components
1. **Native Layer** (`libs/scrap/src/ios/native/ScreenCapture.m`)
- Captures audio using ReplayKit's audio sample buffers
- Supports both app audio and microphone audio
- Converts audio format information for Rust processing
2. **FFI Layer** (`libs/scrap/src/ios/ffi.rs`)
- Provides safe Rust bindings for audio control
- `enable_audio(mic: bool, app_audio: bool)` - Enable/disable audio sources
- `set_audio_callback()` - Register callback for audio data
3. **Audio Service** (`src/server/audio_service.rs::ios_impl`)
- Follows the same pattern as other platforms
- Uses Opus encoder with 48kHz stereo configuration
- Processes audio in 10ms chunks (480 samples)
- Sends encoded audio as `AudioFrame` messages
## Audio Flow
1. **Capture**: ReplayKit provides audio as Linear PCM in CMSampleBuffer format
2. **Callback**: Native code passes raw PCM data to Rust via FFI callback
3. **Conversion**: Rust converts audio data from i16 to f32 normalized [-1.0, 1.0]
4. **Encoding**: Opus encoder compresses audio for network transmission
5. **Transmission**: Encoded audio sent to peers as protobuf messages
## Configuration
- **Sample Rate**: 48,000 Hz (standard for all platforms)
- **Channels**: 2 (Stereo)
- **Format**: Linear PCM, typically 16-bit
- **Encoder**: Opus with LowDelay application mode
- **Frame Size**: 480 samples (10ms at 48kHz)
## Usage
By default, app audio is captured automatically when screen recording starts:
```rust
// In audio_service.rs
enable_audio(false, true); // mic=false, app_audio=true
```
To enable microphone:
```rust
enable_audio(true, true); // mic=true, app_audio=true
```
## Permissions
- **App Audio**: No additional permission required (part of screen recording)
- **Microphone**: Requires `NSMicrophoneUsageDescription` in Info.plist
## Implementation Details
### Audio Format Handling
The native layer logs audio format on first capture:
```
Audio format - Sample rate: 48000, Channels: 2, Bits per channel: 16, Format: 1819304813
```
### Zero Detection
Like other platforms, implements audio zero gate to avoid sending silent frames:
- Tracks consecutive zero frames
- Stops sending after 800 frames of silence
- Resumes immediately when audio detected
### Thread Safety
- Audio callback runs on ReplayKit's audio queue
- Uses Rust channels for thread-safe communication
- Non-blocking receive in service loop
## Limitations
- Audio only available during active screen capture
- System audio requires Broadcast Upload Extension
- Audio/video synchronization handled separately

View File

@@ -1,336 +0,0 @@
# iOS Screen and Audio Capture Implementation Guide
## Overview
This document describes the complete implementation of screen and audio capture for iOS in RustDesk. The implementation uses Apple's ReplayKit framework through FFI, allowing screen recording with minimal overhead while maintaining compatibility with RustDesk's existing architecture.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ iOS System │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ ReplayKit │ │ Main App │ │ Broadcast Ext. │ │
│ │ │ │ │ │ (System-wide) │ │
│ │ - RPScreen │────▶│ Objective-C │◀───│ │ │
│ │ Recorder │ │ ScreenCapture │ │ SampleHandler │ │
│ │ - Video/Audio │ │ ↓ │ │ │ │
│ └─────────────────┘ │ C Interface │ └────────────────┘ │
│ │ ↓ │ │
│ │ Rust FFI │ │
│ │ ↓ │ │
│ │ Capture/Audio │ │
│ │ Services │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## Directory Structure
```
rustdesk/
├── libs/scrap/src/ios/
│ ├── mod.rs # Rust capture implementation
│ ├── ffi.rs # FFI bindings
│ ├── native/
│ │ ├── ScreenCapture.h # C interface header
│ │ └── ScreenCapture.m # Objective-C implementation
│ └── README.md # iOS-specific documentation
├── flutter/ios/
│ ├── Runner/
│ │ └── Info.plist # Permissions
│ └── BroadcastExtension/ # System-wide capture
│ ├── SampleHandler.h/m # Broadcast extension
│ └── Info.plist # Extension config
└── src/server/
└── audio_service.rs # iOS audio integration
```
## Implementation Components
### 1. Native Layer (Objective-C)
#### ScreenCapture.h - C Interface
```objective-c
// Video capture
void ios_capture_init(void);
bool ios_capture_start(void);
void ios_capture_stop(void);
uint32_t ios_capture_get_frame(uint8_t* buffer, uint32_t buffer_size,
uint32_t* out_width, uint32_t* out_height);
// Audio capture
void ios_capture_set_audio_enabled(bool enable_mic, bool enable_app_audio);
typedef void (*audio_callback_t)(const uint8_t* data, uint32_t size, bool is_mic);
void ios_capture_set_audio_callback(audio_callback_t callback);
// System-wide capture
void ios_capture_show_broadcast_picker(void);
bool ios_capture_is_broadcasting(void);
```
#### ScreenCapture.m - Implementation Details
- Uses `RPScreenRecorder` for in-app capture
- Handles both video and audio sample buffers
- Converts BGRA to RGBA pixel format
- Thread-safe frame buffer management
- CFMessagePort for IPC with broadcast extension
### 2. FFI Layer (Rust)
#### ffi.rs - Safe Rust Bindings
```rust
pub fn init()
pub fn start_capture() -> bool
pub fn stop_capture()
pub fn get_frame() -> Option<(Vec<u8>, u32, u32)>
pub fn enable_audio(mic: bool, app_audio: bool)
pub fn set_audio_callback(callback: Option<extern "C" fn(*const u8, u32, bool)>)
pub fn show_broadcast_picker()
```
Key features:
- Lazy static buffers to reduce allocations
- Callback mechanism for asynchronous frame updates
- Thread-safe frame buffer access
### 3. Rust Capture Implementation
#### mod.rs - Capturer Implementation
```rust
pub struct Capturer {
width: usize,
height: usize,
display: Display,
frame_data: Vec<u8>,
last_frame: Vec<u8>,
}
impl TraitCapturer for Capturer {
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<crate::Frame<'a>>
}
```
Features:
- Implements RustDesk's `TraitCapturer` interface
- Frame deduplication using `would_block_if_equal`
- Automatic cleanup on drop
- Compatible with existing video pipeline
### 4. Audio Service Integration
#### audio_service.rs - iOS Audio Module
```rust
#[cfg(target_os = "ios")]
mod ios_impl {
const SAMPLE_RATE: u32 = 48000;
const CHANNELS: u16 = 2;
const FRAMES_PER_BUFFER: usize = 480; // 10ms
pub struct State {
encoder: Option<Encoder>,
receiver: Option<Receiver<Vec<f32>>>,
// ...
}
}
```
Features:
- Opus encoder with 48kHz stereo
- PCM i16 to f32 conversion
- Zero detection for silence gating
- Non-blocking audio processing
### 5. Broadcast Upload Extension
For system-wide capture (captures other apps):
#### SampleHandler.m
- Runs in separate process
- Captures entire screen
- Sends frames via CFMessagePort to main app
- Memory-efficient frame transfer
## Capture Modes
### 1. In-App Capture (Default)
```rust
// Captures only RustDesk app
let display = Display::primary()?;
let mut capturer = Capturer::new(display)?;
```
### 2. System-Wide Capture
```rust
// Shows iOS broadcast picker
ffi::show_broadcast_picker();
// User must manually start from Control Center
```
## Build Configuration
### Cargo.toml
```toml
[build-dependencies]
cc = "1.0" # For compiling Objective-C
```
### build.rs
```rust
if target_os == "ios" {
cc::Build::new()
.file("src/ios/native/ScreenCapture.m")
.flag("-fobjc-arc")
.flag("-fmodules")
.compile("ScreenCapture");
}
```
### Info.plist Permissions
```xml
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for screen recording with audio</string>
```
## Data Flow
### Video Capture Flow
1. ReplayKit captures screen → CMSampleBuffer
2. Native code converts BGRA → RGBA
3. Frame callback or polling from Rust
4. Rust checks for duplicate frames
5. Creates `Frame::PixelBuffer` for video pipeline
6. Existing video encoder/transmission
### Audio Capture Flow
1. ReplayKit captures app audio → CMSampleBuffer
2. Native extracts Linear PCM data
3. FFI callback to Rust audio service
4. Convert i16 PCM → f32 normalized
5. Opus encoding at 48kHz
6. Send as `AudioFrame` protobuf
## Memory Management
### Optimizations
- Reuse static buffers for frame data (33MB max)
- Lazy allocation based on actual frame size
- Frame deduplication to avoid redundant processing
- Proper synchronization with `@synchronized` blocks
- Weak references in completion handlers
### Cleanup
- `dealloc` method for CFMessagePort cleanup
- Drop implementation stops capture
- Automatic buffer cleanup
## Performance Considerations
### Frame Rate
- 30-60 FPS depending on device
- Frame skipping in broadcast extension (every 2nd frame)
- Non-blocking frame retrieval
### Latency
- In-app: ~2-5ms capture latency
- System-wide: ~10-20ms (IPC overhead)
- Audio: ~10ms chunks for low latency
### CPU Usage
- Hardware-accelerated capture
- Efficient pixel format conversion
- Minimal memory copies
## Security & Privacy
### Permissions Required
- Screen Recording (always required)
- Microphone (optional, for mic audio)
### User Control
- Recording indicator shown by iOS
- User must grant permission
- Can stop anytime from Control Center
### App Groups (for Broadcast Extension)
```
group.com.carriez.rustdesk.screenshare
```
## Integration with RustDesk
### Video Service
- Works with existing `scrap` infrastructure
- Compatible with all video encoders (VP8/9, H264/5)
- Standard frame processing pipeline
### Audio Service
- Integrated as platform-specific implementation
- Same Opus encoding as other platforms
- Compatible with existing audio routing
## Limitations
1. **No cursor capture** - iOS doesn't expose cursor
2. **Permission required** - User must explicitly allow
3. **Broadcast extension memory** - Limited to ~50MB
4. **Background execution** - Limited by iOS policies
## Testing
### Build for iOS
```bash
cd flutter
flutter build ios
```
### Required Setup in Xcode
1. Add Broadcast Upload Extension target
2. Configure app groups
3. Set up code signing
4. Link ReplayKit framework
### Test Scenarios
1. In-app screen capture
2. System-wide broadcast
3. Audio capture (app/mic)
4. Permission handling
5. Background/foreground transitions
## Troubleshooting
### Common Issues
1. **No frames received**
- Check screen recording permission
- Verify capture is started
- Check frame timeout settings
2. **Audio not working**
- Verify microphone permission
- Check audio callback registration
- Confirm audio format compatibility
3. **Broadcast extension not appearing**
- Verify bundle identifiers
- Check code signing
- Ensure extension is included in build
4. **Memory warnings**
- Reduce frame rate in broadcast extension
- Check buffer allocations
- Monitor memory usage
## Future Improvements
1. **Hardware encoding** - Use VideoToolbox for H.264
2. **Adaptive quality** - Adjust based on network/CPU
3. **Picture-in-Picture** - Support PiP mode
4. **Screen orientation** - Better rotation handling
5. **Audio enhancements** - Noise suppression, echo cancellation
## Conclusion
This implementation provides full screen and audio capture capabilities for iOS while maintaining compatibility with RustDesk's cross-platform architecture. The use of FFI minimizes overhead while allowing native iOS features to be accessed from Rust code.

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>RustDesk Screen Broadcast</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.broadcast-services-upload</string>
<key>NSExtensionPrincipalClass</key>
<string>SampleHandler</string>
<key>RPBroadcastProcessMode</key>
<string>RPBroadcastProcessModeSampleBuffer</string>
</dict>
</dict>
</plist>

View File

@@ -1,5 +0,0 @@
#import <ReplayKit/ReplayKit.h>
@interface SampleHandler : RPBroadcastSampleHandler
@end

View File

@@ -1,122 +0,0 @@
#import "SampleHandler.h"
#import <os/log.h>
@interface SampleHandler ()
@property (nonatomic, strong) dispatch_queue_t videoQueue;
@property (nonatomic, assign) CFMessagePortRef messagePort;
@property (nonatomic, assign) BOOL isConnected;
@end
@implementation SampleHandler
- (instancetype)init {
self = [super init];
if (self) {
_videoQueue = dispatch_queue_create("com.rustdesk.broadcast.video", DISPATCH_QUEUE_SERIAL);
_isConnected = NO;
}
return self;
}
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// Create message port to communicate with main app
NSString *portName = @"com.rustdesk.screencast.port";
self.messagePort = CFMessagePortCreateRemote(kCFAllocatorDefault, (__bridge CFStringRef)portName);
if (self.messagePort) {
self.isConnected = YES;
os_log_info(OS_LOG_DEFAULT, "Connected to main app via message port");
} else {
os_log_error(OS_LOG_DEFAULT, "Failed to connect to main app");
[self finishBroadcastWithError:[NSError errorWithDomain:@"com.rustdesk.broadcast"
code:1
userInfo:@{NSLocalizedDescriptionKey: @"Failed to connect to main app"}]];
}
}
- (void)broadcastPaused {
// Handle pause
}
- (void)broadcastResumed {
// Handle resume
}
- (void)broadcastFinished {
if (self.messagePort) {
CFRelease(self.messagePort);
self.messagePort = NULL;
}
self.isConnected = NO;
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
if (!self.isConnected || !self.messagePort) {
return;
}
switch (sampleBufferType) {
case RPSampleBufferTypeVideo:
dispatch_async(self.videoQueue, ^{
[self processVideoSampleBuffer:sampleBuffer];
});
break;
case RPSampleBufferTypeAudioApp:
case RPSampleBufferTypeAudioMic:
// Handle audio if needed
break;
default:
break;
}
}
- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!imageBuffer) {
return;
}
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
if (baseAddress) {
// Create a header with frame info
struct FrameHeader {
uint32_t width;
uint32_t height;
uint32_t dataSize;
} header = {
.width = (uint32_t)width,
.height = (uint32_t)height,
.dataSize = (uint32_t)(width * height * 4) // Always RGBA format
};
// Send header first
CFDataRef headerData = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)&header, sizeof(header));
if (headerData) {
SInt32 result = CFMessagePortSendRequest(self.messagePort, 1, headerData, 1.0, 0.0, NULL, NULL);
CFRelease(headerData);
if (result == kCFMessagePortSuccess) {
// Send frame data
CFDataRef frameData = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)baseAddress, header.dataSize);
if (frameData) {
CFMessagePortSendRequest(self.messagePort, 2, frameData, 1.0, 0.0, NULL, NULL);
CFRelease(frameData);
}
}
}
}
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
}
@end

View File

@@ -70,8 +70,6 @@
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to get QR codes from image</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for screen recording with audio</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>

View File

@@ -13,11 +13,13 @@ import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:provider/provider.dart';
import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -42,6 +44,7 @@ import 'package:flutter_hbb/native/win32.dart'
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
import 'package:flutter_hbb/native/common.dart'
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
import 'package:http/http.dart' as http;
final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey();
@@ -74,6 +77,9 @@ bool _ignoreDevicePixelRatio = true;
int windowsBuildNumber = 0;
DesktopType? desktopType;
// Tolerance used for floating-point position comparisons to avoid precision errors.
const double _kPositionEpsilon = 1e-6;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
@@ -105,6 +111,10 @@ enum DesktopType {
portForward,
}
bool isDoubleEqual(double a, double b) {
return (a - b).abs() < _kPositionEpsilon;
}
class IconFont {
static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar';
@@ -1583,7 +1593,9 @@ String bool2option(String option, bool b) {
option == kOptionForceAlwaysRelay) {
res = b ? 'Y' : defaultOptionNo;
} else {
assert(false);
if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) {
assert(false);
}
res = b ? 'Y' : 'N';
}
return res;
@@ -1619,7 +1631,8 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
// Because all session options use `Y` and `<Empty>` as values.
Future<bool> matchPeer(String searchText, Peer peer) async {
Future<bool> matchPeer(
String searchText, Peer peer, PeerTabIndex peerTabIndex) async {
if (searchText.isEmpty) {
return true;
}
@@ -1630,11 +1643,14 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
peer.username.toLowerCase().contains(searchText)) {
return true;
}
final alias = peer.alias;
if (alias.isEmpty) {
return false;
if (peer.alias.toLowerCase().contains(searchText)) {
return true;
}
return alias.toLowerCase().contains(searchText);
if (peerTabShowNote(peerTabIndex) &&
peer.note.toLowerCase().contains(searchText)) {
return true;
}
return false;
}
/// Get the image for the current [platform].
@@ -1664,6 +1680,16 @@ class LastWindowPosition {
LastWindowPosition(this.width, this.height, this.offsetWidth,
this.offsetHeight, this.isMaximized, this.isFullscreen);
bool equals(LastWindowPosition other) {
return (
(width == other.width) &&
(height == other.height) &&
(offsetWidth == other.offsetWidth) &&
(offsetHeight == other.offsetHeight) &&
(isMaximized == other.isMaximized) &&
(isFullscreen == other.isFullscreen));
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
"width": width,
@@ -1703,9 +1729,14 @@ String get windowFramePrefix =>
? "incoming_"
: (bind.isOutgoingOnly() ? "outgoing_" : ""));
typedef WindowKey = ({WindowType type, int? windowId});
LastWindowPosition? _lastWindowPosition = null;
final Debouncer _saveWindowDebounce = Debouncer(delay: Duration(seconds: 1));
/// Save window position and size on exit
/// Note that windowId must be provided if it's subwindow
Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
Future<void> saveWindowPosition(WindowType type, {int? windowId, bool? flush}) async {
if (type != WindowType.Main && windowId == null) {
debugPrint(
"Error: windowId cannot be null when saving positions for sub window");
@@ -1774,16 +1805,40 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
final pos = LastWindowPosition(
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}, isFullscreen:${pos.isFullscreen}");
await bind.setLocalFlutterOption(
k: windowFramePrefix + type.name, v: pos.toString());
final WindowKey key = (type: type, windowId: windowId);
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
windowId != null) {
await _saveSessionWindowPosition(
type, windowId, isMaximized, isFullscreen, pos);
final bool haveNewWindowPosition = (_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
if (haveNewWindowPosition || isPreviousNewWindowPositionPending) {
_lastWindowPosition = pos;
if (flush ?? false) {
// If a previous update is pending, replace it.
_saveWindowDebounce.cancel();
await _saveWindowPositionActual(key);
} else if (haveNewWindowPosition) {
_saveWindowDebounce.call(() => _saveWindowPositionActual(key));
}
}
}
Future<void> _saveWindowPositionActual(WindowKey key) async {
LastWindowPosition? pos = _lastWindowPosition;
if (pos != null) {
debugPrint(
"Saving frame: ${key.windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
await bind.setLocalFlutterOption(
k: windowFramePrefix + key.type.name, v: pos.toString());
if ((key.type == WindowType.RemoteDesktop || key.type == WindowType.ViewCamera) &&
key.windowId != null) {
await _saveSessionWindowPosition(
key.type, key.windowId!, pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
}
}
}
@@ -1849,6 +1904,8 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
return Size(restoreWidth, restoreHeight);
}
// Consider using Rect.contains() instead,
// though the implementation is not exactly the same.
bool isPointInRect(Offset point, Rect rect) {
return point.dx >= rect.left &&
point.dx <= rect.right &&
@@ -1946,8 +2003,24 @@ Future<bool> restoreWindowPosition(WindowType type,
var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) {
debugPrint("no window position saved, ignoring position restoration");
return false;
debugPrint("No window position saved, trying to center the window.");
switch (type) {
case WindowType.Main:
// Center the main window only if no position is saved (on first run).
if (isWindows || isLinux) {
await windowManager.center();
}
// For MacOS, the window is already centered by default.
// See https://github.com/rustdesk/rustdesk/blob/9b9276e7524523d7f667fefcd0694d981443df0e/flutter/macos/Runner/Base.lproj/MainMenu.xib#L333
// If `<windowPositionMask>` in `<window>` is not set, the window will be centered.
break;
default:
// No need to change the position of a sub window if no position is saved,
// since the default position is already centered.
// https://github.com/rustdesk/rustdesk/blob/317639169359936f7f9f85ef445ec9774218772d/flutter/lib/utils/multi_window_manager.dart#L163
break;
}
return true;
}
if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
if (!isRemotePeerPos && windowId != null) {
@@ -2124,6 +2197,10 @@ enum UriLinkType {
terminal,
}
setEnvTerminalAdmin() {
bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y');
}
// uri link handler
bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
List<String>? args;
@@ -2191,6 +2268,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
id = args[i + 1];
i++;
break;
case '--terminal-admin':
setEnvTerminalAdmin();
type = UriLinkType.terminal;
id = args[i + 1];
i++;
break;
case '--password':
password = args[i + 1];
i++;
@@ -2264,7 +2347,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
"view-camera",
"port-forward",
"rdp",
"terminal"
"terminal",
"terminal-admin",
];
if (uri.authority.isEmpty &&
uri.path.split('').every((char) => char == '/')) {
@@ -2334,6 +2418,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
} else if (command == '--terminal') {
connect(Get.context!, id,
isTerminal: true, forceRelay: forceRelay, password: password);
} else if (command == 'terminal-admin') {
setEnvTerminalAdmin();
connect(Get.context!, id,
isTerminal: true, forceRelay: forceRelay, password: password);
} else {
// Default to remote desktop for '--connect', '--play', or direct connection
connect(Get.context!, id, forceRelay: forceRelay, password: password);
@@ -2736,7 +2824,7 @@ class ServerConfig {
} catch (err) {
final input = msg.split('').reversed.join('');
final bytes = base64Decode(base64.normalize(input));
json = jsonDecode(utf8.decode(bytes));
json = jsonDecode(utf8.decode(bytes, allowMalformed: true));
}
idServer = json['host'] ?? '';
relayServer = json['relay'] ?? '';
@@ -3893,3 +3981,39 @@ String get appName {
}
return _appName;
}
String getConnectionText(bool secure, bool direct, String streamType) {
String connectionText;
if (secure && direct) {
connectionText = translate("Direct and encrypted connection");
} else if (secure && !direct) {
connectionText = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
connectionText = translate("Direct and unencrypted connection");
} else {
connectionText = translate("Relayed and unencrypted connection");
}
if (streamType == 'Relay') {
streamType = 'TCP';
}
if (streamType.isEmpty) {
return connectionText;
} else {
return '$connectionText ($streamType)';
}
}
String decode_http_response(http.Response resp) {
try {
// https://github.com/rustdesk/rustdesk-server-pro/discussions/758
return utf8.decode(resp.bodyBytes, allowMalformed: true);
} catch (e) {
debugPrint('Failed to decode response as UTF-8: $e');
// Fallback to bodyString which handles encoding automatically
return resp.body;
}
}
bool peerTabShowNote(PeerTabIndex peerTabIndex) {
return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group;
}

View File

@@ -89,6 +89,7 @@ class PeerPayload {
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
"device_group_name": p.device_group_name,
"note": p.note,
});
}
@@ -248,15 +249,17 @@ class AbProfile {
String name;
String owner;
String? note;
dynamic info;
int rule;
AbProfile(this.guid, this.name, this.owner, this.note, this.rule);
AbProfile(this.guid, this.name, this.owner, this.note, this.rule, this.info);
AbProfile.fromJson(Map<String, dynamic> json)
: guid = json['guid'] ?? '',
name = json['name'] ?? '',
owner = json['owner'] ?? '',
note = json['note'] ?? '',
info = json['info'],
rule = json['rule'] ?? 0;
}

View File

@@ -77,9 +77,11 @@ class CurrentDisplayState {
class ConnectionType {
final Rx<String> _secure = kInvalidValueStr.obs;
final Rx<String> _direct = kInvalidValueStr.obs;
final Rx<String> _stream_type = kInvalidValueStr.obs;
Rx<String> get secure => _secure;
Rx<String> get direct => _direct;
Rx<String> get stream_type => _stream_type;
static String get strSecure => 'secure';
static String get strInsecure => 'insecure';
@@ -94,9 +96,14 @@ class ConnectionType {
_direct.value = v ? strDirect : strIndirect;
}
void setStreamType(String v) {
_stream_type.value = v;
}
bool isValid() {
return _secure.value != kInvalidValueStr &&
_direct.value != kInvalidValueStr;
_direct.value != kInvalidValueStr &&
_stream_type.value != kInvalidValueStr;
}
}

View File

@@ -466,6 +466,7 @@ class _AddressBookState extends State<AddressBook> {
IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: '');
TextEditingController passwordController = TextEditingController(text: '');
TextEditingController noteController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.currentAbTags);
var selectedTag = List<dynamic>.empty(growable: true).obs;
final style = TextStyle(fontSize: 14.0);
@@ -494,7 +495,11 @@ class _AddressBookState extends State<AddressBook> {
password = passwordController.text;
}
String? errMsg2 = await gFFI.abModel.addIdToCurrent(
id, aliasController.text.trim(), password, selectedTag);
id,
aliasController.text.trim(),
password,
selectedTag,
noteController.text);
if (errMsg2 != null) {
setState(() {
isInProgress = false;
@@ -600,6 +605,24 @@ class _AddressBookState extends State<AddressBook> {
),
).workaroundFreezeLinuxMint(),
)),
row(
label: Text(
translate('Note'),
style: style,
),
input: Obx(
() => TextField(
controller: noteController,
maxLines: 3,
minLines: 1,
maxLength: 300,
decoration: InputDecoration(
labelText: stateGlobal.isPortrait.isFalse
? null
: translate('Note'),
),
).workaroundFreezeLinuxMint(),
)),
if (gFFI.abModel.currentAbTags.isNotEmpty)
Align(
alignment: Alignment.centerLeft,

View File

@@ -819,23 +819,33 @@ void enterPasswordDialog(
}
void enterUserLoginDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async {
SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
await _connectDialog(
sessionId,
dialogManager,
osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
);
}
void enterUserLoginAndPasswordDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async {
SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
await _connectDialog(
sessionId,
dialogManager,
osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(),
passwordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
);
}
@@ -845,17 +855,28 @@ _connectDialog(
TextEditingController? osUsernameController,
TextEditingController? osPasswordController,
TextEditingController? passwordController,
String? osAccountDescTip,
bool canRememberAccount = true,
}) async {
final errUsername = ''.obs;
var rememberPassword = false;
if (passwordController != null) {
rememberPassword =
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
}
var rememberAccount = false;
if (osUsernameController != null) {
if (canRememberAccount && osUsernameController != null) {
rememberAccount =
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
}
if (osUsernameController != null) {
osUsernameController.addListener(() {
if (errUsername.value.isNotEmpty) {
errUsername.value = '';
}
});
}
dialogManager.dismissAll();
dialogManager.show((setState, close, context) {
cancel() {
@@ -864,6 +885,13 @@ _connectDialog(
}
submit() {
if (osUsernameController != null) {
if (osUsernameController.text.trim().isEmpty) {
errUsername.value = translate('Empty Username');
setState(() {});
return;
}
}
final osUsername = osUsernameController?.text.trim() ?? '';
final osPassword = osPasswordController?.text.trim() ?? '';
final password = passwordController?.text.trim() ?? '';
@@ -927,26 +955,39 @@ _connectDialog(
}
return Column(
children: [
descWidget(translate('login_linux_tip')),
if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)),
DialogTextField(
title: translate(DialogTextField.kUsernameTitle),
controller: osUsernameController,
prefixIcon: DialogTextField.kUsernameIcon,
errorText: null,
),
if (errUsername.value.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: SelectableText(
errUsername.value,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
textAlign: TextAlign.left,
).paddingOnly(left: 12, bottom: 2),
),
PasswordWidget(
controller: osPasswordController,
autoFocus: false,
),
rememberWidget(
translate('remember_account_tip'),
rememberAccount,
(v) {
if (v != null) {
setState(() => rememberAccount = v);
}
},
),
if (canRememberAccount)
rememberWidget(
translate('remember_account_tip'),
rememberAccount,
(v) {
if (v != null) {
setState(() => rememberAccount = v);
}
},
),
],
);
}
@@ -1136,7 +1177,7 @@ void showRequestElevationDialog(
DialogTextField(
controller: userController,
title: translate('Username'),
hintText: translate('eg: admin'),
hintText: translate('elevation_username_tip'),
prefixIcon: DialogTextField.kUsernameIcon,
errorText: errUser.isEmpty ? null : errUser.value,
),
@@ -1742,6 +1783,49 @@ void editAbTagDialog(
});
}
void editAbPeerNoteDialog(String id) {
var isInProgress = false;
final currentNote = gFFI.abModel.getPeerNote(id);
var controller = TextEditingController(text: currentNote);
gFFI.dialogManager.show((setState, close, context) {
submit() async {
setState(() {
isInProgress = true;
});
await gFFI.abModel.changeNote(id: id, note: controller.text);
close();
}
return CustomAlertDialog(
title: Text(translate("Edit note")),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: controller,
autofocus: true,
maxLines: 3,
minLines: 1,
maxLength: 300,
decoration: InputDecoration(
labelText: translate('Note'),
),
).workaroundFreezeLinuxMint(),
// NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress) const LinearProgressIndicator(),
],
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
void renameDialog(
{required String oldName,
FormFieldValidator<String>? validator,

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
enum GestureState {
none,
@@ -96,6 +97,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
if (onTwoFingerScaleEnd != null) {
onTwoFingerScaleEnd!(d);
}
if (isSpecialHoldDragActive) {
// If we are in special drag mode, we need to reset the state.
// Otherwise, the next `onTwoFingerScaleUpdate()` will handle a wrong `focalPoint`.
_currentState = GestureState.none;
return;
}
break;
case GestureState.threeFingerVerticalDrag:
debugPrint("ThreeFingerState.vertical onEnd");

View File

@@ -127,6 +127,10 @@ class _PeerCardState extends State<_PeerCard>
);
}
bool _showNote(Peer peer) {
return peerTabShowNote(widget.tab) && peer.note.isNotEmpty;
}
makeChild(bool isPortrait, Peer peer) {
final name = hideUsernameOnCard == true
? peer.hostname
@@ -134,6 +138,8 @@ class _PeerCardState extends State<_PeerCard>
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final showNote = _showNote(peer);
return Row(
mainAxisSize: MainAxisSize.max,
children: [
@@ -185,14 +191,44 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall,
)),
]).marginOnly(top: isPortrait ? 0 : 2),
Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Flexible(
child: Tooltip(
message: name,
waitDuration: const Duration(seconds: 1),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
name,
style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
),
),
),
),
if (showNote)
Expanded(
child: Tooltip(
message: peer.note,
waitDuration: const Duration(seconds: 1),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
peer.note,
style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
).marginOnly(
left: peerCardUiType.value ==
PeerUiType.list
? 32
: 4),
),
),
)
],
),
],
).marginOnly(top: 2),
@@ -278,7 +314,7 @@ class _PeerCardState extends State<_PeerCard>
padding: const EdgeInsets.all(6),
child:
getPlatformImage(peer.platform, size: 60),
).marginOnly(top: 4),
),
Row(
children: [
Expanded(
@@ -297,8 +333,26 @@ class _PeerCardState extends State<_PeerCard>
),
],
),
if (_showNote(peer))
Row(
children: [
Expanded(
child: Tooltip(
message: peer.note,
waitDuration: const Duration(seconds: 1),
child: Text(
peer.note,
style: const TextStyle(
color: Colors.white38,
fontSize: 10),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
))
],
),
],
).paddingAll(4.0),
).paddingOnly(top: 4.0, left: 4.0, right: 4.0),
),
],
),
@@ -492,6 +546,7 @@ abstract class BasePeerCard extends StatelessWidget {
bool isTcpTunneling = false,
bool isRDP = false,
bool isTerminal = false,
bool isTerminalRunAsAdmin = false,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@@ -499,6 +554,9 @@ abstract class BasePeerCard extends StatelessWidget {
style: style,
),
proc: () {
if (isTerminalRunAsAdmin) {
setEnvTerminalAdmin();
}
connectInPeerTab(
context,
peer,
@@ -507,7 +565,7 @@ abstract class BasePeerCard extends StatelessWidget {
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
isTerminal: isTerminal,
isTerminal: isTerminal || isTerminalRunAsAdmin,
);
},
padding: menuPadding,
@@ -547,11 +605,20 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase<String> _terminalAction(BuildContext context) {
return _connectCommonAction(
context,
translate('Terminal'),
'${translate('Terminal')} (beta)',
isTerminal: true,
);
}
@protected
MenuEntryBase<String> _terminalRunAsAdminAction(BuildContext context) {
return _connectCommonAction(
context,
'${translate('Terminal (Run as administrator)')} (beta)',
isTerminalRunAsAdmin: true,
);
}
@protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction(
@@ -906,6 +973,10 @@ class RecentPeerCard extends BasePeerCard {
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
@@ -966,6 +1037,11 @@ class FavoritePeerCard extends BasePeerCard {
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
@@ -1022,6 +1098,10 @@ class DiscoveredPeerCard extends BasePeerCard {
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
@@ -1076,6 +1156,11 @@ class AddressBookPeerCard extends BasePeerCard {
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
@@ -1103,6 +1188,7 @@ class AddressBookPeerCard extends BasePeerCard {
if (gFFI.abModel.currentAbTags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id));
}
menuItems.add(_editNoteAction(peer.id));
}
final addressbooks = gFFI.abModel.addressBooksCanWrite();
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
@@ -1142,6 +1228,21 @@ class AddressBookPeerCard extends BasePeerCard {
);
}
@protected
MenuEntryBase<String> _editNoteAction(String id) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Edit note'),
style: style,
),
proc: () {
editAbPeerNoteDialog(id);
},
padding: super.menuPadding,
dismissOnClicked: true,
);
}
@protected
@override
Future<String> _getAlias(String id) async =>
@@ -1212,6 +1313,11 @@ class MyGroupPeerCard extends BasePeerCard {
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
@@ -1455,6 +1561,13 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
password = peer.password;
isSharedPassword = true;
}
if (password.isEmpty) {
final abPassword = gFFI.abModel.getdefaultSharedPassword();
if (abPassword != null) {
password = abPassword;
isSharedPassword = true;
}
}
}
}
connect(context, peer.id,

View File

@@ -71,10 +71,12 @@ class _PeersView extends StatefulWidget {
final Peers peers;
final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder;
final PeerTabIndex peerTabIndex;
const _PeersView(
{required this.peers,
required this.peerCardBuilder,
required this.peerTabIndex,
this.peerFilter,
Key? key})
: super(key: key);
@@ -395,8 +397,8 @@ class _PeersViewState extends State<_PeersView>
return peers;
}
searchText = searchText.toLowerCase();
final matches =
await Future.wait(peers.map((peer) => matchPeer(searchText, peer)));
final matches = await Future.wait(
peers.map((peer) => matchPeer(searchText, peer, widget.peerTabIndex)));
final filteredList = List<Peer>.empty(growable: true);
for (var i = 0; i < peers.length; i++) {
if (matches[i]) {
@@ -441,7 +443,10 @@ abstract class BasePeersView extends StatelessWidget {
break;
}
return _PeersView(
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
peers: peers,
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder,
peerTabIndex: peerTabIndex);
}
}

View File

@@ -51,6 +51,13 @@ class RawKeyFocusScope extends StatelessWidget {
}
}
// For virtual mouse when using the mouse mode on mobile.
// Special hold-drag mode: one finger holds a button (left/right button), another finger pans.
// This flag is to override the scale gesture to a pan gesture.
bool isSpecialHoldDragActive = false;
// Cache the last focal point to calculate deltas in special hold-drag mode.
Offset _lastSpecialHoldDragFocalPoint = Offset.zero;
class RawTouchGestureDetectorRegion extends StatefulWidget {
final Widget child;
final FFI ffi;
@@ -97,6 +104,10 @@ class _RawTouchGestureDetectorRegionState
bool _touchModePanStarted = false;
Offset _doubleFinerTapPosition = Offset.zero;
// For mouse mode, we need to block the events when the cursor is in a blocked area.
// So we need to cache the last tap down position.
Offset? _lastTapDownPositionForMouseMode;
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel;
@@ -112,7 +123,15 @@ class _RawTouchGestureDetectorRegionState
}
bool isNotTouchBasedDevice() {
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
}
// Mobile, mouse mode.
// Check if should block the mouse tap event (`_lastTapDownPositionForMouseMode`).
bool shouldBlockMouseModeEvent() {
return _lastTapDownPositionForMouseMode != null &&
ffi.cursorModel.shouldBlock(_lastTapDownPositionForMouseMode!.dx,
_lastTapDownPositionForMouseMode!.dy);
}
onTapDown(TapDownDetails d) async {
@@ -124,6 +143,8 @@ class _RawTouchGestureDetectorRegionState
_lastPosOfDoubleTapDown = d.localPosition;
// Desktop or mobile "Touch mode"
_lastTapDownDetails = d;
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
}
}
@@ -150,6 +171,11 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
// Using `_lastTapDownPositionForMouseMode` instead.
if (shouldBlockMouseModeEvent()) {
return;
}
// Mobile, "Mouse mode"
await inputModel.tap(MouseButtons.left);
}
@@ -163,6 +189,8 @@ class _RawTouchGestureDetectorRegionState
if (handleTouch) {
_lastPosOfDoubleTapDown = d.localPosition;
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
}
}
@@ -177,6 +205,12 @@ class _RawTouchGestureDetectorRegionState
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
return;
}
// Check if the position is in a blocked area when using the mouse mode.
if (!handleTouch) {
if (shouldBlockMouseModeEvent()) {
return;
}
}
await inputModel.tap(MouseButtons.left);
await inputModel.tap(MouseButtons.left);
}
@@ -198,6 +232,8 @@ class _RawTouchGestureDetectorRegionState
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
await inputModel.tapDown(MouseButtons.left);
}
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
}
}
@@ -222,6 +258,10 @@ class _RawTouchGestureDetectorRegionState
if (!isMoved) {
return;
}
} else {
if (shouldBlockMouseModeEvent()) {
return;
}
}
await inputModel.tap(MouseButtons.right);
} else {
@@ -274,6 +314,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
if (isSpecialHoldDragActive) return;
await inputModel.sendMouse('down', MouseButtons.left);
}
}
@@ -283,6 +324,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
if (isSpecialHoldDragActive) return;
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
}
@@ -377,12 +419,26 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
if (isSpecialHoldDragActive) {
// Initialize the last focal point to calculate deltas manually.
_lastSpecialHoldDragFocalPoint = d.focalPoint;
}
}
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
if (isNotTouchBasedDevice()) {
return;
}
// If in special drag mode, perform a pan instead of a scale.
if (isSpecialHoldDragActive) {
// Calculate delta manually to avoid the jumpy behavior.
final delta = d.focalPoint - _lastSpecialHoldDragFocalPoint;
_lastSpecialHoldDragFocalPoint = d.focalPoint;
await ffi.cursorModel.updatePan(delta * 2.0, d.focalPoint, handleTouch);
return;
}
if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale;
@@ -420,7 +476,9 @@ class _RawTouchGestureDetectorRegionState
// No idea why we need to set the view style to "" here.
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
}
await inputModel.sendMouse('up', MouseButtons.left);
if (!isSpecialHoldDragActive) {
await inputModel.sendMouse('up', MouseButtons.left);
}
}
get onHoldDragCancel => null;

View File

@@ -230,7 +230,6 @@ List<(String, String)> otherDefaultSettings() {
('Disable clipboard', kOptionDisableClipboard),
('Lock after session end', kOptionLockAfterSessionEnd),
('Privacy mode', kOptionPrivacyMode),
if (isMobile) ('Touch mode', kOptionTouchMode),
('True color (4:4:4)', kOptionI444),
('Reverse mouse wheel', kKeyReverseMouseWheel),
('swap-left-right-mouse', kOptionSwapLeftRightMouse),

View File

@@ -183,7 +183,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
);
v.add(
TTextMenu(
child: Text(translate('Terminal')),
child: Text('${translate('Terminal')} (beta)'),
onPressed: () => connectWithToken(isTerminal: true)),
);
v.add(
@@ -363,6 +363,11 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
child: Text(translate('Scale adaptive')),
value: kRemoteViewStyleAdaptive,
groupValue: groupValue,
onChanged: onChanged),
TRadioMenu<String>(
child: Text(translate('Scale custom')),
value: kRemoteViewStyleCustom,
groupValue: groupValue,
onChanged: onChanged)
];
}

View File

@@ -64,6 +64,7 @@ const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewViewCamera = "new_view_camera";
const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventNewTerminal = "new_terminal";
const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions";
const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list";
@@ -154,6 +155,9 @@ const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
const String kOptionEnableUdpPunch = "enable-udp-punch";
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
const String kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
// network options
const String kOptionAllowWebSocket = "allow-websocket";
@@ -171,6 +175,7 @@ const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only";
const String kOptionToggleShowMyCursor = "show-my-cursor";
const String kOptionDisableFloatingWindow = "disable-floating-window";
@@ -311,6 +316,10 @@ const kRemoteViewStyleOriginal = 'original';
/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor.
const kRemoteViewStyleAdaptive = 'adaptive';
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
const kRemoteViewStyleCustom = 'custom';
/// [kRemoteScrollStyleAuto] Scroll image auto by position.
const kRemoteScrollStyleAuto = 'scrollauto';
@@ -343,6 +352,15 @@ const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
PointerDeviceKind.invertedStylus,
};
// Scale custom related constants
const String kCustomScalePercentKey = 'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
const int kScaleCustomMinPercent = 5;
const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track
const int kScaleCustomMaxPercent = 1000;
const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100%
const double kScaleCustomDetentEpsilon = 0.006; // snap range around pivot (~0.6%)
const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300);
// ================================ mobile ================================
// Magic numbers, maybe need to avoid it or use a better way to get them.

View File

@@ -374,6 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
rdpUsername: '',
loginName: '',
device_group_name: '',
note: '',
);
_autocompleteOpts = [emptyPeer];
} else {
@@ -536,64 +537,68 @@ class _ConnectionPageState extends State<ConnectionPage>
builder: (context, setState) {
var offset = Offset(0, 0);
return Obx(() => InkWell(
child: _menuOpen.value
? Transform.rotate(
angle: pi,
child: Icon(IconFont.more, size: 14),
child: _menuOpen.value
? Transform.rotate(
angle: pi,
child: Icon(IconFont.more, size: 14),
)
: Icon(IconFont.more, size: 14),
onTapDown: (e) {
offset = e.globalPosition;
},
onTap: () async {
_menuOpen.value = true;
final x = offset.dx;
final y = offset.dy;
await mod_menu
.showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: [
(
'Transfer file',
() => onConnect(isFileTransfer: true)
),
(
'View camera',
() => onConnect(isViewCamera: true)
),
(
'${translate('Terminal')} (beta)',
() => onConnect(isTerminal: true)
),
]
.map((e) => MenuEntryButton<String>(
childBuilder: (TextStyle? style) =>
Text(
translate(e.$1),
style: style,
),
proc: () => e.$2(),
padding: EdgeInsets.symmetric(
horizontal:
kDesktopMenuPadding.left),
dismissOnClicked: true,
))
.map((e) => e.build(
context,
const MenuConfig(
commonColor: CustomPopupMenuTheme
.commonColor,
height:
CustomPopupMenuTheme.height,
dividerHeight:
CustomPopupMenuTheme
.dividerHeight)))
.expand((i) => i)
.toList(),
elevation: 8,
)
: Icon(IconFont.more, size: 14),
onTapDown: (e) {
offset = e.globalPosition;
},
onTap: () async {
_menuOpen.value = true;
final x = offset.dx;
final y = offset.dy;
await mod_menu
.showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: [
(
'Transfer file',
() => onConnect(isFileTransfer: true)
),
(
'View camera',
() => onConnect(isViewCamera: true)
),
(
'Terminal',
() => onConnect(isTerminal: true)
),
]
.map((e) => MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate(e.$1),
style: style,
),
proc: () => e.$2(),
padding: EdgeInsets.symmetric(
horizontal: kDesktopMenuPadding.left),
dismissOnClicked: true,
))
.map((e) => e.build(
context,
const MenuConfig(
commonColor:
CustomPopupMenuTheme.commonColor,
height: CustomPopupMenuTheme.height,
dividerHeight: CustomPopupMenuTheme
.dividerHeight)))
.expand((i) => i)
.toList(),
elevation: 8,
)
.then((_) {
_menuOpen.value = false;
});
},
));
.then((_) {
_menuOpen.value = false;
});
},
));
},
),
),

View File

@@ -434,7 +434,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
!isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) {
final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled();
String btnText = isToUpdate ? 'Click to update' : 'Click to download';
String btnText = isToUpdate ? 'Update' : 'Download';
GestureTapCallback onPressed = () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);

View File

@@ -1522,9 +1522,8 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy =
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
// final hideWebSocket = isWeb ||
// bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y';
final hideWebSocket = true;
final hideWebSocket = isWeb ||
bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y';
if (hideServer && hideProxy && hideWebSocket) {
return Offstage();

View File

@@ -146,16 +146,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
String msgConn = getConnectionText(
secure, direct, connectionType.stream_type.value);
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {

View File

@@ -124,7 +124,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
},
setter: (bool v) async {
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
bind.sessionToggleOption(
await bind.sessionToggleOption(
sessionId: ffi.sessionId,
value: kOptionTerminalPersistent,
);
@@ -171,10 +171,24 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
forceRelay: args['forceRelay'],
connToken: args['connToken'],
));
} else if (call.method == kWindowEventRestoreTerminalSessions) {
_restoreSessions(call.arguments);
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
if (tabController.state.value.tabs.isEmpty) {
return false;
}
final currentTab = tabController.state.value.selectedTabInfo;
assert(call.arguments is String,
"Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
if (currentTab.key.startsWith(call.arguments)) {
windowOnTop(windowId());
return true;
}
return false;
}
});
Future.delayed(Duration.zero, () {
@@ -188,6 +202,32 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
super.dispose();
}
Future<void> _restoreSessions(String arguments) async {
Map<String, dynamic>? args;
try {
args = jsonDecode(arguments) as Map<String, dynamic>;
} catch (e) {
debugPrint("Error parsing JSON arguments in _restoreSessions: $e");
return;
}
final persistentSessions =
args['persistent_sessions'] as List<dynamic>? ?? [];
final sortedSessions = persistentSessions.whereType<int>().toList()..sort();
for (final terminalId in sortedSessions) {
_addNewTerminalForCurrentPeer(terminalId: terminalId);
// A delay is required to ensure the UI has sufficient time to update
// before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
// may be called prematurely while the tab widget is still in the tab controller.
// This behavior is likely due to a race condition between the UI rendering lifecycle
// and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()`
// to wait for the previous page to be ready were unsuccessful, as the observed call sequence is:
// `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`.
// The `Future.delayed` approach mitigates this issue by introducing a buffer period,
// allowing the UI to stabilize before proceeding.
await Future.delayed(const Duration(milliseconds: 300));
}
}
bool _handleKeyEvent(KeyEvent event) {
if (event is KeyDownEvent) {
// Use Cmd+T on macOS, Ctrl+Shift+T on other platforms
@@ -276,17 +316,20 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
return false;
}
void _addNewTerminal(String peerId) {
void _addNewTerminal(String peerId, {int? terminalId}) {
// Find first tab for this peer to get connection parameters
final firstTab = tabController.state.value.tabs.firstWhere(
(tab) => tab.key.startsWith('$peerId\_'),
);
if (firstTab.page is TerminalPage) {
final page = firstTab.page as TerminalPage;
final terminalId = _nextTerminalId++;
final newTerminalId = terminalId ?? _nextTerminalId++;
if (terminalId != null && terminalId >= _nextTerminalId) {
_nextTerminalId = terminalId + 1;
}
tabController.add(_createTerminalTab(
peerId: peerId,
terminalId: terminalId,
terminalId: newTerminalId,
password: page.password,
isSharedPassword: page.isSharedPassword,
forceRelay: page.forceRelay,
@@ -295,12 +338,12 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
}
}
void _addNewTerminalForCurrentPeer() {
void _addNewTerminalForCurrentPeer({int? terminalId}) {
final currentTab = tabController.state.value.selectedTabInfo;
final parts = currentTab.key.split('_');
if (parts.isNotEmpty) {
final peerId = parts[0];
_addNewTerminal(peerId);
_addNewTerminal(peerId, terminalId: terminalId);
}
}

View File

@@ -145,16 +145,8 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
String msgConn = getConnectionText(
secure, direct, connectionType.stream_type.value);
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {

View File

@@ -25,6 +25,7 @@ import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './kb_layout_type_chooser.dart';
import 'package:flutter_hbb/utils/scale.dart';
class ToolbarState {
late RxBool _pin;
@@ -175,6 +176,12 @@ class RemoteMenuEntry {
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
MenuEntryRadioOption(
text: translate('Scale custom'),
value: kRemoteViewStyleCustom,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
],
curOptionGetter: () async {
// null means peer id is not found, which there's no need to care about
@@ -1024,6 +1031,7 @@ class _DisplayMenu extends StatefulWidget {
}
class _DisplayMenuState extends State<_DisplayMenu> {
final RxInt _customPercent = 100.obs;
late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
id: widget.id,
ffi: widget.ffi,
@@ -1037,13 +1045,27 @@ class _DisplayMenuState extends State<_DisplayMenu> {
FFI get ffi => widget.ffi;
String get id => widget.id;
@override
void initState() {
super.initState();
// Initialize custom percent from stored option once
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
final v = await getSessionCustomScalePercent(widget.ffi.sessionId);
if (_customPercent.value != v) {
_customPercent.value = v;
}
} catch (_) {}
});
}
@override
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
menuChildrenGetter() {
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
viewStyle(),
viewStyle(customPercent: _customPercent),
scrollStyle(),
imageQuality(),
codec(),
@@ -1108,30 +1130,69 @@ class _DisplayMenuState extends State<_DisplayMenu> {
);
}
viewStyle() {
viewStyle({required RxInt customPercent}) {
return futureBuilder(
future: toolbarViewStyle(context, widget.id, widget.ffi),
hasData: (data) {
final v = data as List<TRadioMenu<String>>;
final bool isCustomSelected = v.isNotEmpty
? v.first.groupValue == kRemoteViewStyleCustom
: false;
return Column(children: [
...v
.map((e) => RdoMenuButton<String>(
value: e.value,
groupValue: e.groupValue,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList(),
Divider(),
...v.map((e) {
final isCustom = e.value == kRemoteViewStyleCustom;
final child = isCustom
? Text(translate('Scale custom'))
: e.child;
// Whether the current selection is already custom
final bool isGroupCustomSelected =
e.groupValue == kRemoteViewStyleCustom;
// Keep menu open when switching INTO custom so the slider is visible immediately
final bool keepOpenForThisItem = isCustom && !isGroupCustomSelected;
return RdoMenuButton<String>(
value: e.value,
groupValue: e.groupValue,
onChanged: (value) {
// Perform the original change
e.onChanged?.call(value);
// Only force a rebuild when we keep the menu open to reveal the slider
if (keepOpenForThisItem) {
setState(() {});
}
},
child: child,
ffi: ffi,
// When entering custom, keep submenu open to show the slider controls
closeOnActivate: !keepOpenForThisItem);
}).toList(),
// Only show a divider when custom is NOT selected
if (!isCustomSelected) Divider(),
_customControlsIfCustomSelected(onChanged: (v) => customPercent.value = v),
]);
});
}
Widget _customControlsIfCustomSelected({ValueChanged<int>? onChanged}) {
return futureBuilder(future: () async {
final current = await bind.sessionGetViewStyle(sessionId: ffi.sessionId);
return current == kRemoteViewStyleCustom;
}(), hasData: (data) {
final isCustom = data as bool;
return AnimatedSwitcher(
duration: Duration(milliseconds: 220),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: isCustom ? _CustomScaleMenuControls(ffi: ffi, onChanged: onChanged) : SizedBox.shrink(),
);
});
}
scrollStyle() {
return futureBuilder(future: () async {
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
final visible = viewStyle == kRemoteViewStyleOriginal;
final visible = viewStyle == kRemoteViewStyleOriginal ||
viewStyle == kRemoteViewStyleCustom;
final scrollStyle =
await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
return {'visible': visible, 'scrollStyle': scrollStyle};
@@ -1146,24 +1207,27 @@ class _DisplayMenuState extends State<_DisplayMenu> {
widget.ffi.canvasModel.updateScrollStyle();
}
final enabled = widget.ffi.canvasModel.imageOverflow.value;
return Column(children: [
RdoMenuButton<String>(
child: Text(translate('ScrollAuto')),
value: kRemoteScrollStyleAuto,
groupValue: groupValue,
onChanged: enabled ? (value) => onChange(value) : null,
ffi: widget.ffi,
),
RdoMenuButton<String>(
child: Text(translate('Scrollbar')),
value: kRemoteScrollStyleBar,
groupValue: groupValue,
onChanged: enabled ? (value) => onChange(value) : null,
ffi: widget.ffi,
),
Divider(),
]);
return Obx(() => Column(children: [
RdoMenuButton<String>(
child: Text(translate('ScrollAuto')),
value: kRemoteScrollStyleAuto,
groupValue: groupValue,
onChanged: widget.ffi.canvasModel.imageOverflow.value
? (value) => onChange(value)
: null,
ffi: widget.ffi,
),
RdoMenuButton<String>(
child: Text(translate('Scrollbar')),
value: kRemoteScrollStyleBar,
groupValue: groupValue,
onChanged: widget.ffi.canvasModel.imageOverflow.value
? (value) => onChange(value)
: null,
ffi: widget.ffi,
),
Divider(),
]));
});
}
@@ -1245,6 +1309,296 @@ class _DisplayMenuState extends State<_DisplayMenu> {
}
}
class _CustomScaleMenuControls extends StatefulWidget {
final FFI ffi;
final ValueChanged<int>? onChanged;
const _CustomScaleMenuControls({Key? key, required this.ffi, this.onChanged}) : super(key: key);
@override
State<_CustomScaleMenuControls> createState() => _CustomScaleMenuControlsState();
}
class _CustomScaleMenuControlsState extends State<_CustomScaleMenuControls> {
late int _value;
late final Debouncer<int> _debouncerScale;
// Normalized slider position in [0, 1]. We map it nonlinearly to percent.
double _pos = 0.0;
// Piecewise mapping constants (moved to consts.dart)
static const int _minPercent = kScaleCustomMinPercent;
static const int _pivotPercent = kScaleCustomPivotPercent; // 100% should be at 1/3 of track
static const int _maxPercent = kScaleCustomMaxPercent;
static const double _pivotPos = kScaleCustomPivotPos; // first 1/3 → up to 100%
static const double _detentEpsilon = kScaleCustomDetentEpsilon; // snap range around pivot (~0.6%)
// Clamp helper for local use
int _clamp(int v) => clampCustomScalePercent(v);
// Map normalized position [0,1] → percent [5,1000] with 100 at 1/3 width.
int _mapPosToPercent(double p) {
if (p <= 0.0) return _minPercent;
if (p >= 1.0) return _maxPercent;
if (p <= _pivotPos) {
final q = p / _pivotPos; // 0..1
final v = _minPercent + q * (_pivotPercent - _minPercent);
return _clamp(v.round());
} else {
final q = (p - _pivotPos) / (1.0 - _pivotPos); // 0..1
final v = _pivotPercent + q * (_maxPercent - _pivotPercent);
return _clamp(v.round());
}
}
// Map percent [5,1000] → normalized position [0,1]
double _mapPercentToPos(int percent) {
final p = _clamp(percent);
if (p <= _pivotPercent) {
final q = (p - _minPercent) / (_pivotPercent - _minPercent);
return q * _pivotPos;
} else {
final q = (p - _pivotPercent) / (_maxPercent - _pivotPercent);
return _pivotPos + q * (1.0 - _pivotPos);
}
}
// Snap normalized position to the pivot when close to it
double _snapNormalizedPos(double p) {
if ((p - _pivotPos).abs() <= _detentEpsilon) return _pivotPos;
if (p < 0.0) return 0.0;
if (p > 1.0) return 1.0;
return p;
}
@override
void initState() {
super.initState();
_value = 100;
_debouncerScale = Debouncer<int>(
kDebounceCustomScaleDuration,
onChanged: (v) async {
await _apply(v);
},
initialValue: _value,
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
final v = await getSessionCustomScalePercent(widget.ffi.sessionId);
if (mounted) {
setState(() {
_value = v;
_pos = _mapPercentToPos(v);
});
}
} catch (e, st) {
debugPrint('[CustomScale] Failed to get initial value: $e');
debugPrintStack(stackTrace: st);
}
});
}
Future<void> _apply(int v) async {
v = clampCustomScalePercent(v);
setState(() {
_value = v;
});
try {
await bind.sessionSetFlutterOption(
sessionId: widget.ffi.sessionId,
k: kCustomScalePercentKey,
v: v.toString());
final curStyle = await bind.sessionGetViewStyle(sessionId: widget.ffi.sessionId);
if (curStyle != kRemoteViewStyleCustom) {
await bind.sessionSetViewStyle(
sessionId: widget.ffi.sessionId, value: kRemoteViewStyleCustom);
}
await widget.ffi.canvasModel.updateViewStyle();
if (isMobile) {
HapticFeedback.selectionClick();
}
widget.onChanged?.call(v);
} catch (e, st) {
debugPrint('[CustomScale] Apply failed: $e');
debugPrintStack(stackTrace: st);
}
}
void _nudge(int delta) {
final next = _clamp(_value + delta);
setState(() {
_value = next;
_pos = _mapPercentToPos(next);
});
widget.onChanged?.call(next);
_debouncerScale.value = next;
}
@override
void dispose() {
_debouncerScale.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
const smallBtnConstraints = BoxConstraints(minWidth: 28, minHeight: 28);
final sliderControl = Semantics(
label: translate('Custom scale slider'),
value: '$_value%',
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: colorScheme.primary,
thumbColor: colorScheme.primary,
overlayColor: colorScheme.primary.withOpacity(0.1),
showValueIndicator: ShowValueIndicator.never,
thumbShape: _RectValueThumbShape(
min: _minPercent.toDouble(),
max: _maxPercent.toDouble(),
width: 52,
height: 24,
radius: 4,
// Display the mapped percent for the current normalized value
displayValueForNormalized: (t) => _mapPosToPercent(t),
),
),
child: Slider(
value: _pos,
min: 0.0,
max: 1.0,
// Use a wide range of divisions (calculated as (_maxPercent - _minPercent)) to provide ~1% precision increments.
// This allows users to set precise scale values. Lower values would require more fine-tuning via the +/- buttons, which is undesirable for big ranges.
divisions: (_maxPercent - _minPercent).round(),
onChanged: (v) {
final snapped = _snapNormalizedPos(v);
final next = _mapPosToPercent(snapped);
if (next != _value || snapped != _pos) {
setState(() {
_pos = snapped;
_value = next;
});
widget.onChanged?.call(next);
_debouncerScale.value = next;
}
},
),
),
);
return Column(children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(children: [
Tooltip(
message: translate('Decrease'),
child: IconButton(
iconSize: 16,
padding: EdgeInsets.all(1),
constraints: smallBtnConstraints,
icon: const Icon(Icons.remove),
onPressed: () => _nudge(-1),
),
),
Expanded(child: sliderControl),
Tooltip(
message: translate('Increase'),
child: IconButton(
iconSize: 16,
padding: EdgeInsets.all(1),
constraints: smallBtnConstraints,
icon: const Icon(Icons.add),
onPressed: () => _nudge(1),
),
),
]),
),
Divider(),
]);
}
}
// Lightweight rectangular thumb that paints the current percentage.
// Stateless and uses only SliderTheme colors; avoids allocations beyond a TextPainter per frame.
class _RectValueThumbShape extends SliderComponentShape {
final double min;
final double max;
final double width;
final double height;
final double radius;
// Optional mapper to compute display value from normalized position [0,1]
// If null, falls back to linear interpolation between min and max.
final int Function(double normalized)? displayValueForNormalized;
const _RectValueThumbShape({
required this.min,
required this.max,
required this.width,
required this.height,
required this.radius,
this.displayValueForNormalized,
});
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size(width, height);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
// Resolve color based on enabled/disabled animation, with safe fallbacks.
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final Color? evaluatedColor = colorTween.evaluate(enableAnimation);
final Color? thumbColor = sliderTheme.thumbColor;
final Color fillColor = evaluatedColor ?? thumbColor ?? Colors.blueAccent;
final RRect rrect = RRect.fromRectAndRadius(
Rect.fromCenter(center: center, width: width, height: height),
Radius.circular(radius),
);
final Paint paint = Paint()..color = fillColor;
canvas.drawRRect(rrect, paint);
// Compute displayed percent from normalized slider value.
final int percent = displayValueForNormalized != null
? displayValueForNormalized!(value)
: (min + value * (max - min)).round();
final TextSpan span = TextSpan(
text: '$percent%',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
);
final TextPainter tp = TextPainter(
text: span,
textAlign: TextAlign.center,
textDirection: textDirection,
);
tp.layout(maxWidth: width - 4);
tp.paint(canvas, Offset(center.dx - tp.width / 2, center.dy - tp.height / 2));
}
}
class _ResolutionsMenu extends StatefulWidget {
final String id;
final FFI ffi;
@@ -1593,6 +1947,9 @@ class _KeyboardMenu extends StatelessWidget {
inputSource(),
Divider(),
viewMode(),
if ([kPeerPlatformWindows, kPeerPlatformMacOS, kPeerPlatformLinux]
.contains(pi.platform))
showMyCursor(),
Divider(),
...toolbarToggles(),
...mouseSpeed(),
@@ -1749,12 +2106,43 @@ class _KeyboardMenu extends StatelessWidget {
final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleShowMyCursor);
ffiModel.setShowMyCursor(showMyCursor ?? value);
}
: null,
ffi: ffi,
child: Text(translate('View Mode')));
}
showMyCursor() {
final ffiModel = ffi.ffiModel;
return CkbMenuButton(
value: ffiModel.showMyCursor,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleShowMyCursor);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId,
arg: kOptionToggleShowMyCursor) ??
value;
ffiModel.setShowMyCursor(showMyCursor);
// Also set view only if showMyCursor is enabled and viewOnly is not enabled.
if (showMyCursor && !ffiModel.viewOnly) {
await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value);
}
},
ffi: ffi,
child: Text(translate('Show my cursor')))
.paddingOnly(left: 26.0);
}
mobileActions() {
if (pi.platform != kPeerPlatformAndroid) return [];
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
@@ -2232,6 +2620,8 @@ class RdoMenuButton<T> extends StatelessWidget {
final ValueChanged<T?>? onChanged;
final Widget? child;
final FFI? ffi;
// When true, submenu will be dismissed on activate; when false, it stays open.
final bool closeOnActivate;
const RdoMenuButton({
Key? key,
required this.value,
@@ -2239,6 +2629,7 @@ class RdoMenuButton<T> extends StatelessWidget {
required this.child,
this.ffi,
this.onChanged,
this.closeOnActivate = true,
}) : super(key: key);
@override
@@ -2247,9 +2638,10 @@ class RdoMenuButton<T> extends StatelessWidget {
value: value,
groupValue: groupValue,
child: child,
closeOnActivate: closeOnActivate,
onChanged: onChanged != null
? (T? value) {
if (ffi != null) {
if (ffi != null && closeOnActivate) {
_menuDismissCallback(ffi!);
}
onChanged?.call(value);

View File

@@ -292,7 +292,6 @@ class DesktopTab extends StatefulWidget {
// ignore: must_be_immutable
class _DesktopTabState extends State<DesktopTab>
with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0;
@@ -370,7 +369,7 @@ class _DesktopTabState extends State<DesktopTab>
void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame);
_saveFrame();
setState(() {});
}
@@ -405,24 +404,24 @@ class _DesktopTabState extends State<DesktopTab>
super.onWindowUnmaximize();
}
_saveFrame() async {
_saveFrame({bool? flush}) async {
if (tabType == DesktopTabType.main) {
await saveWindowPosition(WindowType.Main);
await saveWindowPosition(WindowType.Main, flush: flush);
} else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!, windowId: kWindowId);
await saveWindowPosition(kWindowType!, windowId: kWindowId, flush: flush);
}
}
@override
void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame);
_saveFrame();
super.onWindowMoved();
}
@override
void onWindowResized() {
_saveFrameDebounce.call(_saveFrame);
super.onWindowMoved();
_saveFrame();
super.onWindowResized();
}
@override
@@ -460,6 +459,8 @@ class _DesktopTabState extends State<DesktopTab>
});
}
await _saveFrame(flush: true);
// hide window on close
if (isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {

View File

@@ -7,7 +7,10 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
final _isExtracting = false.obs;
void handleUpdate(String releasePageUrl) {
_isExtracting.value = false;
String downloadUrl = releasePageUrl.replaceAll('tag', 'download');
String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
final String downloadFile =
@@ -25,13 +28,15 @@ void handleUpdate(String releasePageUrl) {
gFFI.dialogManager.dismissAll();
gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog(
title: Text(translate('Downloading {$appName}')),
title: Obx(() => Text(translate(_isExtracting.isTrue
? 'Preparing for installation ...'
: 'Downloading {$appName}'))),
content:
UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled)
.marginSymmetric(horizontal: 8)
.paddingOnly(top: 12),
actions: [
dialogButton(translate('Cancel'), onPressed: () async {
if (_isExtracting.isFalse) dialogButton(translate('Cancel'), onPressed: () async {
onCanceled.value();
await bind.mainSetCommon(
key: 'cancel-downloader', value: downloadId.value);
@@ -71,6 +76,7 @@ class UpdateProgressState extends State<UpdateProgress> {
int _downloadedSize = 0;
int _getDataFailedCount = 0;
final String _eventKeyDownloadNewVersion = 'download-new-version';
final String _eventKeyExtractUpdateDmg = 'extract-update-dmg';
@override
void initState() {
@@ -82,6 +88,11 @@ class UpdateProgressState extends State<UpdateProgress> {
_eventKeyDownloadNewVersion, handleDownloadNewVersion,
replace: true);
bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl);
if (isMacOS) {
platformFFI.registerEventHandler(_eventKeyExtractUpdateDmg,
_eventKeyExtractUpdateDmg, handleExtractUpdateDmg,
replace: true);
}
}
@override
@@ -89,6 +100,10 @@ class UpdateProgressState extends State<UpdateProgress> {
cancelQueryTimer();
platformFFI.unregisterEventHandler(
_eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
if (isMacOS) {
platformFFI.unregisterEventHandler(
_eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg);
}
super.dispose();
}
@@ -113,10 +128,13 @@ class UpdateProgressState extends State<UpdateProgress> {
}
}
void _onError(String error) {
// `isExtractDmg` is true when handling extract-update-dmg event.
// It's a rare case that the dmg file is corrupted and cannot be extracted.
void _onError(String error, {bool isExtractDmg = false}) {
cancelQueryTimer();
debugPrint('Download new version error: $error');
debugPrint(
'${isExtractDmg ? "Extract" : "Download"} new version error: $error');
final msgBoxType = 'custom-nocancel-nook-hasclose';
final msgBoxTitle = 'Error';
final msgBoxText = 'download-new-version-failed-tip';
@@ -138,7 +156,7 @@ class UpdateProgressState extends State<UpdateProgress> {
final List<Widget> buttons = [
dialogButton('Download', onPressed: jumplink),
dialogButton('Retry', onPressed: retry),
if (!isExtractDmg) dialogButton('Retry', onPressed: retry),
dialogButton('Close', onPressed: close),
];
dialogManager.dismissAll();
@@ -194,19 +212,13 @@ class UpdateProgressState extends State<UpdateProgress> {
_onError('The download file size is 0.');
} else {
setState(() {});
msgBox(
gFFI.sessionId,
'custom-nocancel',
'{$appName} Update',
'{$appName}-to-update-tip',
'',
gFFI.dialogManager,
onSubmit: () {
debugPrint('Downloaded, update to new version now');
bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
},
submitTimeout: 5,
);
if (isMacOS) {
bind.mainSetCommon(
key: 'extract-update-dmg', value: widget.downloadUrl);
_isExtracting.value = true;
} else {
updateMsgBox();
}
}
} else {
setState(() {});
@@ -214,17 +226,38 @@ class UpdateProgressState extends State<UpdateProgress> {
}
}
@override
Widget build(BuildContext context) {
return onDownloading(context);
void updateMsgBox() {
msgBox(
gFFI.sessionId,
'custom-nocancel',
'{$appName} Update',
'{$appName}-to-update-tip',
'',
gFFI.dialogManager,
onSubmit: () {
debugPrint('Downloaded, update to new version now');
bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
},
submitTimeout: 5,
);
}
Widget onDownloading(BuildContext context) {
final value = _totalSize == null
Future<void> handleExtractUpdateDmg(Map<String, dynamic> evt) async {
_isExtracting.value = false;
if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) {
_onError(evt['err'] as String, isExtractDmg: true);
} else {
updateMsgBox();
}
}
@override
Widget build(BuildContext context) {
getValue() => _totalSize == null
? 0.0
: (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!);
return LinearProgressIndicator(
value: value,
value: _isExtracting.isTrue ? null : getValue(),
minHeight: 20,
borderRadius: BorderRadius.circular(5),
backgroundColor: Colors.grey[300],

View File

@@ -147,9 +147,15 @@ void runMainApp(bool startService) async {
gFFI.userModel.refreshCurrentUser();
runApp(App());
bool? alwaysOnTop;
if (isDesktop) {
alwaysOnTop =
bind.mainGetBuildinOption(key: "main-window-always-on-top") == 'Y';
}
// Set window option.
WindowOptions windowOptions =
getHiddenTitleBarWindowOptions(isMainWindow: true);
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
isMainWindow: true, alwaysOnTop: alwaysOnTop);
windowManager.waitUntilReadyToShow(windowOptions, () async {
// Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main);

View File

@@ -182,6 +182,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
rdpUsername: '',
loginName: '',
device_group_name: '',
note: '',
);
_autocompleteOpts = [emptyPeer];
} else {

View File

@@ -29,9 +29,9 @@ class HomePageState extends State<HomePage> {
int get selectedIndex => _selectedIndex;
final List<PageShape> _pages = [];
int _chatPageTabIndex = -1;
bool get isChatPageCurrentTab => (isAndroid || isIOS)
bool get isChatPageCurrentTab => isAndroid
? _selectedIndex == _chatPageTabIndex
: false;
: false; // change this when ios have chat page
void refreshPages() {
setState(() {
@@ -52,7 +52,7 @@ class HomePageState extends State<HomePage> {
appBarActions: [],
));
}
if ((isAndroid || isIOS) && !bind.isOutgoingOnly()) {
if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
}
@@ -230,6 +230,12 @@ class WebHomePage extends StatelessWidget {
id = args[i + 1];
i++;
break;
case '--terminal-admin':
setEnvTerminalAdmin();
isTerminal = true;
id = args[i + 1];
i++;
break;
case '--password':
password = args[i + 1];
i++;

View File

@@ -6,6 +6,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/mobile/widgets/floating_mouse.dart';
import 'package:flutter_hbb/mobile/widgets/floating_mouse_widgets.dart';
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
@@ -40,7 +42,12 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
}
class RemotePage extends StatefulWidget {
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
RemotePage(
{Key? key,
required this.id,
this.password,
this.isSharedPassword,
this.forceRelay})
: super(key: key);
final String id;
@@ -612,6 +619,15 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
if (showCursorPaint) {
paints.add(CursorPaint(widget.id));
}
if (gFFI.ffiModel.touchMode) {
paints.add(FloatingMouse(
ffi: gFFI,
));
} else {
paints.add(FloatingMouseWidgets(
ffi: gFFI,
));
}
return paints;
}()));
}
@@ -784,13 +800,14 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
controller: ScrollController(),
padding: EdgeInsets.symmetric(vertical: 10),
child: GestureHelp(
touchMode: gFFI.ffiModel.touchMode,
onTouchModeChange: (t) {
gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
bind.sessionPeerOption(
sessionId: sessionId, name: kOptionTouchMode, value: v);
})));
touchMode: gFFI.ffiModel.touchMode,
onTouchModeChange: (t) {
gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : 'N';
bind.mainSetLocalOption(key: kOptionTouchMode, value: v);
},
virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
)));
}
// * Currently mobile does not enable map mode
@@ -1105,7 +1122,7 @@ void showOptions(
BuildContext context, String id, OverlayDialogManager dialogManager) async {
var displays = <Widget>[];
final pi = gFFI.ffiModel.pi;
final image = gFFI.ffiModel.getConnectionImage();
final image = gFFI.ffiModel.getConnectionImageText();
if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
}

View File

@@ -181,11 +181,7 @@ class _ServerPageState extends State<ServerPage> {
_updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
await gFFI.serverModel.fetchID();
});
if (isAndroid) {
gFFI.serverModel.checkAndroidPermission();
} else if (isIOS) {
gFFI.serverModel.checkIOSPermission();
}
gFFI.serverModel.checkAndroidPermission();
}
@override
@@ -244,7 +240,7 @@ class ServiceNotRunningNotification extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate(isAndroid ? "android_start_service_tip" : "Start screen sharing service"),
Text(translate("android_start_service_tip"),
style:
const TextStyle(fontSize: 12, color: MyTheme.darkGray))
.marginOnly(bottom: 8),
@@ -579,7 +575,7 @@ class _PermissionCheckerState extends State<PermissionChecker> {
@override
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
final hasAudioPermission = isIOS || androidVersion >= 30;
final hasAudioPermission = androidVersion >= 30;
return PaddingCard(
title: translate("Permissions"),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
@@ -603,11 +599,10 @@ class _PermissionCheckerState extends State<PermissionChecker> {
: serverModel.toggleService),
PermissionRow(translate("Input Control"), serverModel.inputOk,
serverModel.toggleInput),
if (!isIOS)
PermissionRow(translate("Transfer file"), serverModel.fileOk,
serverModel.toggleFile),
PermissionRow(translate("Transfer file"), serverModel.fileOk,
serverModel.toggleFile),
hasAudioPermission
? PermissionRow(translate(isIOS ? "Microphone" : "Audio Capture"), serverModel.audioOk,
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
serverModel.toggleAudio)
: Row(children: [
Icon(Icons.info_outline).marginOnly(right: 15),
@@ -617,19 +612,8 @@ class _PermissionCheckerState extends State<PermissionChecker> {
style: const TextStyle(color: MyTheme.darkGray),
))
]),
if (!isIOS)
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
if (isIOS) ...[
Row(children: [
Icon(Icons.info_outline, size: 16).marginOnly(right: 8),
Expanded(
child: Text(
translate("File transfer and clipboard sync are not available during iOS screen sharing"),
style: const TextStyle(fontSize: 12, color: MyTheme.darkGray),
))
]).marginOnly(top: 8),
],
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
]));
}
}

View File

@@ -378,7 +378,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
},
),
SettingsTile.switchTile(
title: Text('${translate('Adaptive bitrate')} (beta)'),
title: Text(translate('Adaptive bitrate')),
initialValue: _enableAbr,
onToggle: isOptionFixed(kOptionEnableAbr)
? null
@@ -540,7 +540,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: _enableStartOnBoot,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text("${translate('Start on boot')} (beta)"),
Text(translate('Start on boot')),
Text(
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
style: Theme.of(context).textTheme.bodySmall),
@@ -602,44 +602,39 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
gFFI.serverModel.androidUpdatekeepScreenOn();
}
if (isAndroid) {
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: !_floatingWindowDisabled,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(translate('Floating window')),
Text('* ${translate('floating_window_tip')}',
style: Theme.of(context).textTheme.bodySmall),
]),
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
? null
: onFloatingWindowChanged));
}
if (isAndroid) {
enhancementsTiles.add(_getPopupDialogRadioEntry(
title: 'Keep screen on',
list: [
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
_RadioEntry('During controlled',
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
_RadioEntry('During service is on',
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
],
getter: () => _keepScreenOnToOption(
_floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: !_floatingWindowDisabled,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(translate('Floating window')),
Text('* ${translate('floating_window_tip')}',
style: Theme.of(context).textTheme.bodySmall),
]),
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
? null
: (value) async {
await bind.mainSetLocalOption(
key: kOptionKeepScreenOn, value: value);
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
gFFI.serverModel.androidUpdatekeepScreenOn();
},
));
}
: onFloatingWindowChanged));
enhancementsTiles.add(_getPopupDialogRadioEntry(
title: 'Keep screen on',
list: [
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
_RadioEntry('During controlled',
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
_RadioEntry('During service is on',
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
],
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
? null
: (value) async {
await bind.mainSetLocalOption(
key: kOptionKeepScreenOn, value: value);
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
gFFI.serverModel.androidUpdatekeepScreenOn();
},
));
final disabledSettings = bind.isDisableSettings();
final hideSecuritySettings =
@@ -674,7 +669,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onPressed: (context) {
showServerSettings(gFFI.dialogManager);
}),
if (!_hideNetwork && !_hideProxy)
if (!isIOS && !_hideNetwork && !_hideProxy)
SettingsTile(
title: Text(translate('Socks5/Http(s) Proxy')),
leading: Icon(Icons.network_ping),
@@ -815,7 +810,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
!outgoingOnly &&
!hideSecuritySettings)
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
if ((isAndroid || isIOS) &&
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)
@@ -824,7 +819,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
tiles: shareScreenTiles,
),
if (!bind.isIncomingOnly()) defaultDisplaySection(),
if ((isAndroid || isIOS) &&
if (isAndroid &&
!disabledSettings &&
!outgoingOnly &&
!hideSecuritySettings)

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:xterm/xterm.dart';
import '../../desktop/pages/terminal_connection_manager.dart';
@@ -31,6 +32,12 @@ class _TerminalPageState extends State<TerminalPage>
late FFI _ffi;
late TerminalModel _terminalModel;
// For web only.
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
final String _robotoMonoFontFamily = isWeb
? (GoogleFonts.robotoMono().fontFamily ?? 'monospace')
: 'monospace';
@override
void initState() {
super.initState();
@@ -81,6 +88,7 @@ class _TerminalPageState extends State<TerminalPage>
_terminalModel.terminal,
controller: _terminalModel.terminalController,
autofocus: true,
textStyle: _getTerminalStyle(),
backgroundOpacity: 0.7,
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
onSecondaryTapDown: (details, offset) async {
@@ -101,6 +109,17 @@ class _TerminalPageState extends State<TerminalPage>
);
}
// https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472
// https://github.com/TerminalStudio/xterm.dart/issues/198#issuecomment-2526548458
TerminalStyle _getTerminalStyle() {
return isWeb
? TerminalStyle(
fontFamily: _robotoMonoFontFamily,
fontSize: 14,
)
: const TerminalStyle();
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -39,7 +39,11 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
class ViewCameraPage extends StatefulWidget {
ViewCameraPage(
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
{Key? key,
required this.id,
this.password,
this.isSharedPassword,
this.forceRelay})
: super(key: key);
final String id;
@@ -579,7 +583,7 @@ void showOptions(
BuildContext context, String id, OverlayDialogManager dialogManager) async {
var displays = <Widget>[];
final pi = gFFI.ffiModel.pi;
final image = gFFI.ffiModel.getConnectionImage();
final image = gFFI.ffiModel.getConnectionImageText();
if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,880 @@
// These floating mouse widgets are used to simulate a physical mouse
// when "mobile" -> "desktop" in mouse mode.
// This file does not contain whole mouse widgets, it only contains
// parts that help to control, such as wheel scroll and wheel button.
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
// Used for the wheel button and wheel scroll widgets
const double _kSpaceToHorizontalEdge = 25;
const double _wheelWidth = 50;
const double _wheelHeight = 162;
// Used for the left/right button widgets
const double _kSpaceToVerticalEdge = 15;
const double _kSpaceBetweenLeftRightButtons = 40;
const double _kLeftRightButtonWidth = 55;
const double _kLeftRightButtonHeight = 40;
const double _kBorderWidth = 1;
final Color _kDefaultBorderColor = Colors.white.withOpacity(0.7);
final Color _kDefaultColor = Colors.black.withOpacity(0.4);
final Color _kTapDownColor = Colors.blue.withOpacity(0.7);
final Color _kWidgetHighlightColor = Colors.white.withOpacity(0.9);
const int _kInputTimerIntervalMillis = 100;
class FloatingMouseWidgets extends StatefulWidget {
final FFI ffi;
const FloatingMouseWidgets({
super.key,
required this.ffi,
});
@override
State<FloatingMouseWidgets> createState() => _FloatingMouseWidgetsState();
}
class _FloatingMouseWidgetsState extends State<FloatingMouseWidgets> {
InputModel get _inputModel => widget.ffi.inputModel;
CursorModel get _cursorModel => widget.ffi.cursorModel;
late final VirtualMouseMode _virtualMouseMode;
@override
void initState() {
super.initState();
_virtualMouseMode = widget.ffi.ffiModel.virtualMouseMode;
_virtualMouseMode.addListener(_onVirtualMouseModeChanged);
_cursorModel.blockEvents = false;
isSpecialHoldDragActive = false;
}
void _onVirtualMouseModeChanged() {
if (mounted) {
setState(() {});
}
}
@override
void dispose() {
_virtualMouseMode.removeListener(_onVirtualMouseModeChanged);
super.dispose();
_cursorModel.blockEvents = false;
isSpecialHoldDragActive = false;
}
@override
Widget build(BuildContext context) {
final virtualMouseMode = _virtualMouseMode;
if (!virtualMouseMode.showVirtualMouse) {
return const Offstage();
}
return Stack(
children: [
FloatingWheel(
inputModel: _inputModel,
cursorModel: _cursorModel,
),
if (virtualMouseMode.showVirtualJoystick)
VirtualJoystick(cursorModel: _cursorModel),
FloatingLeftRightButton(
isLeft: true,
inputModel: _inputModel,
cursorModel: _cursorModel,
),
FloatingLeftRightButton(
isLeft: false,
inputModel: _inputModel,
cursorModel: _cursorModel,
),
],
);
}
}
class FloatingWheel extends StatefulWidget {
final InputModel inputModel;
final CursorModel cursorModel;
const FloatingWheel(
{super.key, required this.inputModel, required this.cursorModel});
@override
State<FloatingWheel> createState() => _FloatingWheelState();
}
class _FloatingWheelState extends State<FloatingWheel> {
Offset _position = Offset.zero;
bool _isInitialized = false;
Rect? _lastBlockedRect;
bool _isUpDown = false;
bool _isMidDown = false;
bool _isDownDown = false;
Orientation? _previousOrientation;
Timer? _scrollTimer;
InputModel get _inputModel => widget.inputModel;
CursorModel get _cursorModel => widget.cursorModel;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_resetPosition();
});
}
void _resetPosition() {
final size = MediaQuery.of(context).size;
setState(() {
_position = Offset(
size.width - _wheelWidth - _kSpaceToHorizontalEdge,
(size.height - _wheelHeight) / 2,
);
_isInitialized = true;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateBlockedRect();
});
}
void _updateBlockedRect() {
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
final newRect =
Rect.fromLTWH(_position.dx, _position.dy, _wheelWidth, _wheelHeight);
_cursorModel.addBlockedRect(newRect);
_lastBlockedRect = newRect;
}
@override
void dispose() {
_scrollTimer?.cancel();
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentOrientation = MediaQuery.of(context).orientation;
if (_previousOrientation != null &&
_previousOrientation != currentOrientation) {
_resetPosition();
}
_previousOrientation = currentOrientation;
}
Widget _buildUpDownButton(
void Function(PointerDownEvent) onPointerDown,
void Function(PointerUpEvent) onPointerUp,
void Function(PointerCancelEvent) onPointerCancel,
bool Function() flagGetter,
BorderRadiusGeometry borderRadius,
IconData iconData) {
return Listener(
onPointerDown: onPointerDown,
onPointerUp: onPointerUp,
onPointerCancel: onPointerCancel,
child: Container(
width: _wheelWidth,
height: 55,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _kDefaultColor,
border: Border.all(
color: flagGetter() ? _kTapDownColor : _kDefaultBorderColor,
width: 1),
borderRadius: borderRadius,
),
child: Icon(iconData, color: _kDefaultBorderColor, size: 32),
),
);
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Positioned(child: Offstage());
}
return Positioned(
left: _position.dx,
top: _position.dy,
child: _buildWidget(context),
);
}
Widget _buildWidget(BuildContext context) {
return Container(
width: _wheelWidth,
height: _wheelHeight,
child: Column(
children: [
_buildUpDownButton(
(event) {
setState(() {
_isUpDown = true;
});
_startScrollTimer(1);
},
(event) {
setState(() {
_isUpDown = false;
});
_stopScrollTimer();
},
(event) {
setState(() {
_isUpDown = false;
});
_stopScrollTimer();
},
() => _isUpDown,
BorderRadius.vertical(top: Radius.circular(_wheelWidth * 0.5)),
Icons.keyboard_arrow_up,
),
Listener(
onPointerDown: (event) {
setState(() {
_isMidDown = true;
});
_inputModel.tapDown(MouseButtons.wheel);
},
onPointerUp: (event) {
setState(() {
_isMidDown = false;
});
_inputModel.tapUp(MouseButtons.wheel);
},
onPointerCancel: (event) {
setState(() {
_isMidDown = false;
});
_inputModel.tapUp(MouseButtons.wheel);
},
child: Container(
width: _wheelWidth,
height: 52,
decoration: BoxDecoration(
color: _kDefaultColor,
border: Border.symmetric(
vertical: BorderSide(
color:
_isMidDown ? _kTapDownColor : _kDefaultBorderColor,
width: _kBorderWidth)),
),
child: Center(
child: Container(
width: _wheelWidth - 10,
height: _wheelWidth - 10,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 18,
height: 2,
color: _kDefaultBorderColor,
),
SizedBox(height: 6),
Container(
width: 24,
height: 2,
color: _kDefaultBorderColor,
),
SizedBox(height: 6),
Container(
width: 18,
height: 2,
color: _kDefaultBorderColor,
),
],
),
),
),
),
),
),
_buildUpDownButton(
(event) {
setState(() {
_isDownDown = true;
});
_startScrollTimer(-1);
},
(event) {
setState(() {
_isDownDown = false;
});
_stopScrollTimer();
},
(event) {
setState(() {
_isDownDown = false;
});
_stopScrollTimer();
},
() => _isDownDown,
BorderRadius.vertical(bottom: Radius.circular(_wheelWidth * 0.5)),
Icons.keyboard_arrow_down,
),
],
),
);
}
void _startScrollTimer(int direction) {
_scrollTimer?.cancel();
_inputModel.scroll(direction);
_scrollTimer = Timer.periodic(
Duration(milliseconds: _kInputTimerIntervalMillis), (timer) {
_inputModel.scroll(direction);
});
}
void _stopScrollTimer() {
_scrollTimer?.cancel();
_scrollTimer = null;
}
}
class FloatingLeftRightButton extends StatefulWidget {
final bool isLeft;
final InputModel inputModel;
final CursorModel cursorModel;
const FloatingLeftRightButton(
{super.key,
required this.isLeft,
required this.inputModel,
required this.cursorModel});
@override
State<FloatingLeftRightButton> createState() =>
_FloatingLeftRightButtonState();
}
class _FloatingLeftRightButtonState extends State<FloatingLeftRightButton> {
Offset _position = Offset.zero;
bool _isInitialized = false;
bool _isDown = false;
Rect? _lastBlockedRect;
Orientation? _previousOrientation;
Offset _preSavedPos = Offset.zero;
// Gesture ambiguity resolution
Timer? _tapDownTimer;
final Duration _pressTimeout = const Duration(milliseconds: 200);
bool _isDragging = false;
bool get _isLeft => widget.isLeft;
InputModel get _inputModel => widget.inputModel;
CursorModel get _cursorModel => widget.cursorModel;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final currentOrientation = MediaQuery.of(context).orientation;
_previousOrientation = currentOrientation;
_resetPosition(currentOrientation);
});
}
@override
void dispose() {
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
_tapDownTimer?.cancel();
_trySavePosition();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentOrientation = MediaQuery.of(context).orientation;
if (_previousOrientation == null ||
_previousOrientation != currentOrientation) {
_resetPosition(currentOrientation);
}
_previousOrientation = currentOrientation;
}
double _getOffsetX(double w) {
if (_isLeft) {
return (w - _kLeftRightButtonWidth * 2 - _kSpaceBetweenLeftRightButtons) *
0.5;
} else {
return (w + _kSpaceBetweenLeftRightButtons) * 0.5;
}
}
String _getPositionKey(Orientation ori) {
final strLeftRight = _isLeft ? 'l' : 'r';
final strOri = ori == Orientation.landscape ? 'l' : 'p';
return '$strLeftRight$strOri-mouse-btn-pos';
}
static Offset? _loadPositionFromString(String s) {
if (s.isEmpty) {
return null;
}
try {
final m = jsonDecode(s);
return Offset(m['x'], m['y']);
} catch (e) {
debugPrintStack(label: 'Failed to load position "$s" $e');
return null;
}
}
void _trySavePosition() {
if (_previousOrientation == null) return;
if (((_position - _preSavedPos)).distanceSquared < 0.1) return;
final pos = jsonEncode({
'x': _position.dx,
'y': _position.dy,
});
bind.setLocalFlutterOption(
k: _getPositionKey(_previousOrientation!), v: pos);
_preSavedPos = _position;
}
void _restorePosition(Orientation ori) {
final ps = bind.getLocalFlutterOption(k: _getPositionKey(ori));
final pos = _loadPositionFromString(ps);
if (pos == null) {
final size = MediaQuery.of(context).size;
_position = Offset(_getOffsetX(size.width),
size.height - _kSpaceToVerticalEdge - _kLeftRightButtonHeight);
} else {
_position = pos;
_preSavedPos = pos;
}
}
void _resetPosition(Orientation ori) {
setState(() {
_restorePosition(ori);
_isInitialized = true;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateBlockedRect();
});
}
void _updateBlockedRect() {
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
final newRect = Rect.fromLTWH(_position.dx, _position.dy,
_kLeftRightButtonWidth, _kLeftRightButtonHeight);
_cursorModel.addBlockedRect(newRect);
_lastBlockedRect = newRect;
}
void _onMoveUpdateDelta(Offset delta) {
final context = this.context;
final size = MediaQuery.of(context).size;
Offset newPosition = _position + delta;
double minX = _kSpaceToHorizontalEdge;
double minY = _kSpaceToVerticalEdge;
double maxX = size.width - _kLeftRightButtonWidth - _kSpaceToHorizontalEdge;
double maxY = size.height - _kLeftRightButtonHeight - _kSpaceToVerticalEdge;
newPosition = Offset(
newPosition.dx.clamp(minX, maxX),
newPosition.dy.clamp(minY, maxY),
);
final isPositionChanged = !(isDoubleEqual(newPosition.dx, _position.dx) &&
isDoubleEqual(newPosition.dy, _position.dy));
setState(() {
_position = newPosition;
});
if (isPositionChanged) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateBlockedRect();
});
}
}
void _onBodyPointerMoveUpdate(PointerMoveEvent event) {
_cursorModel.blockEvents = true;
// If move, it's a drag, not a tap.
_isDragging = true;
// Cancel the timer to prevent it from being recognized as a tap/hold.
_tapDownTimer?.cancel();
_tapDownTimer = null;
_onMoveUpdateDelta(event.delta);
}
Widget _buildButtonIcon() {
final double w = _kLeftRightButtonWidth * 0.45;
final double h = _kLeftRightButtonHeight * 0.75;
final double borderRadius = w * 0.5;
final double quarterCircleRadius = borderRadius * 0.9;
return Stack(
children: [
Container(
width: w,
height: h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_kLeftRightButtonWidth * 0.225),
color: Colors.white,
),
),
Positioned(
left: _isLeft ? quarterCircleRadius * 0.25 : null,
right: _isLeft ? null : quarterCircleRadius * 0.25,
top: quarterCircleRadius * 0.25,
child: CustomPaint(
size: Size(quarterCircleRadius * 2, quarterCircleRadius * 2),
painter: _QuarterCirclePainter(
color: _kDefaultColor,
isLeft: _isLeft,
radius: quarterCircleRadius,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Positioned(child: Offstage());
}
return Positioned(
left: _position.dx,
top: _position.dy,
// We can't use the GestureDetector here, because `onTapDown` may be
// triggered sometimes when dragging.
child: Listener(
onPointerMove: _onBodyPointerMoveUpdate,
onPointerDown: (event) async {
_isDragging = false;
setState(() {
_isDown = true;
});
// Start a timer. If it fires, it's a hold.
_tapDownTimer?.cancel();
_tapDownTimer = Timer(_pressTimeout, () {
isSpecialHoldDragActive = true;
() async {
await _cursorModel.syncCursorPosition();
await _inputModel
.tapDown(_isLeft ? MouseButtons.left : MouseButtons.right);
}();
_tapDownTimer = null;
});
},
onPointerUp: (event) {
_cursorModel.blockEvents = false;
setState(() {
_isDown = false;
});
// If timer is active, it's a quick tap.
if (_tapDownTimer != null) {
_tapDownTimer!.cancel();
_tapDownTimer = null;
// Fire tap down and up quickly.
_inputModel
.tapDown(_isLeft ? MouseButtons.left : MouseButtons.right)
.then(
(_) => Future.delayed(const Duration(milliseconds: 50), () {
_inputModel.tapUp(
_isLeft ? MouseButtons.left : MouseButtons.right);
}));
} else {
// If it's not a quick tap, it could be a hold or drag.
// If it was a hold, isSpecialHoldDragActive is true.
if (isSpecialHoldDragActive) {
_inputModel
.tapUp(_isLeft ? MouseButtons.left : MouseButtons.right);
}
}
if (_isDragging) {
_trySavePosition();
}
isSpecialHoldDragActive = false;
},
onPointerCancel: (event) {
_cursorModel.blockEvents = false;
setState(() {
_isDown = false;
});
_tapDownTimer?.cancel();
_tapDownTimer = null;
if (isSpecialHoldDragActive) {
_inputModel.tapUp(_isLeft ? MouseButtons.left : MouseButtons.right);
}
isSpecialHoldDragActive = false;
if (_isDragging) {
_trySavePosition();
}
},
child: Container(
width: _kLeftRightButtonWidth,
height: _kLeftRightButtonHeight,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _kDefaultColor,
border: Border.all(
color: _isDown ? _kTapDownColor : _kDefaultBorderColor,
width: _kBorderWidth),
borderRadius: _isLeft
? BorderRadius.horizontal(
left: Radius.circular(_kLeftRightButtonHeight * 0.5))
: BorderRadius.horizontal(
right: Radius.circular(_kLeftRightButtonHeight * 0.5)),
),
child: _buildButtonIcon(),
),
),
);
}
}
class _QuarterCirclePainter extends CustomPainter {
final Color color;
final bool isLeft;
final double radius;
_QuarterCirclePainter(
{required this.color, required this.isLeft, required this.radius});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final rect = Rect.fromLTWH(0, 0, radius * 2, radius * 2);
if (isLeft) {
canvas.drawArc(rect, -pi, pi / 2, true, paint);
} else {
canvas.drawArc(rect, -pi / 2, pi / 2, true, paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
// Virtual joystick sends the absolute movement for now.
// Maybe we need to change it to relative movement in the future.
class VirtualJoystick extends StatefulWidget {
final CursorModel cursorModel;
const VirtualJoystick({super.key, required this.cursorModel});
@override
State<VirtualJoystick> createState() => _VirtualJoystickState();
}
class _VirtualJoystickState extends State<VirtualJoystick> {
Offset _position = Offset.zero;
bool _isInitialized = false;
Offset _offset = Offset.zero;
final double _joystickRadius = 50.0;
final double _thumbRadius = 20.0;
final double _moveStep = 3.0;
final double _speed = 1.0;
// One-shot timer to detect a drag gesture
Timer? _dragStartTimer;
// Periodic timer for continuous movement
Timer? _continuousMoveTimer;
Size? _lastScreenSize;
bool _isPressed = false;
@override
void initState() {
super.initState();
widget.cursorModel.blockEvents = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
_lastScreenSize = MediaQuery.of(context).size;
_resetPosition();
});
}
@override
void dispose() {
_stopSendEventTimer();
widget.cursorModel.blockEvents = false;
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentScreenSize = MediaQuery.of(context).size;
if (_lastScreenSize != null && _lastScreenSize != currentScreenSize) {
_resetPosition();
}
_lastScreenSize = currentScreenSize;
}
void _resetPosition() {
final size = MediaQuery.of(context).size;
setState(() {
_position = Offset(
_kSpaceToHorizontalEdge + _joystickRadius,
size.height * 0.5 + _joystickRadius * 1.5,
);
_isInitialized = true;
});
}
Offset _offsetToPanDelta(Offset offset) {
return Offset(
offset.dx / _joystickRadius,
offset.dy / _joystickRadius,
);
}
void _stopSendEventTimer() {
_dragStartTimer?.cancel();
_continuousMoveTimer?.cancel();
_dragStartTimer = null;
_continuousMoveTimer = null;
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Positioned(child: Offstage());
}
return Positioned(
left: _position.dx - _joystickRadius,
top: _position.dy - _joystickRadius,
child: GestureDetector(
onPanStart: (details) {
setState(() {
_isPressed = true;
});
widget.cursorModel.blockEvents = true;
_updateOffset(details.localPosition);
// 1. Send a single, small pan event immediately for responsiveness.
// The movement is small for a gentle start.
final initialDelta = _offsetToPanDelta(_offset);
if (initialDelta.distance > 0) {
widget.cursorModel.updatePan(initialDelta, Offset.zero, false);
}
// 2. Start a one-shot timer to check if the user is holding for a drag.
_dragStartTimer?.cancel();
_dragStartTimer = Timer(const Duration(milliseconds: 120), () {
// 3. If the timer fires, it's a drag. Start the continuous movement timer.
_continuousMoveTimer?.cancel();
_continuousMoveTimer =
periodic_immediate(const Duration(milliseconds: 20), () async {
if (_offset != Offset.zero) {
widget.cursorModel.updatePan(
_offsetToPanDelta(_offset) * _moveStep * _speed,
Offset.zero,
false);
}
});
});
},
onPanUpdate: (details) {
_updateOffset(details.localPosition);
},
onPanEnd: (details) {
setState(() {
_offset = Offset.zero;
_isPressed = false;
});
widget.cursorModel.blockEvents = false;
// 4. Critical step: On pan end, cancel all timers.
// If it was a flick, this cancels the drag detection before it fires.
// If it was a drag, this stops the continuous movement.
_stopSendEventTimer();
},
child: CustomPaint(
size: Size(_joystickRadius * 2, _joystickRadius * 2),
painter: _JoystickPainter(
_offset, _joystickRadius, _thumbRadius, _isPressed),
),
),
);
}
void _updateOffset(Offset localPosition) {
final center = Offset(_joystickRadius, _joystickRadius);
final offset = localPosition - center;
final distance = offset.distance;
if (distance <= _joystickRadius) {
setState(() {
_offset = offset;
});
} else {
final clampedOffset = offset / distance * _joystickRadius;
setState(() {
_offset = clampedOffset;
});
}
}
}
class _JoystickPainter extends CustomPainter {
final Offset _offset;
final double _joystickRadius;
final double _thumbRadius;
final bool _isPressed;
_JoystickPainter(
this._offset, this._joystickRadius, this._thumbRadius, this._isPressed);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final joystickColor = _kDefaultColor;
final borderColor = _isPressed ? _kTapDownColor : _kDefaultBorderColor;
final thumbColor = _kWidgetHighlightColor;
final joystickPaint = Paint()
..color = joystickColor
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final thumbPaint = Paint()
..color = thumbColor
..style = PaintingStyle.fill;
// Draw joystick base and border
canvas.drawCircle(center, _joystickRadius, joystickPaint);
canvas.drawCircle(center, _joystickRadius, borderPaint);
// Draw thumb
final thumbCenter = center + _offset;
canvas.drawCircle(thumbCenter, _thumbRadius, thumbPaint);
}
@override
bool shouldRepaint(covariant _JoystickPainter oldDelegate) {
return oldDelegate._offset != _offset ||
oldDelegate._isPressed != _isPressed;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:toggle_switch/toggle_switch.dart';
class GestureIcons {
@@ -35,20 +36,27 @@ typedef OnTouchModeChange = void Function(bool);
class GestureHelp extends StatefulWidget {
GestureHelp(
{Key? key, required this.touchMode, required this.onTouchModeChange})
{Key? key,
required this.touchMode,
required this.onTouchModeChange,
required this.virtualMouseMode})
: super(key: key);
final bool touchMode;
final OnTouchModeChange onTouchModeChange;
final VirtualMouseMode virtualMouseMode;
@override
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
State<StatefulWidget> createState() =>
_GestureHelpState(touchMode, virtualMouseMode);
}
class _GestureHelpState extends State<GestureHelp> {
late int _selectedIndex;
late bool _touchMode;
final VirtualMouseMode _virtualMouseMode;
_GestureHelpState(bool touchMode) {
_GestureHelpState(bool touchMode, VirtualMouseMode virtualMouseMode)
: _virtualMouseMode = virtualMouseMode {
_touchMode = touchMode;
_selectedIndex = _touchMode ? 1 : 0;
}
@@ -68,31 +76,144 @@ class _GestureHelpState extends State<GestureHelp> {
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ToggleSwitch(
initialLabelIndex: _selectedIndex,
activeFgColor: Colors.white,
inactiveFgColor: Colors.white60,
activeBgColor: [MyTheme.accent],
inactiveBgColor: Theme.of(context).hintColor,
totalSwitches: 2,
minWidth: 150,
fontSize: 15,
iconSize: 18,
labels: [translate("Mouse mode"), translate("Touch mode")],
icons: [Icons.mouse, Icons.touch_app],
onToggle: (index) {
setState(() {
if (_selectedIndex != index) {
_selectedIndex = index ?? 0;
_touchMode = index == 0 ? false : true;
widget.onTouchModeChange(_touchMode);
}
});
},
Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ToggleSwitch(
initialLabelIndex: _selectedIndex,
activeFgColor: Colors.white,
inactiveFgColor: Colors.white60,
activeBgColor: [MyTheme.accent],
inactiveBgColor: Theme.of(context).hintColor,
totalSwitches: 2,
minWidth: 150,
fontSize: 15,
iconSize: 18,
labels: [
translate("Mouse mode"),
translate("Touch mode")
],
icons: [Icons.mouse, Icons.touch_app],
onToggle: (index) {
setState(() {
if (_selectedIndex != index) {
_selectedIndex = index ?? 0;
_touchMode = index == 0 ? false : true;
widget.onTouchModeChange(_touchMode);
}
});
},
),
Transform.translate(
offset: const Offset(-10.0, 0.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: _virtualMouseMode.showVirtualMouse,
onChanged: (value) async {
if (value == null) return;
await _virtualMouseMode.toggleVirtualMouse();
setState(() {});
},
),
InkWell(
onTap: () async {
await _virtualMouseMode.toggleVirtualMouse();
setState(() {});
},
child: Text(translate('Show virtual mouse')),
),
],
),
),
if (_touchMode && _virtualMouseMode.showVirtualMouse)
Padding(
// Indent "Virtual mouse size"
padding: const EdgeInsets.only(left: 24.0),
child: SizedBox(
width: 260,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: 0.0, bottom: 0),
child: Text(translate('Virtual mouse size')),
),
Transform.translate(
offset: Offset(-0.0, -6.0),
child: Row(
children: [
Padding(
padding:
const EdgeInsets.only(left: 0.0),
child: Text(translate('Small')),
),
Expanded(
child: Slider(
value: _virtualMouseMode
.virtualMouseScale,
min: 0.8,
max: 1.8,
divisions: 10,
onChanged: (value) {
_virtualMouseMode
.setVirtualMouseScale(value);
setState(() {});
},
),
),
Padding(
padding:
const EdgeInsets.only(right: 16.0),
child: Text(translate('Large')),
),
],
),
),
],
),
),
),
if (!_touchMode && _virtualMouseMode.showVirtualMouse)
Transform.translate(
offset: const Offset(-10.0, -12.0),
child: Padding(
// Indent "Show virtual joystick"
padding: const EdgeInsets.only(left: 24.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value:
_virtualMouseMode.showVirtualJoystick,
onChanged: (value) async {
if (value == null) return;
await _virtualMouseMode
.toggleVirtualJoystick();
setState(() {});
},
),
InkWell(
onTap: () async {
await _virtualMouseMode
.toggleVirtualJoystick();
setState(() {});
},
child: Text(
translate("Show virtual joystick")),
),
],
)),
),
],
),
),
const SizedBox(height: 30),
Container(
child: Wrap(
spacing: space,

View File

@@ -140,7 +140,7 @@ class AbModel {
debugPrint("pull ab list");
List<AbProfile> abProfiles = List.empty(growable: true);
abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName,
gFFI.userModel.userName.value, null, ShareRule.read.value));
gFFI.userModel.userName.value, null, ShareRule.read.value, null));
// get all address book name
await _getSharedAbProfiles(abProfiles);
addressbooks.removeWhere((key, value) =>
@@ -208,7 +208,7 @@ class AbModel {
return false;
}
Map<String, dynamic> json =
_jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
@@ -234,7 +234,7 @@ class AbModel {
return false;
}
Map<String, dynamic> json =
_jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
@@ -271,7 +271,7 @@ class AbModel {
headers['Content-Type'] = "application/json";
final resp = await http.post(uri, headers: headers);
Map<String, dynamic> json =
_jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
@@ -319,8 +319,8 @@ class AbModel {
// #endregion
// #region peer
Future<String?> addIdToCurrent(
String id, String alias, String password, List<dynamic> tags) async {
Future<String?> addIdToCurrent(String id, String alias, String password,
List<dynamic> tags, String note) async {
if (currentAbPeers.where((element) => element.id == id).isNotEmpty) {
return "$id already exists in address book $_currentName";
}
@@ -333,6 +333,9 @@ class AbModel {
if (password.isNotEmpty) {
peer['password'] = password;
}
if (note.isNotEmpty) {
peer['note'] = note;
}
final ret = await addPeersTo([peer], _currentName.value);
_syncAllFromRecent = true;
return ret;
@@ -376,6 +379,14 @@ class AbModel {
return res;
}
Future<bool> changeNote({required String id, required String note}) async {
bool res = await current.changeNote(id: id, note: note);
await pullNonLegacyAfterChange();
currentAbPeers.refresh();
// no need to save cache
return res;
}
Future<bool> changePersonalHashPassword(String id, String hash) async {
var ret = false;
final personalAb = addressbooks[_personalAddressBookName];
@@ -609,7 +620,7 @@ class AbModel {
if (name == null || guid == null) {
continue;
}
ab = Ab(AbProfile(guid, name, '', '', ShareRule.read.value),
ab = Ab(AbProfile(guid, name, '', '', ShareRule.read.value, null),
name == _personalAddressBookName);
}
addressbooks[name] = ab;
@@ -658,6 +669,15 @@ class AbModel {
}
}
String getPeerNote(String id) {
final it = currentAbPeers.where((p0) => p0.id == id);
if (it.isEmpty) {
return '';
} else {
return it.first.note;
}
}
Color getCurrentAbTagColor(String tag) {
if (tag == kUntagged) {
return MyTheme.accent;
@@ -767,6 +787,28 @@ class AbModel {
_peerIdUpdateListeners.remove(key);
}
String? getdefaultSharedPassword() {
if (current.isPersonal()) {
return null;
}
final profile = current.sharedProfile();
if (profile == null) {
return null;
}
try {
if (profile.info is Map) {
final password = (profile.info as Map)['password'];
if (password is String && password.isNotEmpty) {
return password;
}
}
return null;
} catch (e) {
debugPrint("getdefaultSharedPassword: $e");
return null;
}
}
// #endregion
}
@@ -841,6 +883,8 @@ abstract class BaseAb {
Future<bool> changeAlias({required String id, required String alias});
Future<bool> changeNote({required String id, required String note});
Future<bool> changePersonalHashPassword(String id, String hash);
Future<bool> changeSharedPassword(String id, String password);
@@ -925,7 +969,7 @@ class LegacyAb extends BaseAb {
peers.clear();
} else if (resp.body.isNotEmpty) {
Map<String, dynamic> json =
_jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
} else if (json.containsKey('data')) {
@@ -983,7 +1027,7 @@ class LegacyAb extends BaseAb {
ret = true;
} else {
Map<String, dynamic> json =
_jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
} else if (resp.statusCode == 200) {
@@ -1068,6 +1112,12 @@ class LegacyAb extends BaseAb {
return await pushAb();
}
@override
Future<bool> changeNote({required String id, required String note}) async {
// no need to implement
return false;
}
@override
Future<bool> changeSharedPassword(String id, String password) async {
// no need to implement
@@ -1359,7 +1409,7 @@ class Ab extends BaseAb {
final resp = await http.post(uri, headers: headers);
statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
@@ -1416,7 +1466,7 @@ class Ab extends BaseAb {
final resp = await http.post(uri, headers: headers);
statusCode = resp.statusCode;
List<dynamic> json =
_jsonDecodeRespList(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeRespList(decode_http_response(resp), resp.statusCode);
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
@@ -1527,6 +1577,27 @@ class Ab extends BaseAb {
}
}
@override
Future<bool> changeNote({required String id, required String note}) async {
try {
final api =
"${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
final body = jsonEncode({"id": id, "note": note});
final resp = await http.put(Uri.parse(api), headers: headers, body: body);
final errMsg = _jsonDecodeActionResp(resp);
if (errMsg.isNotEmpty) {
BotToast.showText(contentColor: Colors.red, text: errMsg);
return false;
}
return true;
} catch (err) {
debugPrint('changeNote err: ${err.toString()}');
return false;
}
}
Future<bool> _setPassword(Object bodyContent) async {
try {
final api =
@@ -1793,6 +1864,11 @@ class DummyAb extends BaseAb {
return false;
}
@override
Future<bool> changeNote({required String id, required String note}) async {
return false;
}
@override
Future<bool> changePersonalHashPassword(String id, String hash) async {
return false;

View File

@@ -30,15 +30,17 @@ enum SortBy {
class JobID {
int _count = 0;
int next() {
String v = bind.mainGetCommonSync(key: 'transfer-job-id');
try {
return int.parse(v);
if (!isWeb) {
String v = bind.mainGetCommonSync(key: 'transfer-job-id');
return int.parse(v);
}
} catch (e) {
// unreachable. But we still handle it to make it safe.
// If we return -1, we have to check it in the caller.
_count++;
return _count;
debugPrint("Failed to get transfer job id: $e");
}
// Finally increase the count if on the web or if failed to get the id.
_count++;
return _count;
}
}

View File

@@ -122,7 +122,7 @@ class GroupModel {
final resp = await http.get(uri, headers: getHttpHeaders());
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeResp(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
@@ -180,7 +180,7 @@ class GroupModel {
final resp = await http.get(uri, headers: getHttpHeaders());
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeResp(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
if (json['error'] == 'Admin required!' ||
json['error']
@@ -246,7 +246,7 @@ class GroupModel {
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
_jsonDecodeResp(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}

View File

@@ -371,6 +371,7 @@ class InputModel {
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
int get trackpadSpeed => _trackpadSpeed;
@@ -765,6 +766,11 @@ class InputModel {
command: command);
}
static Map<String, dynamic> getMouseEventMove() => {
'type': _kMouseEventMove,
'buttons': 0,
};
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
@@ -876,7 +882,7 @@ class InputModel {
void onPointHoverImage(PointerHoverEvent e) {
_stopFling = true;
if (isViewOnly) return;
if (isViewOnly && !showMyCursor) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (!isPhysicalMouse.value) {
isPhysicalMouse.value = true;
@@ -1037,7 +1043,7 @@ class InputModel {
if (isDesktop) _queryOtherWindowCoords = true;
_remoteWindowCoords = [];
_windowRect = null;
if (isViewOnly) return;
if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) {
@@ -1051,7 +1057,7 @@ class InputModel {
void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return;
if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
@@ -1060,7 +1066,7 @@ class InputModel {
}
void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return;
if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) {
@@ -1221,16 +1227,17 @@ class InputModel {
return false;
}
void handleMouse(
Map<String, dynamic>? processEventToPeer(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
}) {
if (isViewCamera) return;
if (isViewCamera) return null;
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
return;
return null;
}
var type = kMouseEventTypeDefault;
@@ -1247,7 +1254,7 @@ class InputModel {
isMove = true;
break;
default:
return;
return null;
}
evt['type'] = type;
@@ -1265,9 +1272,10 @@ class InputModel {
type,
onExit: onExit,
buttons: evt['buttons'],
moveCanvas: moveCanvas,
);
if (pos == null) {
return;
return null;
}
if (type != '') {
evt['x'] = '0';
@@ -1285,7 +1293,22 @@ class InputModel {
kForwardMouseButton: 'forward'
};
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
return evt;
}
Map<String, dynamic>? handleMouse(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
}) {
final evtToPeer =
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas);
if (evtToPeer != null) {
bind.sessionSendMouse(
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
}
return evtToPeer;
}
Point? handlePointerDevicePos(
@@ -1296,6 +1319,7 @@ class InputModel {
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
bool moveCanvas = true,
}) {
final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
@@ -1312,15 +1336,19 @@ class InputModel {
isMove = false;
canvas = coords.canvas;
rect = coords.remoteRect;
x -= coords.relativeOffset.dx / devicePixelRatio;
y -= coords.relativeOffset.dy / devicePixelRatio;
x -= isWindows
? coords.relativeOffset.dx / devicePixelRatio
: coords.relativeOffset.dx;
y -= isWindows
? coords.relativeOffset.dy / devicePixelRatio
: coords.relativeOffset.dy;
}
}
}
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
if (isMove) {
if (isMove && moveCanvas) {
parent.target!.canvasModel.moveDesktopMouse(x, y);
}
@@ -1338,15 +1366,21 @@ class InputModel {
}
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
var w = _windowRect!.width;
var h = _windowRect!.height;
if (isWindows) {
w /= devicePixelRatio;
h /= devicePixelRatio;
}
return x >= 0 && y >= 0 && x <= w && y <= h;
}
static RemoteWindowCoords? findRemoteCoords(double x, double y,
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
x *= devicePixelRatio;
y *= devicePixelRatio;
if (isWindows) {
x *= devicePixelRatio;
y *= devicePixelRatio;
}
for (final c in remoteWindowCoords) {
if (x >= c.relativeOffset.dx &&
y >= c.relativeOffset.dy &&

View File

@@ -42,6 +42,7 @@ import '../utils/image.dart' as img;
import '../common/widgets/dialog.dart';
import 'input_model.dart';
import 'platform_model.dart';
import 'package:flutter_hbb/utils/scale.dart';
import 'package:flutter_hbb/generated_bridge.dart'
if (dart.library.html) 'package:flutter_hbb/web/bridge.dart';
@@ -61,6 +62,7 @@ class CachedPeerData {
bool secure = false;
bool direct = false;
String streamType = '';
CachedPeerData();
@@ -74,6 +76,7 @@ class CachedPeerData {
'permissions': permissions,
'secure': secure,
'direct': direct,
'streamType': streamType,
});
}
@@ -92,6 +95,7 @@ class CachedPeerData {
});
data.secure = map['secure'];
data.direct = map['direct'];
data.streamType = map['streamType'];
return data;
} catch (e) {
debugPrint('Failed to parse CachedPeerData: $e');
@@ -110,9 +114,11 @@ class FfiModel with ChangeNotifier {
bool? _secure;
bool? _direct;
bool _touchMode = false;
late VirtualMouseMode virtualMouseMode;
Timer? _timer;
var _reconnects = 1;
bool _viewOnly = false;
bool _showMyCursor = false;
WeakReference<FFI> parent;
late final SessionID sessionId;
@@ -151,6 +157,7 @@ class FfiModel with ChangeNotifier {
bool get isPeerMobile => isPeerAndroid;
bool get viewOnly => _viewOnly;
bool get showMyCursor => _showMyCursor;
set inputBlocked(v) {
_inputBlocked = v;
@@ -160,6 +167,7 @@ class FfiModel with ChangeNotifier {
clear();
sessionId = parent.target!.sessionId;
cachedPeerData.permissions = _permissions;
virtualMouseMode = VirtualMouseMode(this);
}
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
@@ -223,27 +231,45 @@ class FfiModel with ChangeNotifier {
timerScreenshot?.cancel();
}
setConnectionType(String peerId, bool secure, bool direct) {
setConnectionType(
String peerId, bool secure, bool direct, String streamType) {
cachedPeerData.secure = secure;
cachedPeerData.direct = direct;
cachedPeerData.streamType = streamType;
_secure = secure;
_direct = direct;
try {
var connectionType = ConnectionTypeState.find(peerId);
connectionType.setSecure(secure);
connectionType.setDirect(direct);
connectionType.setStreamType(streamType);
} catch (e) {
//
}
}
Widget? getConnectionImage() {
Widget? getConnectionImageText() {
if (secure == null || direct == null) {
return null;
} else {
final icon =
'${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}';
return SvgPicture.asset('assets/$icon.svg', width: 48, height: 48);
final iconWidget =
SvgPicture.asset('assets/$icon.svg', width: 48, height: 48);
String connectionText =
getConnectionText(secure!, direct!, cachedPeerData.streamType);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
iconWidget,
SizedBox(height: 4),
Text(
connectionText,
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
],
);
}
}
@@ -260,7 +286,7 @@ class FfiModel with ChangeNotifier {
'link': '',
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
setConnectionType(peerId, data.secure, data.direct, data.streamType);
await handlePeerInfo(data.peerInfo, peerId, true);
for (final element in data.cursorDataList) {
updateLastCursorId(element);
@@ -289,8 +315,8 @@ class FfiModel with ChangeNotifier {
} else if (name == 'sync_platform_additions') {
handlePlatformAdditions(evt, sessionId, peerId);
} else if (name == 'connection_ready') {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
setConnectionType(peerId, evt['secure'] == 'true',
evt['direct'] == 'true', evt['stream_type'] ?? '');
} else if (name == 'switch_display') {
// switch display is kept for backward compatibility
handleSwitchDisplay(evt, sessionId, peerId);
@@ -836,10 +862,16 @@ class FfiModel with ChangeNotifier {
} else if (type == 'input-password') {
enterPasswordDialog(sessionId, dialogManager);
} else if (type == 'session-login' || type == 'session-re-login') {
enterUserLoginDialog(sessionId, dialogManager);
} else if (type == 'session-login-password' ||
type == 'session-login-password') {
enterUserLoginAndPasswordDialog(sessionId, dialogManager);
enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true);
} else if (type == 'session-login-password') {
enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'login_linux_tip', true);
} else if (type == 'terminal-admin-login') {
enterUserLoginDialog(
sessionId, dialogManager, 'terminal-admin-login-tip', false);
} else if (type == 'terminal-admin-login-password') {
enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'terminal-admin-login-tip', false);
} else if (type == 'restarting') {
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
hasCancel: false);
@@ -1075,9 +1107,23 @@ class FfiModel with ChangeNotifier {
if (isPeerAndroid) {
_touchMode = true;
} else {
_touchMode = await bind.sessionGetOption(
sessionId: sessionId, arg: kOptionTouchMode) !=
'';
// `kOptionTouchMode` is originally peer option, but it is moved to local option later.
// We check local option first, if not set, then check peer option.
// Because if local option is not empty:
// 1. User has set the touch mode explicitly.
// 2. The advanced option (custom client) is set.
// Then we choose to use the local option.
final optLocal = bind.mainGetLocalOption(key: kOptionTouchMode);
if (optLocal != '') {
_touchMode = optLocal == 'Y';
} else {
final optSession = await bind.sessionGetOption(
sessionId: sessionId, arg: kOptionTouchMode);
_touchMode = optSession != '';
}
}
if (isMobile) {
virtualMouseMode.loadOptions();
}
if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady();
@@ -1117,6 +1163,8 @@ class FfiModel with ChangeNotifier {
peerId,
bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionToggleViewOnly));
setShowMyCursor(bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionToggleShowMyCursor));
}
if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) {
final platformAdditions = evt['platform_additions'];
@@ -1467,6 +1515,79 @@ class FfiModel with ChangeNotifier {
notifyListeners();
}
}
void setShowMyCursor(bool value) {
if (_showMyCursor != value) {
_showMyCursor = value;
notifyListeners();
}
}
}
class VirtualMouseMode with ChangeNotifier {
bool _showVirtualMouse = false;
double _virtualMouseScale = 1.0;
bool _showVirtualJoystick = false;
bool get showVirtualMouse => _showVirtualMouse;
double get virtualMouseScale => _virtualMouseScale;
bool get showVirtualJoystick => _showVirtualJoystick;
FfiModel ffiModel;
VirtualMouseMode(this.ffiModel);
bool _shouldShow() => !ffiModel.isPeerAndroid;
setShowVirtualMouse(bool b) {
if (b == _showVirtualMouse) return;
if (_shouldShow()) {
_showVirtualMouse = b;
notifyListeners();
}
}
setVirtualMouseScale(double s) {
if (s <= 0) return;
if (s == _virtualMouseScale) return;
_virtualMouseScale = s;
bind.mainSetLocalOption(key: kOptionVirtualMouseScale, value: s.toString());
notifyListeners();
}
setShowVirtualJoystick(bool b) {
if (b == _showVirtualJoystick) return;
if (_shouldShow()) {
_showVirtualJoystick = b;
notifyListeners();
}
}
void loadOptions() {
_showVirtualMouse =
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y';
_virtualMouseScale = double.tryParse(
bind.mainGetLocalOption(key: kOptionVirtualMouseScale)) ??
1.0;
_showVirtualJoystick =
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y';
notifyListeners();
}
Future<void> toggleVirtualMouse() async {
await bind.mainSetLocalOption(
key: kOptionShowVirtualMouse, value: showVirtualMouse ? 'N' : 'Y');
setShowVirtualMouse(
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y');
}
Future<void> toggleVirtualJoystick() async {
await bind.mainSetLocalOption(
key: kOptionShowVirtualJoystick,
value: showVirtualJoystick ? 'N' : 'Y');
setShowVirtualJoystick(
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y');
}
}
class ImageModel with ChangeNotifier {
@@ -1661,6 +1782,8 @@ class ViewStyle {
final s2 = height / displayHeight;
s = s1 < s2 ? s1 : s2;
}
} else if (style == kRemoteViewStyleCustom) {
// Custom scale is session-scoped and applied in CanvasModel.updateViewStyle()
}
return s;
}
@@ -1777,7 +1900,13 @@ class CanvasModel with ChangeNotifier {
displayWidth: displayWidth,
displayHeight: displayHeight,
);
if (_lastViewStyle == viewStyle) {
// If only the Custom scale percent changed, proceed to update even if
// the basic ViewStyle fields are equal.
// In Custom scale mode, the scale percent can change independently of the other
// ViewStyle fields and is not captured by the equality check. Therefore, we must
// allow updates to proceed when style == kRemoteViewStyleCustom, even if the
// rest of the ViewStyle fields are unchanged.
if (_lastViewStyle == viewStyle && style != kRemoteViewStyleCustom) {
return;
}
if (_lastViewStyle.style != viewStyle.style) {
@@ -1786,12 +1915,30 @@ class CanvasModel with ChangeNotifier {
_lastViewStyle = viewStyle;
_scale = viewStyle.scale;
// Apply custom scale percent when in Custom mode
if (style == kRemoteViewStyleCustom) {
try {
_scale = await getSessionCustomScale(sessionId);
} catch (e, stack) {
debugPrint('Error in getSessionCustomScale: $e');
debugPrintStack(stackTrace: stack);
_scale = 1.0;
}
}
_devicePixelRatio = ui.window.devicePixelRatio;
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / _devicePixelRatio;
if (kIgnoreDpi) {
if (style == kRemoteViewStyleOriginal) {
_scale = 1.0 / _devicePixelRatio;
} else if (_scale != 0 && style == kRemoteViewStyleCustom) {
_scale /= _devicePixelRatio;
}
}
_resetCanvasOffset(displayWidth, displayHeight);
_imageOverflow.value = _x < 0 || y < 0;
final overflow = _x < 0 || y < 0;
if (_imageOverflow.value != overflow) {
_imageOverflow.value = overflow;
}
if (notify) {
notifyListeners();
}
@@ -1812,7 +1959,7 @@ class CanvasModel with ChangeNotifier {
tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
if (style != kRemoteViewStyleOriginal) {
if (style != kRemoteViewStyleOriginal && style != kRemoteViewStyleCustom) {
return;
}
@@ -2228,9 +2375,25 @@ class CursorModel with ChangeNotifier {
Rect? get keyHelpToolsRectToAdjustCanvas =>
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
_keyHelpToolsRect = r;
if (r == null) {
// The blocked rect is used to block the pointer/touch events in the remote page.
final List<Rect> _blockedRects = [];
// Used in shouldBlock().
// _blockEvents is a flag to block pointer/touch events on the remote image.
// It is set to true to prevent accidental touch events in the following scenarios:
// 1. In floating mouse mode, when the scroll circle is shown.
// 2. In floating mouse widgets mode, when the left/right buttons are moving.
// 3. In floating mouse widgets mode, when using the virtual joystick.
// When _blockEvents is true, all pointer/touch events are blocked regardless of the contents of _blockedRects.
// _blockedRects contains specific rectangular regions where events are blocked; these are checked when _blockEvents is false.
// In summary: _blockEvents acts as a global block, while _blockedRects provides fine-grained blocking.
bool _blockEvents = false;
List<Rect> get blockedRects => List.unmodifiable(_blockedRects);
set blockEvents(bool v) => _blockEvents = v;
keyHelpToolsVisibilityChanged(Rect? rect, bool keyboardIsVisible) {
_keyHelpToolsRect = rect;
if (rect == null) {
_lastIsBlocked = false;
} else {
// Block the touch event is safe here.
@@ -2245,6 +2408,14 @@ class CursorModel with ChangeNotifier {
_lastKeyboardIsVisible = keyboardIsVisible;
}
addBlockedRect(Rect rect) {
_blockedRects.add(rect);
}
removeBlockedRect(Rect rect) {
_blockedRects.remove(rect);
}
get lastIsBlocked => _lastIsBlocked;
ui.Image? get image => _image;
@@ -2311,13 +2482,22 @@ class CursorModel with ChangeNotifier {
// mobile Soft keyboard, block touch event from the KeyHelpTools
shouldBlock(double x, double y) {
if (_blockEvents) {
return true;
}
final offset = Offset(x, y);
for (final rect in _blockedRects) {
if (isPointInRect(offset, rect)) {
return true;
}
}
// For help tools rectangle, only block touch event when in touch mode.
if (!(parent.target?.ffiModel.touchMode ?? false)) {
return false;
}
if (_keyHelpToolsRect == null) {
return false;
}
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
if (_keyHelpToolsRect != null &&
isPointInRect(offset, _keyHelpToolsRect!)) {
return true;
}
return false;
@@ -2337,6 +2517,10 @@ class CursorModel with ChangeNotifier {
return true;
}
Future<void> syncCursorPosition() async {
await parent.target?.inputModel.moveMouse(_x, _y);
}
bool isInRemoteRect(Offset offset) {
return getRemotePosInRect(offset) != null;
}
@@ -3208,7 +3392,7 @@ class FFI {
}
void routeTerminalResponse(Map<String, dynamic> evt) {
final int terminalId = evt['terminal_id'] ?? 0;
final int terminalId = TerminalModel.getTerminalIdFromEvt(evt);
// Route to specific terminal model if it exists
final model = _terminalModels[terminalId];

View File

@@ -156,7 +156,10 @@ class PlatformFFI {
// only support for android
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
} else if (isIOS) {
_homeDir = _ffiBind.mainGetDataDirIos();
// The previous code was `_homeDir = (await getDownloadsDirectory())?.path ?? '';`,
// which provided the `downloads` path in the sandbox.
// It is unclear why we now use the `data` directory in the sandbox instead.
_homeDir = _ffiBind.mainGetDataDirIos(appDir: _dir);
} else {
// no need to set home dir
}

View File

@@ -20,6 +20,7 @@ class Peer {
bool online = false;
String loginName; //login username
String device_group_name;
String note;
bool? sameServer;
String getId() {
@@ -43,6 +44,7 @@ class Peer {
rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '',
device_group_name = json['device_group_name'] ?? '',
note = json['note'] is String ? json['note'] : '',
sameServer = json['same_server'];
Map<String, dynamic> toJson() {
@@ -60,6 +62,7 @@ class Peer {
"rdpUsername": rdpUsername,
'loginName': loginName,
'device_group_name': device_group_name,
'note': note,
'same_server': sameServer,
};
}
@@ -104,6 +107,7 @@ class Peer {
required this.rdpUsername,
required this.loginName,
required this.device_group_name,
required this.note,
this.sameServer,
});
@@ -122,6 +126,7 @@ class Peer {
rdpUsername: '',
loginName: '',
device_group_name: '',
note: '',
);
bool equal(Peer other) {
return id == other.id &&
@@ -136,7 +141,8 @@ class Peer {
rdpPort == other.rdpPort &&
rdpUsername == other.rdpUsername &&
device_group_name == other.device_group_name &&
loginName == other.loginName;
loginName == other.loginName &&
note == other.note;
}
Peer.copy(Peer other)
@@ -154,6 +160,7 @@ class Peer {
rdpUsername: other.rdpUsername,
loginName: other.loginName,
device_group_name: other.device_group_name,
note: other.note,
sameServer: other.sameServer);
}

View File

@@ -40,7 +40,7 @@ class PeerTabModel with ChangeNotifier {
List<bool> isEnabled = List.from([
true,
true,
!isWeb,
!isWeb && bind.mainGetLocalOption(key: "disable-discovery-panel") != "Y",
!(bind.isDisableAb() || bind.isDisableAccount()),
!(bind.isDisableGroupPanel() || bind.isDisableAccount()),
]);

View File

@@ -226,30 +226,6 @@ class ServerModel with ChangeNotifier {
notifyListeners();
}
/// Check iOS permissions for screen recording and microphone
checkIOSPermission() async {
// For iOS, we need to check screen recording permission
// This is typically done when user tries to start screen sharing
// microphone - only audio available on iOS
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
_audioOk = audioOption != 'N';
// file - Not available on iOS during screen share
_fileOk = false;
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
// clipboard - Not available on iOS during screen share
_clipboardOk = false;
bind.mainSetOption(key: kOptionEnableClipboard, value: "N");
// media/screen recording - will be checked when actually starting
_mediaOk = true;
_inputOk = true;
notifyListeners();
}
updatePasswordModel() async {
var update = false;
final temporaryPassword = await bind.mainGetTemporaryPassword();
@@ -335,14 +311,6 @@ class ServerModel with ChangeNotifier {
_audioOk = !_audioOk;
bind.mainSetOption(
key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
// For iOS, automatically restart the service to apply microphone change
// iOS ReplayKit sets microphoneEnabled when capture starts and cannot be changed dynamically
// Must restart capture with new microphone setting
if (isIOS && _isStart) {
_restartServiceForAudio();
}
notifyListeners();
}
@@ -523,25 +491,6 @@ class ServerModel with ChangeNotifier {
}
}
/// Restart service for iOS audio permission change
/// iOS ReplayKit requires setting microphoneEnabled at capture start time
/// Cannot dynamically enable/disable microphone during active capture session
_restartServiceForAudio() async {
if (!isIOS) return;
// Show a quick toast to inform user
showToast(translate("Restarting service to apply microphone change"));
// Stop the current capture
parent.target?.invokeMethod("stop_service");
// Small delay to ensure clean stop
await Future.delayed(Duration(milliseconds: 500));
// Start with new audio settings
parent.target?.invokeMethod("start_service");
}
changeStatue(String name, bool value) {
debugPrint("changeStatue value $value");
switch (name) {
@@ -836,7 +785,6 @@ class ServerModel with ChangeNotifier {
}
}
void androidUpdatekeepScreenOn() async {
if (!isAndroid) return;
var floatingWindowDisabled =

View File

@@ -1,7 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:xterm/xterm.dart';
import 'model.dart';
@@ -21,7 +25,20 @@ class TerminalModel with ChangeNotifier {
final _inputBuffer = <String>[];
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
Future<void> _handleInput(String data) async {
// If we press the `Enter` button on Android,
// `data` can be '\r' or '\n' when using different keyboards.
// Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline.
// Android -> Linux. Both '\r' and '\n' work as expected (execute a command).
// So when we receive '\n', we may need to convert it to '\r' to ensure compatibility.
// Desktop -> Desktop works fine.
// Check if we are on mobile or web(mobile), and convert '\n' to '\r'.
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
if (isMobileOrWebMobile && isPeerWindows && data == '\n') {
data = '\r';
}
if (_terminalOpened) {
// Send user input to remote terminal
try {
@@ -148,9 +165,58 @@ class TerminalModel with ChangeNotifier {
}
}
static int getTerminalIdFromEvt(Map<String, dynamic> evt) {
if (evt.containsKey('terminal_id')) {
final v = evt['terminal_id'];
if (v is int) {
// Desktop and mobile send terminal_id as an int
return v;
} else if (v is String) {
// Web sends terminal_id as a string
final parsed = int.tryParse(v);
if (parsed != null) {
return parsed;
} else {
debugPrint(
'[TerminalModel] Failed to parse terminal_id as integer: $v. Expected a numeric string.');
return 0;
}
} else {
// Unexpected type, log and handle gracefully
debugPrint(
'[TerminalModel] Unexpected terminal_id type: ${v.runtimeType}, value: $v. Expected int or String.');
return 0;
}
} else {
debugPrint('[TerminalModel] Event does not contain terminal_id');
return 0;
}
}
static bool getSuccessFromEvt(Map<String, dynamic> evt) {
if (evt.containsKey('success')) {
final v = evt['success'];
if (v is bool) {
// Desktop and mobile
return v;
} else if (v is String) {
// Web
return v.toLowerCase() == 'true';
} else {
// Unexpected type, log and handle gracefully
debugPrint(
'[TerminalModel] Unexpected success type: ${v.runtimeType}, value: $v. Expected bool or String.');
return false;
}
} else {
debugPrint('[TerminalModel] Event does not contain success');
return false;
}
}
void handleTerminalResponse(Map<String, dynamic> evt) {
final String? type = evt['type'];
final int evtTerminalId = evt['terminal_id'] ?? 0;
final int evtTerminalId = getTerminalIdFromEvt(evt);
// Only handle events for this terminal
if (evtTerminalId != terminalId) {
@@ -176,7 +242,7 @@ class TerminalModel with ChangeNotifier {
}
void _handleTerminalOpened(Map<String, dynamic> evt) {
final bool success = evt['success'] ?? false;
final bool success = getSuccessFromEvt(evt);
final String message = evt['message'] ?? '';
final String? serviceId = evt['service_id'];
@@ -195,6 +261,17 @@ class TerminalModel with ChangeNotifier {
debugPrint('[TerminalModel] Error processing buffered input: $e');
notifyListeners();
});
final persistentSessions =
evt['persistent_sessions'] as List<dynamic>? ?? [];
if (kWindowId != null && persistentSessions.isNotEmpty) {
DesktopMultiWindow.invokeMethod(
kWindowId!,
kWindowEventRestoreTerminalSessions,
jsonEncode({
'persistent_sessions': persistentSessions,
}));
}
} else {
terminal.write('Failed to open terminal: $message\r\n');
}
@@ -227,14 +304,14 @@ class TerminalModel with ChangeNotifier {
// Try to decode as base64 first
try {
final bytes = base64Decode(data);
text = utf8.decode(bytes);
text = utf8.decode(bytes, allowMalformed: true);
} catch (e) {
// If base64 decode fails, treat as plain text
text = data;
}
} else if (data is List) {
// Handle if data comes as byte array
text = utf8.decode(List<int>.from(data));
text = utf8.decode(List<int>.from(data), allowMalformed: true);
} else {
debugPrint('[TerminalModel] Unknown data type: ${data.runtimeType}');
return;

View File

@@ -66,7 +66,7 @@ class UserModel {
reset(resetOther: status == 401);
return;
}
final data = json.decode(utf8.decode(response.bodyBytes));
final data = json.decode(decode_http_response(response));
final error = data['error'];
if (error != null) {
throw error;
@@ -160,7 +160,7 @@ class UserModel {
final Map<String, dynamic> body;
try {
body = jsonDecode(utf8.decode(resp.bodyBytes));
body = jsonDecode(decode_http_response(resp));
} catch (e) {
debugPrint("login: jsonDecode resp body failed: ${e.toString()}");
if (resp.statusCode != 200) {

View File

@@ -354,6 +354,16 @@ class RustDeskMultiWindowManager {
bool? forceRelay,
String? connToken,
}) async {
// Iterate through terminal windows in reverse order to prioritize
// the most recently added or used windows, as they are more likely
// to have an active session.
for (final windowId in _terminalWindows.reversed) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return MultiWindowCallResult(windowId, null);
}
}
// Terminal windows should always create new windows, not reuse
// This avoids the MissingPluginException when trying to invoke
// new_terminal on an inactive window
@@ -366,7 +376,7 @@ class RustDeskMultiWindowManager {
"connToken": connToken,
};
final msg = jsonEncode(params);
// Always create a new window for terminal
final windowId = await newSessionWindow(
WindowType.Terminal, remoteId, msg, _terminalWindows, false);
@@ -460,9 +470,13 @@ class RustDeskMultiWindowManager {
if (windows.isEmpty) {
return;
}
for (final wId in windows) {
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
await saveWindowPosition(type, windowId: wId);
for (int i = 0; i < windows.length; i++) {
final wId = windows[i];
final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1;
if (shouldSavePos) {
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
await saveWindowPosition(type, windowId: wId);
}
try {
await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close();

View File

@@ -0,0 +1,34 @@
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:uuid/uuid.dart';
/// Clamp custom scale percent to supported bounds.
/// Keep this in sync with the slider's minimum in the desktop toolbar UI.
///
/// This function exists to ensure consistent clamping behavior across the app
/// and to provide a single point of reference for the valid scale range.
int clampCustomScalePercent(int percent) {
return percent.clamp(kScaleCustomMinPercent, kScaleCustomMaxPercent);
}
/// Parse a string percent and clamp. Defaults to 100 when invalid.
int parseCustomScalePercent(String? s, {int defaultPercent = 100}) {
final parsed = int.tryParse(s ?? '') ?? defaultPercent;
return clampCustomScalePercent(parsed);
}
/// Convert a percent value to scale factor after clamping.
double percentToScale(int percent) => clampCustomScalePercent(percent) / 100.0;
/// Fetch, parse and clamp the custom scale percent for a session.
Future<int> getSessionCustomScalePercent(UuidValue sessionId) async {
final opt = await bind.sessionGetFlutterOption(
sessionId: sessionId, k: kCustomScalePercentKey);
return parseCustomScalePercent(opt);
}
/// Fetch and compute the custom scale factor for a session.
Future<double> getSessionCustomScale(UuidValue sessionId) async {
final p = await getSessionCustomScalePercent(sessionId);
return percentToScale(p);
}

View File

@@ -908,8 +908,18 @@ class RustdeskImpl {
return js.context.callMethod('getByName', ['option:local', key]);
}
// Do not return the real environment variables.
// Use the global variable as the environment variable in web.
String mainGetEnv({required String key, dynamic hint}) {
throw UnimplementedError("mainGetEnv");
return js.context.callMethod('getByName', ['envvar', key]);
}
// Use the global variable as the environment variable in web.
void mainSetEnv({required String key, String? value, dynamic hint}) {
js.context.callMethod('setByName', [
'envvar',
jsonEncode({'name': key, 'value': value})
]);
}
Future<void> mainSetLocalOption(
@@ -1960,9 +1970,7 @@ class RustdeskImpl {
}
Future<void> sessionCloseTerminal(
{required UuidValue sessionId,
required int terminalId,
dynamic hint}) {
{required UuidValue sessionId, required int terminalId, dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [
'close_terminal',
jsonEncode({

View File

@@ -433,7 +433,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = HZF9JMC8YN;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -579,7 +579,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = HZF9JMC8YN;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -609,7 +609,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = HZF9JMC8YN;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (

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