411 Commits

Author SHA1 Message Date
rustdesk
4423e0a1dc Fix mouse hover issue on menu bar when controlling Mac from iPad
Fixes #8789

The issue was that when using the virtual/floating mouse on mobile
devices to control a Mac, mouse clicks would occur at the wrong
position, particularly noticeable in the menu bar where hover menus
would disappear immediately.

Root cause: The tapDown() method was sending mouse button events
without ensuring the cursor position was updated on the server side
first. This caused clicks to register at stale cursor positions.

Solution: Before sending a mouse down event, check if the pointer
has moved since entering the session. If not, send a mouse move
event first to update the cursor position, then wait briefly before
sending the click event.

This ensures the remote cursor is at the correct position before
any mouse button action is performed.
2025-11-19 00:31:38 +08:00
fufesou
0a672f092a fix: flatpak, wayland, cursor image (#13544)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-18 23:32:40 +08:00
alonginwind
ef62f1db29 Fix terminal clear command to remove residual output (#13531)
* Fix terminal clear command to remove residual output

* Update flutter/lib/models/terminal_model.dart

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

* Update flutter/lib/desktop/pages/terminal_page.dart

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

* Fix: Prevent "Build scheduled during frame" in terminal resize

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 23:31:54 +08:00
fufesou
7f804a0e45 refact: wayland, pipewire display offset cache to file (#13542)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-18 14:16:59 +08:00
fufesou
b2dff336ce fix: wayland controlled side, cursor misalignment (#13537)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-18 00:37:15 +08:00
fufesou
a6571e71e4 feat: macos, update dmg (#13539)
* feat: macos, update dmg

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

* Update src/platform/macos.rs

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

* fix: macos update, remove temp update dir

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

* refact: macos update, print

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:30:23 +08:00
rustdesk
81f711eb00 update lock 2025-11-17 23:09:29 +08:00
rustdesk
c8a8e06558 update common 2025-11-17 19:09:50 +08:00
rustdesk
2c079f53a9 web client custom 2025-11-17 00:30:38 +08:00
Lynilia
322ffe288e Update fr.rs (#13533) 2025-11-16 23:47:25 +08:00
Mr-Update
c340eb0e57 Update de.rs (#13529) 2025-11-15 21:07:09 +08:00
rustdesk
4e953291ed fix ci android failure 2025-11-15 15:00:29 +08:00
solokot
1dea5fee0e Update ru.rs (#13518) 2025-11-14 17:02:33 +08:00
VenusGirl❤
9f24b46fee Update Korean (#13516) 2025-11-14 17:02:21 +08:00
bovirus
0808c41a1c Italian language update (#13513) 2025-11-14 17:02:09 +08:00
21pages
296c6df462 ask for note at end of connection (#13499)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-11-13 23:35:40 +08:00
Dzung Do
13ee3e907d Update Vietnamese translations for various terms (#13490) 2025-11-12 16:26:17 +08:00
Jonathan Gilbert
ce7d794b4c Fix config sync reconnection retry loop (#13487)
* Updated the server connection retry loop when syncing config changes in src/server.rs to not break after reconnecting.

* Update server.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-11-12 00:27:58 +08:00
Alireza Shahamiri
fb10069632 fix(fa.rs): Fixes persian translation typo (#13459) 2025-11-11 14:45:38 +08:00
21pages
43a7677644 add user_group.py, device_group.py, update users.py (#13453)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-11-11 14:45:04 +08:00
fufesou
58fa32d7ea fix: sciter ui (#13474)
Element has no method - is_outgoing_only.

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-10 22:30:20 +08:00
fufesou
934d6c3987 refact: rust backtrace logs (#13467)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-10 15:43:46 +08:00
rustdesk
2d7c6ef21f log crash traceback, copilot 2025-11-10 10:01:15 +08:00
Alex Rijckaert
99a97e6a6c Update Dutch translations in nl.rs (#13457) 2025-11-09 21:30:48 +08:00
rustdesk
017a10e8c8 1.4.4 2025-11-07 15:16:59 +08:00
Mr-Update
41ffa8ba08 Update de.rs (#13448) 2025-11-07 15:15:38 +08:00
21pages
e029d00cfa edge scroll thickness adjustment (#13445)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-11-07 01:15:13 +08:00
Lynilia
268534d5e7 Update fr.rs (#13438) 2025-11-06 17:13:27 +08:00
fufesou
a7d2bc63f9 fix: sciter, advanced options, UI (#13429)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-06 17:13:11 +08:00
bovirus
559115c43c Italian language update (#13414)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-11-05 17:53:35 +08:00
Greg
1277c7d60c See https://github.com/rustdesk/rustdesk/discussions/13350 (#13427)
This adds DBUS_SESSION_BUS_ADDRESS to the collection of "pilfered environment" variables on Linux.

The net effect should be that Wayland sub processes launched by rustdesk --service (--server and --tray) get the right bus.

Presumably this happens with by systemd environment management, but on Void Linux & other non-systemd, this prevents a connection to a client from any controller with a message about service not available. (As the DBUS lookup fails).

On X11, this is not an issue as the retrieval of Wayland capabilities via DBUS registry is not required.

In general, this is a more robust Wayland solution than just grabbing WAYLAND_DISPLAY, since WAYLAND is heavily dependent on DBUS for protocol registration.

Co-authored-by: Greg Ke <Greg-repo-sync@akua.com>
2025-11-05 10:29:37 +08:00
fufesou
9b69c7e972 refact: show proxy settings on ios (#13423)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-04 17:55:04 +08:00
Jonathan Gilbert
a903f710ea Eliminate build warnings from the Scrap crate (#13383)
* Updated build.rs to tell RustC that dxgi, quartz and x11 are expected configurations.
Added lifetime annotations to various methods in common/aom.rs and common/vpxcodec.rs.
Updated common/vpx.rs to allow unused_imports in the generated bindings.
Updated dxgi/mag.rs to allow non_snake_case identifiers like "dwFilterMode".

* Added lifetime annotations to methods in common/hwcodec.rs and common/vram.rs.

* Switched syntax for the rustc-check-cfg directive emitted by build.rs in the scrap crate to use syntax compatible with Rust toolchain version 1.75. The double-colon syntax requires 1.77 or newer, but the older single-colon syntax works fine on newer versions for this directive.

* Update libs/scrap/build.rs

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

* Revert apparently-erroneous AI suggestion. It's usually pretty good, but not always right it seems. :-)

This reverts commit bf862b13f6.

* Removed redundant configuration directives from libs/scrap/build.rs.

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-04 10:19:13 +08:00
21pages
b75f4daa47 flutter: keep chat window within screen bounds to prevent hidden chat window (fixes rustdesk#13397) (#13406) 2025-11-04 09:44:13 +08:00
fufesou
fef44ffa57 refact: translate tip id (#13412)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-04 08:56:43 +08:00
fufesou
5a812e3b2f fix: ui issues (#13381)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-03 23:23:08 +08:00
fufesou
910dcf2036 refact: tls, native-tls fallback rustls-tls (#13263)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-11-03 23:21:01 +08:00
21pages
44a28aa5bd update hwcodec, support H265 encoding on Intel chip Macs (#13411)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-11-03 22:55:03 +08:00
21pages
f7f947beb9 restrict CLI options when settings disabled (#13400)
Prevent --password, --set-id, and --option command line arguments
from modifying settings when is_disable_settings() returns true.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-11-03 09:43:27 +08:00
RustDesk
d03a9e2baf Fix macos bigsur cvbuffer crash (#13392)
* Fix macOS Big Sur crash with CVBufferCopyAttachments

Add FFmpeg patch to use weak_import for CVBufferCopyAttachments API
to prevent dyld crash on macOS Big Sur (11.x).

The CVBufferCopyAttachments function is only available on macOS 12+.
Even though FFmpeg has a runtime check with __builtin_available, the
symbol is still resolved at load time, causing immediate crash on older
macOS versions.

With weak_import attribute, the function pointer will be NULL on
macOS < 12, allowing the code to safely fall back to the deprecated
CVBufferGetAttachments API.

Fixes: #13377

* update common
2025-11-02 22:08:03 +08:00
Mr-Update
ca22316e95 Update de.rs (#13375) 2025-11-02 21:20:56 +08:00
solokot
ef99c479aa Update ru.rs (#13367) 2025-11-02 21:20:42 +08:00
Jonathan Gilbert
fa9260c763 Made the import of hbb_common::sysinfo::System more precisely conditioned in src/platform/mod.rs. (#13388) 2025-11-02 21:19:44 +08:00
Jonathan Gilbert
fab11c8ffa Allow non_snake_case identifiers in src/setup/mod.rs for libs/remote_printer. (#13384) 2025-11-02 21:19:13 +08:00
VenusGirl❤
9bd9658a92 Update Korean (#13359)
Update Korean
2025-11-01 14:40:28 +08:00
bovirus
213880c14d Italian language update (#13358) 2025-11-01 14:40:17 +08:00
Alessandro De Blasis
0550397046 fix: scale custom on mobile (#13324)
* fix: prevent custom scale dialog from closing when interacting with slider

Wrapped MobileCustomScaleControls in GestureDetector with opaque behavior
to prevent touch events from propagating to parent dialog's clickMaskDismiss
handler. The slider now works correctly without closing the dialog.

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

* Update flutter/lib/mobile/widgets/custom_scale_widget.dart

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

* Update flutter/lib/mobile/widgets/custom_scale_widget.dart

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

* Update flutter/lib/mobile/widgets/custom_scale_widget.dart

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

* Update flutter/lib/mobile/widgets/custom_scale_widget.dart

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

* Revert "fix: mobile remove "Scale custom" (#13323)"

This reverts commit 265d08fc3b.

* chore: keep remote_toolbar.dart cleanup (remove dead code)

  The dead code removed in 265d08fc3 hasn't been used since Aug 2023.
  Only reverting toolbar.dart is needed for the mobile Scale custom fix.

* Update flutter/lib/mobile/pages/remote_page.dart

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

* refactor: Implement CustomScaleControlsMixin for shared scaling logic across mobile and desktop widgets

- Introduced a new mixin `CustomScaleControlsMixin` to encapsulate custom scale control logic, allowing for code reuse in both mobile and desktop widgets.
- Refactored `_CustomScaleMenuControlsState` and `_MobileCustomScaleControlsState` to utilize the new mixin, simplifying the scaling logic and reducing code duplication.
- Updated slider handling and state management to leverage the mixin's methods for improved maintainability.

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>

* Update flutter/lib/mobile/widgets/custom_scale_widget.dart

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

* Update flutter/lib/mobile/pages/remote_page.dart

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

* refactor: changed from mixin to abstract class

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

* Revert "Update flutter/lib/mobile/pages/remote_page.dart"

This reverts commit 7c35897408.

* refactor: remove unnecessary tap event handling in custom scale controls

- Removed the `onTap` handler from the

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

* refactor: simplify MobileCustomScaleControls usage in remote_page.dart

- Removed unnecessary GestureDetector wrapper around MobileCustomScaleControls for cleaner code.

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>
2025-10-31 11:08:03 +08:00
21pages
f7a5a506f6 rename RustDeskApplication to MainApplication (#13362)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-31 11:07:32 +08:00
21pages
0f34c50bd2 fix reqwest proxy auth (#13354)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-30 20:01:17 +08:00
Jonathan Gilbert
055826e26f Edge scrolling (#13247)
* Repurposed the MacOS-specific platform channel mechanism for all platforms:
- Renamed the channel from "org.rustdesk.rustdesk/macos" to "org.rustdesk.rustdesk/host".
- Renamed _osxMethodChannel in platform_channel.dart to _hostMethodChannel.
- Updated linux/my_application.cc to use the fl_* API to set up a Method Channel and to dispose it during my_application_dispose.
- Updated windows/runner/flutter_window.cpp to use the C++ API to set up a Method Channel.
- Updated the channel name in macos/Runner/MainFlutterWindow.swift.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Added a method "bumpMouse" to the Platform Channel.
Added a thunk to call the method through the channel to platform_channel.dart.
Added implementation bump_mouse() in linux/my_application.cc using Gdk API calls. Updated host_channel_call_handler to process "bumpMouse" method call messages by calling bump_mouse.
Added implementation Win32Desktop::BumpMouse in windows/runner/win32_desktop.cpp/.h.  Updated the inline method call handler in flutter_window.cpp to handle "bumpMouse" method calls by calling Win32Desktop::BumpMouse.
Updated the method call handler in macos/Runner/MainFlutterWindow.swift to handle "bumpMouse" method call messages. Updated MainFlutterWindow to use a subclass of FlutterViewController exposing access to mouseLocationOutsideOfEventStream.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Added message type kWindowBumpMouse to the multiwindow window event model:
- Added constant kWindowBumpMouse to consts.dart.
- Updated the method handler attached to rustDeskWinManager by DesktopHomePageState to recognize kWindowBumpMouse and translate it to a call to RdPlatformChannel.bumpMouse.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Centralized serialization of ScrollStyle values, moving JSON and string conversions into methods toString/fromString and toJson/fromJson within the type.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Added new scroll style for edge scrolling:
- Added ScrollStyle enum member "scrolledge". Added corresponding constant kRemoteScrollStyleEdge to consts.dart for the string serialized form.
- Updated sites checking specifically for ScrollStyle.scrollbar to instead check for NOT ScrollStyle.scrollauto.
- Added radio buttons for the new "ScrollEdge" style to desktop_setting_page.dart and remote_toolbar.dart. Added new string "ScrollEdge" to lang/template.rs.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Implemented edge scrolling:
- Added methods edgeScrollMouse and pushScrollPositionToUI to class CanvasModel in model.dart.
- Added boolean parameter edgeScroll to handleMouse, handlePointerDevicePos and processEventToPeer in input_model.dart.
- Updated handlePointerDevicePos in input_model.dart to call edgeScrollMouse on move events when the edgeScroll parameter is true.
- Added convenience accessor useEdgeScroll to the InputModel class. Updated call sites to handleMouse to use it to supply the value for the edgeScroll parameter.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Updated CanvasModel.edgeScrollMouse to be resilient to receiving events when _horizontal/_vertical aren't wired up to any UI.

* Updated CanvasModel to take notifications of resizes via method notifyResize and to suppress edge scrolling briefly after a resize.
Updated the onWindowResized handler in tabbar_widget.dart to call notifyResize on the canvasModel of any RemotePage tabs.

* Half a go at fixing MainFlutterWindow.swift.

* Copilot feedback.

* Applied fix suggested by Copilot in its explanation of the build error.

* Fixed a couple of silly errors in windows/runner/flutter_window.cpp.

* Fixed MainFlutterWindow.swift build errors.

Co-Authored-By: fufesou <linlong1266@gmail.com>
Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Moved new translation to the end of template.rs.
Reran res/lang.py.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Switched MainFlutterWindow.swift to use NSEvent.mouseLocation.

* Updated MainFlutterWindow.swift code based on build error.

* Fixed silly typo.

* Reintroduced the coordinate system translation in MainFlutterWindow.swift.

* Updated edgeScrollMouse in model.dart to add a "safe zone" around the window frame that doesn't trigger edge scrolling.

* Updated the bumpMouse handler in MainFlutterWindow.swift to call CGAssociateMouseAndMouseCursorPosition to cancel event suppression.

* Added debug annotation to the onWindowResized event in tabbar_widget.dart.

* Fix parameter type for CGAssociateMouseAndMouseCursorPosition in MainFlutterWindow.swift.

* tabbar_widget.dart: onWindowResized -> onWindowResize

* Removed temporary diagnostic debugPrint from tabbar_widget.dart.

* Updated MainFlutterWindow.swift to obtain the mouse position by creating a dummy CGEvent. The old NSEvent.mouseLocation code is left as a fallback.

* The documentation said to be sure to call CFRelease, but apparently it's a build error to do so. :-P

* Replaced CGEvent calls in MainFlutterWindow.swift with uses of the CGEvent wrapper struct.

* Added argument label to call to CGEvent.init.

* Changed mouseLoc from piecewise assignment to assignment of the whole structure, as it is not yet initialized at that point.

* Linux platform channel: Refactored bump_mouse, setting the stage for a future Wayland implementation.
- Made a new top-level bump_mouse method in bump_mouse.cc/.h.
- Moved the X11-specific implementation to bump_mouse_x11 in bump_mouse_x11.cc/h.
Reworked the bumpMouse operation to have a boolean return value:
- Updated bumpMouse in platform_channel.dart to return a Future<bool> instead of a Future<void>.
- Windows platform channel: Updated BumpMouse in win32_desktop.cpp to return a bool value. Updated the method call handler "bumpMouse" branch in flutter_window.cpp to propagate the BumpMouse return value back to the originating MethodCall.
- MacOS platform channel: Updated the "bumpMouse" branch in the method call handler in MainFlutterWindow.swift to pass true or false into the 'result()' call.
- Linux platform channel: Updated the bump_mouse top-level method and its underlying implementation bump_mouse_x11 to return bool values. Updated the "bumpMouse" branch of host_channel_call_handler in my_application.cc to propagate the result value back up the method channel.
- Updated the kWindowBumpMouse branch of the method handler registered in desktop_home_page.dart to propagate a return value from RdplatformChannel.bumpMouse.

* Reworked the edge scrolling computations in model.dart to use Vector2 from the vector_math package. Updated pubspec.yaml to declare a dependency on vector_math.

* Added an alternative edge scrolling mechanism for when "Bump Mouse" functionality is unavailable:
- Added methods setEdgeScrollTimer and cancelEdgeScrollTimer to model.dart, along with a few state fields.
- Updated edgeScrollMouse to latch the (x, y) coordinate of the last edge scroll event, in case it will be autorepeating.
- Updated edgeScrollMouse to check whether the call to the kWindowBumpMouse method of rustDeskWinManager (and thus the underlying bump_mouse method) succeeded, and to switch to timer-based autorepeat if it fails. Made edgeScrollMouse async to allow awaiting the result of the kWindowBumpMouse method call.
- Updated input_model.dart to call cancelEdgeScrollTimer when a new move event is being processed.
- Updated remote_page.dart to call cancelEdgeScrollTimer when the pointer exits the area represented by the view.

* Fixed scroll percentage math in edgeScrollMouse in model.dart.

* Fixed declared return value for Win32Desktop::BumpMouse in win32_desktop.h.

* Fixed vector_math dependency version in pubspec.yaml to be compatible with the codebase standard Flutter version.

* Added class EdgeScrollFallbackState to model.dart for tracking the state of the edge scroll fallback strategy. Factored out the actual edge scrolling action from CanvasModel.edgeScrollMouse to new method performEdgeScroll so that EdgeScrollFallbackState can call it. Updated edgeScrollMouse to not call performEdgeScroll when it's enabling the fallback strategy.
Updated CanvasModel to use EdgeScrollFallbackState instead of directly tracking the state. Removed method setEdgeScrollTimer.
Added method initializeEdgeScrollFallback to CanvasModel that takes a TickerProvider. Updated _RemotePageState to include the mixin TickerProviderStateMixin. Updated _RemotePageState.initState to call canvasModel.initializeEdgeScrollFallback.
Updated handlePointerDevicePos in input_model.dart to not call cancelEdgeScrollTimer before edgeScrollMouse.
Renamed CanvasModel.cancelEdgeScrollTimer to CanvasModel.cancelEdgeScroll.
Updated the calculations in CanvasModel.edgeScrollMouse to only factor in the safe zone if BumpMouse is working. (Otherwise the problem with resizing can't possibly occur.)

* Updated CanvasModel.edgeScrollMouse in model.dart to handle the situation where only one of the scrollbars is active. Factored extraction of scrollbar data into new function getScrollInfo.

* Updated onWindowResize in tabbar_widget.dart to be resilient to RemotePage instances that don't yet have an ffi reference. Added property hasFFI to remote_page.dart.

* Removed debug output from model.dart.

* PR feedback:
- Added filtering to diagnostic output in the method handler in desktop_home_page.dart to exclude the very chatty kWindowBumpMouse-related output.
- Removed the diagnostic output from bumpMouse in platform_channel.dart for the same reason.
- Updated setScrollPercent to coalesce NaN values for x and y to 0.
- Initialized the GError pointer variable passed into fl_method_call_respond_success in linux/my_application.cc to NULL.
- Added bounds checking of the argument values in the EncodableList branch of the "bumpMouse" method call handler in windows/runner/flutter_window.cpp.

* Added a latch mechanism that keeps edge scrolling disabled until the cursor is observed to be in the inner area bounded by the edge scroll areas:
- Added tristate enumerated type EdgeScrollState to model.dart. In addition to inactive and active states, there is state armed which behaves like inactive but can transition to active when conditions are met.
- Added a field to CanvasModel of type EdgeScrollState. Added methods disableEdgeScroll and rearmEdgeScroll.
- Updated enterView to call canvasModel.rearmEdgeScroll and leaveView to call canvasModel.disableEdgeScroll in remote_page.dart.
- Updated edgeScrollMouse to check the state, disabling edge scrolling when the state is not active and transitioning from armed to active when the mouse is in the interior space.
- Removed the notifyResize/_suppressEdgeScroll mechanism from CanvasModel in model.dart as it is no longer necessary.
- Removed the "safe zone" mechanism from CanvasModel.edgeScrollMouse in model.dart as it is no longer necessary.
- Switched the onWindowResize handler in DesktopTabState in tabbar_widget.dart back to onWindowResized, now that it is no longer delivering canvasModel.notifyResize to all RemotePage tabs.

* Fixed memory leak: Added call to free GError object returned by Flutter API in the event of an error.

* PR feedback:
- Copilot: Use type annotations.
- Copilot: Condition to stop edge scrolling when fallback strategy is in use and the mouse is moved back to the centre.
- Copilot: Check FLValue type before calling fl_value_get_int.
- Copilot: Support list-style method channel dispatch in "bumpMouse" handler for macos as the linux and windows implementations already do.
- Naming convention for constants.
- Left-over variable from previous strategy: _suppressEdgeScroll.
- Unnecessary extra parentheses in edge scroll area conditions.

* Removed property suppressEdgeScroll referencing now-removed field _suppressEdgeScroll in model.dart.
Removed accidental extra blank line in MainFlutterWindow.swift.

* Switched CanvasModel.setScrollPercent to use double.isFinite instead of double.isNaN to test for proper numerical values.

* PR feedback:
- Copilot: Use Vector2.length2 instead of Vector2.length to avoid an unnecessary sqrt in comparison with zero.
- Copilot: Baleet unnecessary semicolons from Swift code.

* PR feedback:
- Copilot: Check argList.count before indexing it

* Oops with the semicolons again.

* Edge scroll, active local cursor

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

* Remove duplicated condition checks

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

* Chore

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

* PR feedback:
- Copilot: Removed unused property hasFFI from remote_page.dart.
- Copilot: Updated updateScrollStyle in model.dart to be resilient to the possibility of bind.sessionGetScrollStyle returning null.

* Factored local cursor updates out of CanvasModel.moveDesktopMouse in model.dart, adding new methods activateLocalCursor and updateLocalCursor.
Updated handlePointerDevicePos in input_model.dart to call canvasModel.updateLocalCursor on every mouse event.
Updated initState in remote_page.dart to schedule a call to canvasModel.activateLocalCursor as a first-image callback.

* Updated the explanation for rounding away from 0 in edgeScrollMouse in model.dart.

---------

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2025-10-30 19:54:11 +08:00
flusheDData
a30582c840 New Spanish transtion terms (#13344)
* Update es.rs

New terms added

* Update es.rs

New terms added
2025-10-30 15:34:56 +08:00
21pages
d106d97b99 mobile verify both webpki and installed CA (#13272)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-30 13:59:00 +08:00
alonginwind
d4410e78e2 feat(ui): show alias instead of peerId in terminal tab label (#13332) 2025-10-29 16:11:20 +08:00
fufesou
e3fcc6cce3 fix: file transfer, auto start on reconnect (#13329)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-29 15:15:05 +08:00
fufesou
265d08fc3b fix: mobile remove "Scale custom" (#13323)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-28 20:25:33 +08:00
21pages
7c8329c5c6 fix mac hwcodec check (#13320)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-28 16:29:42 +08:00
Kendall
d3947c9a19 Fix typo in Spanish translation for downloading message (#13289) 2025-10-27 17:09:42 +08:00
Luca-rickrolled-himself
cd99331668 Add Romanian Locale (#13270)
* Create CODE_OF_CONDUCT-RO.md

* Create CONTRIBUTING-RO.md

* Create SECURITY-RO.md

* Create README-RO.md

* Update README.md
2025-10-27 16:52:36 +08:00
Re*Index. (ot_inc)
472e18b10a Update Japanese (#13268) 2025-10-27 16:52:22 +08:00
Ivan Beà
d443f5de28 Update catalan translation ca.rs (#13267)
Update catalan translation
2025-10-27 16:52:10 +08:00
21pages
3242d132f6 opt ui of Windows session dialog (#13303)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-27 16:51:30 +08:00
Tomppaa
e66d2facd4 Create fi.rs (#13212)
Please add Finnish language rustdesk.
2025-10-26 21:28:56 +08:00
Tomppaa
f15b8dc0da Update lang.rs (#13259)
added finnish language.
2025-10-26 21:28:35 +08:00
Nguyễn Quý Hy
3275824aec Allow flipping sort order in mobile app's file transfer (#13273)
* Allow flipping sort order in mobile app's file transfer

Signed-off-by: Nguyen Quy Hy <nguyenquyhy@live.com.sg>

* Change ascending to be non-nullable

Signed-off-by: Nguyen Quy Hy <nguyenquyhy@live.com.sg>

* Revert file_model change

Signed-off-by: Nguyen Quy Hy <nguyenquyhy@live.com.sg>

---------

Signed-off-by: Nguyen Quy Hy <nguyenquyhy@live.com.sg>
2025-10-25 21:10:26 +08:00
21pages
965cb704ec add try catch on android setCodecInfo in case of unexpected crash (#13280)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-24 21:04:18 +08:00
fufesou
938e165470 fix: save frame, LateInitializationError (#13265)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-24 17:20:56 +08:00
esterTion
9058ef3344 ios: Enable file sharing and document browser support (#13226) 2025-10-23 15:58:50 +08:00
fufesou
ed39cc3038 fix: video service, wait timeout (#13208)
Use multiple frame fetched notifiers.

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-10-22 13:19:08 +08:00
21pages
a77752c4cb fix tab lable translation (#13240)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-21 15:39:52 +08:00
21pages
c9940957f0 fix camera large error log (#13227)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-10-20 13:23:41 +08:00
Andrzej Rudnik
c90d72d720 Update pl.rs (#13210) 2025-10-19 14:19:53 +08:00
XLion
2c30bd9d24 Update tw.rs (#13203) 2025-10-18 16:39:08 +08:00
rustdesk
f2dc8e21a8 build 61 2025-10-18 08:50:07 +08:00
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
solokot
dd7a124334 Update ru.rs (#12227) 2025-07-06 18:03:45 +08:00
bovirus
7447a36782 Italian language update (#12210) 2025-07-06 18:00:05 +08:00
Lynilia
e2830347e6 Update fr.rs (#12203) 2025-07-06 17:59:51 +08:00
rustdesk
9389f3306d fix https://github.com/rustdesk/rustdesk/issues/12233 2025-07-05 09:24:18 +08:00
RustDesk
f3819e19d4 improve sas (#12226)
* improve sas

* Update windows.rs
2025-07-04 16:47:08 +08:00
Alex Rijckaert
9caf0dddc3 Update nl.rs (#12202) 2025-07-04 16:21:32 +08:00
fufesou
f766d28c36 Fix/linux keep terminal sessions (#12222)
* fix: linux, keep terminal sessions

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

* fix: terminal service stucked at reader join

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-03 17:27:50 +08:00
COYG⚡️
7ad3023285 docs: Render correct "CAUTION" (#12204) 2025-07-02 18:59:59 +08:00
COYG⚡️
86e79b0162 docs: correct jump to other language markdown files (#12205) 2025-07-02 18:59:34 +08:00
COYG⚡️
09098e86ca docs: Correct the path to CONTRIBUTING.md links in the README files for each language to ensure that you point to the correct file location. (#12207) 2025-07-02 18:59:18 +08:00
rustdesk
7ce13a21f8 reorder lang/template.rs 2025-07-01 13:23:59 +08:00
Naveenkumar
cf0d090c08 Update ta.rs (#12200)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-07-01 13:18:23 +08:00
fufesou
f26d2a7b84 feat: stylus support (#12196)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-07-01 13:13:41 +08:00
RustDesk
5faf0ad3cf terminal works basically. (#12189)
* terminal works basically.
todo:
- persistent
- sessions restore
- web
- mobile

* missed terminal persistent option change

* android sdk 34 -> 35

* +#![cfg_attr(lt_1_77, feature(c_str_literals))]

* fixing ci

* fix ci

* fix ci for android

* try "Fix Android SDK Platform 35"

* fix android 34

* revert flutter_plugin_android_lifecycle to 2.0.17 which used in rustdesk 1.4.0

* refactor, but break something of desktop terminal (new tab showing loading)

* fix connecting...
2025-07-01 13:12:55 +08:00
Alex Rijckaert
ee5cdc3155 Update nl.rs (#12194) 2025-06-30 14:59:21 +08:00
rustdesk
e0f5fa39f3 terminal of hbb common 2025-06-29 14:09:59 +08:00
Melroy dsilva
d21a1023d2 docs: improve grammar and clarity in READM (#12155) 2025-06-28 15:26:00 +08:00
Andrzej Rudnik
884373794a Update pl.rs (#12162) 2025-06-27 18:49:25 +08:00
fufesou
9060f9ec8a fix: linux tray, defunct process (#12177)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-26 18:27:22 +08:00
fufesou
fd4e0146e1 fix: replace sh with CMD_SH (#12173)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-26 15:54:16 +08:00
fufesou
58fd2d3ccd fix: linux, get_env, break loop (#12174)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-26 15:47:01 +08:00
fufesou
bb6e080c1c fix: linux workaround cmd path (#12172)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-26 09:49:22 +08:00
21pages
7b7c93b78d fix record directory of custom client (#12171)
* For custom client, the incoming record directory of installing Windows app and the Android record directory still use RustDesk,  it works, but replace 'RustDesk' with the custom client's name.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-06-26 09:29:41 +08:00
Mahdi Rahimi
94ae3886c5 Update Arabic translation in ar.rs (#12134) 2025-06-25 12:43:35 +08:00
RustDesk
79c6da98d2 Update common.rs (#12159) 2025-06-24 21:38:59 +08:00
RustDesk
18ea3a4b59 Update common.rs 2025-06-24 21:38:36 +08:00
Mahdi Rahimi
2ae7f00ceb Updated Persian translations in fa.rs (#12133) 2025-06-24 13:23:35 +08:00
fufesou
4d8bfab86e fix: sequentially post conn audit (#12152)
* fix: sequentially post conn audit

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

* Update connection.rs

* refact: simplify loop

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

* Update connection.rs

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-06-23 23:55:07 +08:00
rustdesk
50b1c02243 fix dup msg for relay request 2025-06-23 20:29:24 +08:00
Adam Lewicki
fa61693ccd Update pl.rs (#12118) 2025-06-22 13:48:31 +08:00
bovirus
7822d3d923 Italian language update (#12095) 2025-06-21 16:29:23 +08:00
solokot
98d99fae64 Update ru.rs (#12096) 2025-06-21 16:29:11 +08:00
fufesou
7330dc70f3 fix: android 7.1, input, crash (#12129)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-20 17:50:28 +08:00
fufesou
46cd090f98 Revert "try fix firefox v2 paste problem" (#12126)
This reverts commit 590ecc43ff.
2025-06-19 22:31:40 +08:00
fufesou
d6ba063655 fix: win, privacy mode 2 (#12123)
* fix: win, privacy mode 2

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

* Update src/privacy_mode/win_virtual_display.rs

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-19 18:39:15 +08:00
rustdesk
590ecc43ff try fix firefox v2 paste problem 2025-06-19 13:21:47 +09:00
rustdesk
1eee03818d fix https://github.com/rustdesk/rustdesk/discussions/11838 2025-06-19 12:28:38 +09:00
fufesou
5dd15d1282 fix: privacy mode, msgbox sometimes does not show (#12117)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-18 16:25:15 +08:00
largemouth
8754579181 chore: fix some typos in comment (#12102)
Signed-off-by: largemouth <largemouth@aliyun.com>
2025-06-17 21:02:35 +08:00
rustdesk
57b826c56b try removing bom 2025-06-16 23:14:49 +09:00
rustdesk
bfd6ca79f8 debugg invalid id issue https://discord.com/channels/804630702657110016/804630702657110018/1384157888603357337 2025-06-16 22:44:22 +09:00
rustdesk
d84b26a9cd try to fix 1.3.8 not work on win7 sp1, https://github.com/rustdesk/rustdesk/discussions/12097 2025-06-16 18:36:15 +09:00
Mr-Update
181b3afc2d Update de.rs (#12092) 2025-06-16 13:30:14 +08:00
XLion
31934e9bd8 Update tw.rs (#12091) 2025-06-16 13:29:59 +08:00
rustdesk
14a8f00e5b fix punch option for non-public 2025-06-15 14:58:12 +08:00
rustdesk
44e00f8ec2 remove xpsprint.dll hard dep, https://github.com/rustdesk/rustdesk/discussions/12042#discussioncomment-13464313 2025-06-14 21:58:51 +08:00
rustdesk
645a76d43f udp / ipv6 punch option 2025-06-14 21:42:18 +08:00
Ibnul Mutaki
bf77f582d0 trans(ID): fix some phrase and add more translation (#12050)
* trans: fix some phrase and add more translation

* trans: change : upgrade tip

* fixing typo Downliad -> Download
2025-06-14 21:17:45 +08:00
rustdesk
c58fd145f2 CLAUDE.md 2025-06-14 18:04:12 +08:00
fufesou
a5a3352655 fix: linux, nokhwa, camera index (#12045)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-14 13:19:59 +08:00
21pages
2533493c66 Remove non-existent tags when importing ab peers from another ab (#12062)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-06-13 14:34:27 +08:00
rustdesk
832458c59e 1024 -> 1500 2025-06-13 00:42:52 +08:00
rustdesk
5beebf967d fix kcp_stream 2025-06-13 00:30:21 +08:00
rustdesk
4e9bdcbc1f fix ci 2025-06-13 00:12:07 +08:00
rustdesk
070b0354fd bring back allow-https-21114 https://github.com/rustdesk/rustdesk-server-pro/discussions/570#discussioncomment-13449526 2025-06-12 23:11:07 +08:00
rustdesk
f9405711c6 fix ci 2025-06-12 21:35:32 +08:00
rustdesk
7792ac1481 udp punch and ipv6 punch 2025-06-12 21:32:28 +08:00
lichon
05a812247a fix: use default camera, first element in query result might not be a camera (#12010) 2025-06-12 13:35:42 +08:00
WC3D
645cfd3b3d Bump ring from 0.17.8 to 0.17.13 in the cargo group across 1 directory (#12028)
Bumps the cargo group with 1 update in the / directory: [ring](https://github.com/briansmith/ring).


Updates `ring` from 0.17.8 to 0.17.13
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:28:51 +08:00
rustdesk
294ffcd9d3 hide-powered-by-me 2025-06-10 22:01:30 +08:00
Syed Ghufran Hassan
738afb54d7 Update main.rs (#12027)
common::global_init() might fail silently. Since return is used without any error logging, the user won’t know why the application didn't start so that is why added error print statement in case if it fails
2025-06-10 10:56:10 +08:00
mehdi-song
83f45b2212 Update fa.rs (#11971)
;-)
2025-06-07 19:21:42 +08:00
fufesou
8b2643e060 refact: remove unnecessary printing (#12000)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-06-06 14:52:01 +08:00
Kleofass
e79724644d Update lv.rs (#11966) 2025-06-05 13:06:21 +08:00
Lynilia
861fc91578 Update fr.rs (#11940) 2025-06-04 13:11:14 +08:00
Mahdi Rahimi
fa7770d901 Update Arabic translation in ar.rs (#11934) 2025-06-03 13:31:18 +08:00
21pages
e0f35b9046 test nat type for outgoing-only client (#11962)
* test nat type for outgoing-only client

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

* test nat type for ios

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-06-03 09:27:02 +08:00
XLion
32e96e3705 Update tw.rs (#11931) 2025-06-02 22:16:24 +08:00
rustdesk
d52d9da043 upgrade android plugin to 7.3.1 2025-06-01 20:16:42 +08:00
rustdesk
68eaedfddc enable force-always-relay option for address book and accessible devices 2025-06-01 19:27:00 +08:00
rustdesk
e08cf3c0eb update kotlin 2025-06-01 17:58:03 +08:00
rustdesk
f919f297ac fix https://github.com/rustdesk/rustdesk/issues/11943 2025-06-01 16:44:55 +08:00
Mahdi Rahimi
c39c49fd17 Updated Persian translations in fa.rs (#11933) 2025-05-31 13:08:30 +08:00
rustdesk
90cb0ee56d fix https://github.com/rustdesk/rustdesk/issues/11927 2025-05-30 23:32:17 +08:00
Robert Galoyan
edab44afdf Update Russian docs to keep them in par with the original readme (#11901)
* Keep `README-RU` up to date with original readme

* Update `CONTRIBUTING-RU.md`

Minor reformat and grammar/orphography fixes
2025-05-30 13:39:38 +08:00
21pages
ec0456e606 clear the accessible devices tab when retrieving accessible devices disabled (#11913)
* clear the accessible devices tab when retrieving accessible devices is disabled

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

* Update group_model.dart

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-05-29 17:07:32 +08:00
Jun, Koo
4e1a814aeb Update: Korean translation for various strings (#11892)
Updates the Korean translation file (`ko.rs`) to include new and corrected translations for various UI elements and messages. This improves the accuracy and completeness of the Korean localization.
2025-05-29 14:44:09 +08:00
rustdesk
4f8f34ec01 improve err 2025-05-28 23:27:12 +08:00
rustdesk
836950354b force secure tcp 2025-05-28 23:02:46 +08:00
Jun, Koo
527be17eaf Docs: Improve Korean translation for clarity and consistency (#11889)
* Docs: Improve Korean translation for clarity and consistency

Corrected minor grammatical errors and improved phrasing in `CONTRIBUTING-KR.md` and `README-KR.md` for better readability and consistency.

* Docs(KR): Update Korean README/CONTRIBUTING to align with latest English versions and refine translations
2025-05-28 21:42:14 +08:00
rustdesk
a0f4984ba5 update reqwest 2025-05-27 22:37:12 +08:00
Alex Rijckaert
4121e3fd14 Update nl.rs (#11873) 2025-05-27 13:08:03 +08:00
bovirus
a7a2f77ea3 Italian language update (#11862) 2025-05-26 21:05:13 +08:00
Mr-Update
46622f7576 Update de.rs (#11856) 2025-05-25 15:03:53 +08:00
solokot
45c9c505db Update ru.rs (#11855) 2025-05-24 14:27:22 +08:00
rustdesk
777c25bba2 no api for unregistered device 2025-05-24 09:21:06 +08:00
rustdesk
01146574f2 prepare no-register-device 2025-05-23 22:15:31 +08:00
rustdesk
39151531d7 fix ci 2025-05-23 17:22:13 +08:00
flusheDData
a5fefaddf5 New terms added (#11823)
* Update es.rs

* Update es.rs

* Update es.rs

New terms added

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2025-05-23 17:13:15 +08:00
Y-Ploni
f68d333bf1 Update he.rs (#11795)
* Update he.rs

* Update he.rs
2025-05-23 17:11:40 +08:00
fufesou
3c028fe5b5 feat: numeric one-time password (#11846)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-23 17:10:47 +08:00
fufesou
6ff679c6b4 fix: win, upload sysinfo (#11849)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-23 16:46:50 +08:00
rustdesk
48da2709d7 add youtube to readme 2025-05-23 11:14:35 +08:00
luzpaz
042d031a04 fix: source typo in src/clipboard.rs (#11726)
Found via codespell
2025-05-22 16:17:59 +08:00
VenusGirl❤
b2d5eb9714 Update SECURITY-KR.md (#11725) 2025-05-22 16:17:31 +08:00
fufesou
511a0b3693 refact: macos, comments, resolution list (#11830)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-21 18:23:14 +08:00
fufesou
06ab987e32 fix: macos, hidpi, resolutions (#11825)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-21 16:53:02 +08:00
rustdesk
b4a30cac73 Try to fix https://github.com/rustdesk/rustdesk/discussions/5602#discussioncomment-12482865 2025-05-21 15:00:12 +08:00
rustdesk
f801c251ed enable web socket for all except web 2025-05-20 20:49:21 +08:00
Lars
d3d7b09fe7 fix: mobile never connecting with password from url scheme (#11797) 2025-05-20 16:35:36 +08:00
fufesou
6144a1c97e fix: osx, reset modifiers' state after locking screen (#11806)
https://github.com/rustdesk/rustdesk/issues/11802

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-19 21:02:07 +08:00
fufesou
118552ad0e refact: osx, handle key events, sleep (#11798)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-19 10:01:42 +08:00
rustdesk
9217205229 all key/mouse in QUEUE since --server has GUI too (--tray) 2025-05-17 14:40:44 +08:00
fufesou
4f6ae08110 fix: macos, key input lags, when service running (#11786)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-17 11:03:02 +08:00
solokot
90ad55d4aa Update ru.rs (#11769) 2025-05-16 15:27:40 +08:00
YGF
f1a4494e3c fix: parameter error (#11777) 2025-05-16 09:52:40 +08:00
fufesou
5fa17e440a fix: nokhwa, windows, x86 target runs on x64 (#11774)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-15 11:04:21 +08:00
fufesou
a73fa3cbf6 refact: oidc, launch url (#11772)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-15 09:06:37 +08:00
rustdesk
ae7faea6d5 --address_book_alias 2025-05-14 19:07:39 +08:00
fufesou
b525185d7f feat: web oidc (#11755)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-14 17:35:50 +08:00
Y-Ploni
dad841e493 Update he.rs (#11761) 2025-05-14 17:34:48 +08:00
21pages
550dd5ad72 update hbb_common, fix sync socks from advanced options to config file (#11757)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2025-05-14 15:32:04 +08:00
rustdesk
b4eeaee737 vn -> vi, fix https://github.com/rustdesk/rustdesk/issues/11756 2025-05-14 12:51:53 +08:00
RustDesk
cee69bb8b4 Update winget.yml 2025-05-14 01:33:08 +08:00
RustDesk
43501b663e Update winget.yml 2025-05-14 01:21:04 +08:00
RustDesk
9c0711e1db Update winget.yml 2025-05-14 01:18:54 +08:00
rustdesk
d00b8bb580 stupid me 2025-05-13 22:40:49 +08:00
rustdesk
c735fbd54c improve self-host server switch case https://github.com/rustdesk/rustdesk/issues/11749 2025-05-13 19:56:48 +08:00
fufesou
9d0d729522 Refact/update printer (#11748)
* refact: update printer

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

* fix: uninstall the printer for normal users

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-13 19:33:00 +08:00
fufesou
4c354ee1ae refact: install printer (#11745)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-13 14:14:54 +08:00
fufesou
f56c5c1bbb fix: win, prompt uac, update_install_option (#11741)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-13 10:20:57 +08:00
fufesou
a615b5e119 fix: nokhwa, dll search path (#11738)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-12 23:17:59 +08:00
Lynilia
12a9745b88 Update fr.rs (#11719) 2025-05-12 21:46:29 +08:00
John Fowler
b05a77ece2 Update hu.rs (#11718)
* Update hu.rs

Translation of the new character strings into Hungarian.

* Update hu.rs

Translation of the new character strings into Hungarian.
I replaced bad characters with good characters.
2025-05-12 21:46:18 +08:00
fufesou
ea106354af fix: win, only start tray if is installed exe (#11737)
Signed-off-by: fufesou <linlong1266@gmail.com>
2025-05-12 21:46:05 +08:00
351 changed files with 27132 additions and 9353 deletions

View File

@@ -0,0 +1,56 @@
You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to analyze and improve the instructions for Claude Code.
Follow these steps carefully:
1. Analysis Phase:
Review the chat history in your context window.
Then, examine the current Claude instructions, commands and config
<claude_instructions>
/CLAUDE.md
/.claude/commands/*
**/CLAUDE.md
.claude/settings.json
.claude/settings.local.json
</claude_instructions>
Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for:
- Inconsistencies in Claude's responses
- Misunderstandings of user requests
- Areas where Claude could provide more detailed or accurate information
- Opportunities to enhance Claude's ability to handle specific types of queries or tasks
- New commands or improvements to a commands name, function or response
- Permissions and MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them for the command to work
2. Interaction Phase:
Present your findings and improvement ideas to the human. For each suggestion:
a) Explain the current issue you've identified
b) Propose a specific change or addition to the instructions
c) Describe how this change would improve Claude's performance
Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the implementation phase. If not, refine your suggestion or move on to the next idea.
3. Implementation Phase:
For each approved change:
a) Clearly state the section of the instructions you're modifying
b) Present the new or modified text for that section
c) Explain how this change addresses the issue identified in the analysis phase
4. Output Format:
Present your final output in the following structure:
<analysis>
[List the issues identified and potential improvements]
</analysis>
<improvements>
[For each approved improvement:
1. Section being modified
2. New or modified instruction text
3. Explanation of how this addresses the identified issue]
</improvements>
<final_instructions>
[Present the complete, updated set of instructions for Claude, incorporating all approved changes]
</final_instructions>
Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your implementations.

View File

@@ -40,9 +40,9 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libclang-11-dev \ libclang-dev \
libgtk-3-dev \ libgtk-3-dev \
llvm-11-dev \ llvm-dev \
nasm \ nasm \
ninja-build \ ninja-build \
pkg-config \ pkg-config \

View File

@@ -5,7 +5,7 @@ env:
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
on: on:
workflow_dispatch: 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 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" CARGO_NDK_VERSION: "3.1.2"
SCITER_ARMV7_CMAKE_VERSION: "3.29.7" 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" LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.24.5" FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.24.5" ANDROID_FLUTTER_VERSION: "3.24.5"
@@ -31,14 +31,15 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}" TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" 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: # If we change the `VCPKG COMMIT_ID`, please remember:
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
# Or we may face build issue like # Or we may face build issue like
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.0" ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.4"
NDK_VERSION: "r27c" NDK_VERSION: "r27c"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -177,24 +178,24 @@ jobs:
# Download printer driver files and extract them to ./rustdesk # Download printer driver files and extract them to ./rustdesk
try { 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/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums
# Check and move the files # 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 $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.zip -Algorithm SHA256 $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256
$checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value $checksum_adapter = (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 $downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) { if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) {
Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file." Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file."
Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath . Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath .
mkdir ./rustdesk/drivers 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 . Expand-Archive printer_driver_adapter.zip -DestinationPath .
mv -Force .\printer_driver_adapter.dll ./rustdesk mv -Force .\printer_driver_adapter.dll ./rustdesk
} elseif ($checksum_driver -ne $downloadsum_driver.Hash) { } 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 { } else {
Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file."
} }
@@ -391,6 +392,13 @@ jobs:
ls -l ./libs/portable/Runner.res; ls -l ./libs/portable/Runner.res;
fi 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 - name: Sign rustdesk files
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
shell: bash shell: bash
@@ -424,80 +432,6 @@ jobs:
files: | files: |
./SignOutput/rustdesk-*.exe ./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: build-rustdesk-ios:
if: ${{ inputs.upload-artifact }} if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa name: build rustdesk ios ipa
@@ -617,63 +551,6 @@ jobs:
# files: | # files: |
# flutter/build/ios/ipa/*.ipa # 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: build-for-macOS:
name: ${{ matrix.job.target }} name: ${{ matrix.job.target }}
@@ -692,7 +569,7 @@ jobs:
} }
- { - {
target: aarch64-apple-darwin, 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: "--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", extra-build-args: "--screencapturekit",
arch: aarch64, arch: aarch64,
@@ -746,7 +623,7 @@ jobs:
- name: Install build runtime - name: Install build runtime
run: | 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 # 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 if command -v pkg-config &>/dev/null; then
echo "pkg-config is already installed" echo "pkg-config is already installed"
@@ -886,6 +763,7 @@ jobs:
needs: needs:
- build-for-macOS - build-for-macOS
- build-for-windows-flutter - build-for-windows-flutter
- build-for-windows-sciter
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ inputs.upload-artifact }} if: ${{ inputs.upload-artifact }}
steps: steps:
@@ -907,9 +785,15 @@ jobs:
name: rustdesk-unsigned-windows-x86_64 name: rustdesk-unsigned-windows-x86_64
path: ./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 - name: Combine unsigned app
run: | 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 - name: Publish unsigned app
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
@@ -929,21 +813,21 @@ jobs:
- { - {
arch: aarch64, arch: aarch64,
target: aarch64-linux-android, target: aarch64-linux-android,
os: ubuntu-22.04, os: ubuntu-24.04,
reltype: release, reltype: release,
suffix: "", suffix: "",
} }
- { - {
arch: armv7, arch: armv7,
target: armv7-linux-androideabi, target: armv7-linux-androideabi,
os: ubuntu-22.04, os: ubuntu-24.04,
reltype: release, reltype: release,
suffix: "", suffix: "",
} }
- { - {
arch: x86_64, arch: x86_64,
target: x86_64-linux-android, target: x86_64-linux-android,
os: ubuntu-22.04, os: ubuntu-24.04,
reltype: release, reltype: release,
suffix: "", suffix: "",
} }
@@ -980,7 +864,7 @@ jobs:
libayatana-appindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-11-dev \ libclang-dev \
libunwind-dev \ libunwind-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
@@ -993,7 +877,7 @@ jobs:
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
libxdo-dev \ libxdo-dev \
libxfixes-dev \ libxfixes-dev \
llvm-11-dev \ llvm-dev \
nasm \ nasm \
ninja-build \ ninja-build \
openjdk-17-jdk-headless \ openjdk-17-jdk-headless \
@@ -1117,6 +1001,8 @@ jobs:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: | run: |
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# Increase Gradle JVM memory for CI builds
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
# temporary use debug sign config # temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in case ${{ matrix.job.target }} in
@@ -1212,7 +1098,7 @@ jobs:
needs: [build-rustdesk-android] needs: [build-rustdesk-android]
name: build rustdesk android universal apk name: build rustdesk android universal apk
if: ${{ inputs.upload-artifact }} if: ${{ inputs.upload-artifact }}
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
env: env:
reltype: release reltype: release
x86_target: "" # can be ",android-x86" x86_target: "" # can be ",android-x86"
@@ -1250,7 +1136,7 @@ jobs:
libayatana-appindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-11-dev \ libclang-dev \
libunwind-dev \ libunwind-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
@@ -1263,7 +1149,7 @@ jobs:
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
libxdo-dev \ libxdo-dev \
libxfixes-dev \ libxfixes-dev \
llvm-11-dev \ llvm-dev \
nasm \ nasm \
ninja-build \ ninja-build \
openjdk-17-jdk-headless \ openjdk-17-jdk-headless \
@@ -1324,6 +1210,8 @@ jobs:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: | run: |
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# Increase Gradle JVM memory for CI builds
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
# temporary use debug sign config # temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
@@ -1559,7 +1447,8 @@ jobs:
rpm \ rpm \
unzip \ unzip \
wget \ wget \
xz-utils xz-utils \
libssl-dev
# we have libopus compiled by us. # we have libopus compiled by us.
apt-get remove -y libopus-dev || true apt-get remove -y libopus-dev || true
# output devs # output devs
@@ -1768,6 +1657,14 @@ jobs:
with: with:
submodules: recursive 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 - name: Free Space
run: | run: |
df -h df -h
@@ -1831,12 +1728,13 @@ jobs:
unzip \ unzip \
wget \ wget \
xz-utils \ xz-utils \
zip zip \
libssl-dev
# arm-linux needs CMake and vcokg built from source as there # arm-linux needs CMake and vcokg built from source as there
# are no prebuilts available from Kitware and Microsoft # are no prebuilts available from Kitware and Microsoft
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
# install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake # install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
apt-get install -y gcc-8 g++-8 libssl-dev apt-get install -y gcc-8 g++-8
# bootstrap CMake amd add it to PATH # bootstrap CMake amd add it to PATH
git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
pushd /tmp/cmake pushd /tmp/cmake
@@ -1853,11 +1751,12 @@ jobs:
rm -rf vcpkg rm -rf vcpkg
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
pushd 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 # 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 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 CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics
else else
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
sh bootstrap-vcpkg.sh -disableMetrics sh bootstrap-vcpkg.sh -disableMetrics
fi fi
popd popd
@@ -1978,11 +1877,8 @@ jobs:
# https://github.com/AppImage/AppImageKit/wiki/FUSE # https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo apt-get install -y libarchive-tools libfuse2 sudo apt-get install -y libarchive-tools libfuse2
# set-up appimage-builder # set-up appimage-builder
pushd /tmp # https://github.com/AppImage/AppImageKit/issues/1395
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 sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
popd
# run appimage-builder # run appimage-builder
pushd appimage pushd appimage
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
@@ -2009,14 +1905,15 @@ jobs:
job: job:
- { - {
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04, # https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981
distro: ubuntu22.04,
on: ubuntu-22.04, on: ubuntu-22.04,
arch: x86_64, arch: x86_64,
suffix: "", suffix: "",
} }
- { - {
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04, distro: ubuntu22.04,
on: ubuntu-22.04, on: ubuntu-22.04,
arch: x86_64, arch: x86_64,
suffix: "-sciter", suffix: "-sciter",
@@ -2084,6 +1981,8 @@ jobs:
if: False if: False
name: build-rustdesk-web name: build-rustdesk-web
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions:
contents: read
strategy: strategy:
fail-fast: false fail-fast: false
env: env:

View File

@@ -16,8 +16,8 @@ env:
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly" TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.0" VERSION: "1.4.4"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -126,7 +126,7 @@ jobs:
- name: Install build runtime - name: Install build runtime
run: | run: |
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config brew install llvm create-dmg nasm pkg-config
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
@@ -266,7 +266,7 @@ jobs:
libayatana-appindicator3-dev\ libayatana-appindicator3-dev\
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-11-dev \ libclang-dev \
libunwind-dev \ libunwind-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
@@ -280,7 +280,7 @@ jobs:
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
libxdo-dev \ libxdo-dev \
libxfixes-dev \ libxfixes-dev \
llvm-11-dev \ llvm-dev \
nasm \ nasm \
yasm \ yasm \
ninja-build \ ninja-build \

View File

@@ -2,6 +2,7 @@ name: Publish to WinGet
on: on:
release: release:
types: [released] types: [released]
workflow_dispatch:
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -9,5 +10,6 @@ jobs:
- uses: vedantmgoyal9/winget-releaser@main - uses: vedantmgoyal9/winget-releaser@main
with: with:
identifier: RustDesk.RustDesk identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }} version: "1.4.4"
release-tag: "1.4.4"
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}

91
CLAUDE.md Normal file
View File

@@ -0,0 +1,91 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Build Commands
- `cargo run` - Build and run the desktop application (requires libsciter library)
- `python3 build.py --flutter` - Build Flutter version (desktop)
- `python3 build.py --flutter --release` - Build Flutter version in release mode
- `python3 build.py --hwcodec` - Build with hardware codec support
- `python3 build.py --vram` - Build with VRAM feature (Windows only)
- `cargo build --release` - Build Rust binary in release mode
- `cargo build --features hwcodec` - Build with specific features
### Flutter Mobile Commands
- `cd flutter && flutter build android` - Build Android APK
- `cd flutter && flutter build ios` - Build iOS app
- `cd flutter && flutter run` - Run Flutter app in development mode
- `cd flutter && flutter test` - Run Flutter tests
### Testing
- `cargo test` - Run Rust tests
- `cd flutter && flutter test` - Run Flutter tests
### Platform-Specific Build Scripts
- `flutter/build_android.sh` - Android build script
- `flutter/build_ios.sh` - iOS build script
- `flutter/build_fdroid.sh` - F-Droid build script
## Project Architecture
### Directory Structure
- **`src/`** - Main Rust application code
- `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
- `src/server/` - Audio/clipboard/input/video services and network connections
- `src/client.rs` - Peer connection handling
- `src/platform/` - Platform-specific code
- **`flutter/`** - Flutter UI code for desktop and mobile
- **`libs/`** - Core libraries
- `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
- `libs/scrap/` - Screen capture functionality
- `libs/enigo/` - Platform-specific keyboard/mouse control
- `libs/clipboard/` - Cross-platform clipboard implementation
### Key Components
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
### UI Architecture
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
- **Modern UI**: Flutter-based - files in `flutter/`
- Desktop: `flutter/lib/desktop/`
- Mobile: `flutter/lib/mobile/`
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
## Important Build Notes
### Dependencies
- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom`
- Set `VCPKG_ROOT` environment variable
- Download appropriate Sciter library for legacy UI support
### Ignore Patterns
When working with files, ignore these directories:
- `target/` - Rust build artifacts
- `flutter/build/` - Flutter build output
- `flutter/.dart_tool/` - Flutter tooling files
### Cross-Platform Considerations
- Windows builds require additional DLLs and virtual display drivers
- macOS builds need proper signing and notarization for distribution
- Linux builds support multiple package formats (deb, rpm, AppImage)
- Mobile builds require platform-specific toolchains (Android SDK, Xcode)
### Feature Flags
- `hwcodec` - Hardware video encoding/decoding
- `vram` - VRAM optimization (Windows only)
- `flutter` - Enable Flutter UI
- `unix-file-copy-paste` - Unix file clipboard support
- `screencapturekit` - macOS ScreenCaptureKit (macOS only)
### Config
All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types:
- Settings
- Local
- Display
- Built-in

2084
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.4.0" version = "1.4.4"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -81,6 +81,9 @@ fon = "0.6"
zip = "0.6" zip = "0.6"
shutdown_hooks = "0.1" shutdown_hooks = "0.1"
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
stunclient = "0.4"
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"}
reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux # https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
@@ -97,6 +100,7 @@ ctrlc = "3.2"
# arboard = { version = "3.4", features = ["wayland-data-control"] } # arboard = { version = "3.4", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", 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" } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" }
system_shutdown = "4.0" system_shutdown = "4.0"
qrcode-generator = "4.1" qrcode-generator = "4.1"
@@ -142,6 +146,10 @@ core-graphics = "0.22"
include_dir = "0.7" include_dir = "0.7"
fruitbasket = "0.10" fruitbasket = "0.10"
objc_id = "0.1" 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] [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" } tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
@@ -153,13 +161,11 @@ keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" } wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
tiny-skia = "0.11"
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] softbuffer = "0.4"
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support fontdb = "0.23"
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false } bytemuck = "1.23"
ttf-parser = "0.25"
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" } psimple = { package = "libpulse-simple-binding", version = "2.27" }
@@ -178,6 +184,11 @@ once_cell = {version = "1.18", optional = true}
nix = { version = "0.29", features = ["term", "process"]} nix = { version = "0.29", features = ["term", "process"]}
gtk = "0.18" gtk = "0.18"
termios = "0.3" termios = "0.3"
terminfo = "0.8"
winit = "0.30"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"

View File

@@ -4,7 +4,7 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>]<br> [<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b> <b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
</p> </p>
@@ -13,11 +13,11 @@
> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application. > The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application.
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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 software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). 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).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -46,7 +46,7 @@ Please download Sciter dynamic library yourself.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [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) [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Raw steps to build ## Raw Steps to build
- Prepare your Rust development env and C++ build env - Prepare your Rust development env and C++ build env
@@ -59,7 +59,7 @@ Please download Sciter dynamic library yourself.
## [Build](https://rustdesk.com/docs/en/dev/build/) ## [Build](https://rustdesk.com/docs/en/dev/build/)
## How to build on Linux ## How to Build on Linux
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
@@ -154,7 +154,7 @@ Or, if you're running a release executable:
target/release/rustdesk target/release/rustdesk
``` ```
Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. Please ensure that you run these commands from the root of the RustDesk repository, or the application may not find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
## File Structure ## File Structure

View File

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

View File

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

View File

@@ -68,11 +68,8 @@ fn install_android_deps() {
} }
path.push(target); path.push(target);
println!( println!(
"{}", "cargo:rustc-link-search={}",
format!( 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=ndk_compat");
println!("cargo:rustc-link-lib=oboe"); 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

@@ -0,0 +1,85 @@
# Codul de Conduită al Contributorilor
## Angajamentul Nostru
Noi, ca membri, contribuitori și lideri, ne angajăm să facem ca participarea în comunitatea noastră să fie o experiență fără hărțuire pentru toată lumea, indiferent de vârstă, dimensiunea corpului, dizabilități vizibile sau invizibile, etnie, caracteristici sexuale, identitate și exprimare de gen, nivel de experiență, educație, statut socio-economic, naționalitate, aspect personal, rasă, religie sau identitate și orientare sexuală.
Ne angajăm să acționăm și să interacționăm în moduri care contribuie la o comunitate deschisă, primitoare, diversă, incluzivă și sănătoasă.
## Standardele Noastre
Exemple de comportamente care contribuie la un mediu pozitiv pentru comunitatea noastră includ:
* Demonstrarea empatiei și a bunătății față de ceilalți
* Respectarea opiniilor, punctelor de vedere și experiențelor diferite
* Oferirea și acceptarea cu grație a feedback-ului constructiv
* Asumarea responsabilității și cererea de scuze celor afectați de greșelile noastre și învățarea din experiență
* Concentrarea pe ceea ce este cel mai bun nu doar pentru noi ca indivizi, ci pentru întreaga comunitate
Exemple de comportamente inacceptabile includ:
* Utilizarea limbajului sau imaginilor sexualizate, precum și atenția sau avansurile sexuale de orice fel
* Trollare, insulte sau comentarii denigratoare și atacuri personale sau politice
* Hărțuire publică sau privată
* Publicarea informațiilor private ale altora, cum ar fi adresa fizică sau de e-mail, fără permisiunea explicită
* Alte comportamente care ar putea fi considerate inadecvate într-un cadru profesional
## Responsabilități de Aplicare
Liderii comunității sunt responsabili pentru clarificarea și aplicarea standardelor noastre de comportament acceptabil și vor lua măsuri corective adecvate și echitabile ca răspuns la orice comportament pe care îl consideră inadecvat, amenințător, ofensator sau dăunător.
Liderii comunității au dreptul și responsabilitatea de a elimina, edita sau respinge comentarii, commit-uri, cod, editări wiki, probleme și alte contribuții care nu se aliniază acestui Cod de Conduită și vor comunica motivele pentru deciziile de moderare atunci când este cazul.
## Domeniu de Aplicare
Acest Cod de Conduită se aplică în toate spațiile comunității și se aplică și atunci când un individ reprezintă oficial comunitatea în spații publice.
Exemple de reprezentare a comunității includ utilizarea unei adrese de e-mail oficiale, postarea printr-un cont oficial de social media sau acționarea ca reprezentant desemnat la un eveniment online sau offline.
## Aplicare
Cazurile de comportament abuziv, hărțuitor sau altfel inacceptabil pot fi raportate liderilor comunității responsabili pentru aplicare la [info@rustdesk.com](mailto:info@rustdesk.com).
Toate plângerile vor fi revizuite și investigate prompt și corect.
Toți liderii comunității sunt obligați să respecte confidențialitatea și securitatea persoanei care raportează orice incident.
## Ghiduri de Aplicare
Liderii comunității vor urma aceste Ghiduri privind Impactul Comunității pentru a stabili consecințele pentru orice acțiune pe care o consideră o încălcare a acestui Cod de Conduită:
### 1. Corectare
**Impact asupra comunității**: Utilizarea limbajului neadecvat sau alte comportamente considerate neprofesionale sau nedorite în comunitate.
**Consecință**: O avertizare scrisă și privată din partea liderilor comunității, oferind claritate asupra naturii încălcării și o explicație despre motivul pentru care comportamentul a fost inadecvat. Poate fi cerută o scuză publică.
### 2. Avertisment
**Impact asupra comunității**: Încălcare printr-un incident singular sau o serie de acțiuni.
**Consecință**: Un avertisment cu consecințe pentru continuarea comportamentului. Nicio interacțiune cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, pentru o perioadă specificată. Aceasta include evitarea interacțiunilor în spațiile comunității, precum și pe canale externe, cum ar fi rețelele sociale. Încălcarea acestor termeni poate duce la o suspendare temporară sau permanentă.
### 3. Suspendare Temporară
**Impact asupra comunității**: O încălcare serioasă a standardelor comunității, inclusiv comportament neadecvat susținut.
**Consecință**: Suspendare temporară de la orice tip de interacțiune sau comunicare publică cu comunitatea pentru o perioadă specificată. Nicio interacțiune publică sau privată cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, nu este permisă în această perioadă. Încălcarea acestor termeni poate duce la o interdicție permanentă.
### 4. Interdicție Permanentă
**Impact asupra comunității**: Demonstrând un tipar de încălcare a standardelor comunității, inclusiv comportament neadecvat susținut, hărțuire a unei persoane sau agresiune față de sau denigrare a unor grupuri de persoane.
**Consecință**: Interdicție permanentă de la orice tip de interacțiune publică în cadrul comunității.
## Atribuire
Acest Cod de Conduită este adaptat din [Contributor Covenant][homepage], versiunea 2.0, disponibil la [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Ghidurile privind Impactul Comunității au fost inspirate de [scara de aplicare a codului de conduită Mozilla][Mozilla CoC].
Pentru răspunsuri la întrebări frecvente despre acest cod de conduită, vezi FAQ la [https://www.contributor-covenant.org/faq][FAQ]. Traduceri sunt disponibile la [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,35 +1,41 @@
# RustDesk 기여 가이드라인 # RustDesk 기여하기
RustDesk는 모든 사람여를 환영합니다. 만약 RustDesk에 기여하고 싶다면 아래 가이드를 참고해주세요: RustDesk는 모든 분들여를 환영합니다. 저희를 도와주실 생각이 있으시다면
다음 지침을 따르세요:
## 기여 방식 ## 기여
RustDesk 또는 종속성에 대한 기여는 GitHub Pull Request 형태로 이루어져야 합니다. RustDesk 또는 종속성에 대한 기여는 GitHub 풀 리퀘스트 형태로
모든 Pull Request는 코어 기여자가 검토하며, 메인 저장소에 반영되거나 필요한 수정 사항에 대한 피드백을 받습니다. 이루어져야 합니다. 각 풀 리퀘스트는 핵심 기여자 (패치 적용 권한이
모든 기여는 이 형식을 따라야 합니다. 있는 사람)가 검토하여 메인 트리에 추가하거나 필요한 변경 사항에
대한 피드백을 제공합니다. 핵심 기여자의 기여를 포함하여 모든 기여는
이 형식을 따라야 합니다.
특정 이슈에 작업하고 싶다면, 먼저 GitHub 이슈에 댓글을 달아 작업하겠다고 알려주세요. 이슈에 대해 작업하고 싶으시면 먼저 해당 이슈에 대해 작업하고 싶다는
이는 동일한 작업에 대해 중복 기여가 발생하는 것을 방지하기 위함입니다. 댓글을 달아 해당 이슈를 요청하세요. 이는 동일한 이슈에 대한 기여자의
중복된 노력을 방지하기 위한 것입니다.
## Pull Request Checklist ## 풀 리퀘스트 체크리스트
- master 브랜치에서 브랜치를 생성하고 작업하세요.<br/> - Master 브랜치에서 브랜치를 만들고, 필요한 경우 풀 리퀘스트를 제출하기
필요한 경우 PR 제출 전에 최신 master 브랜치 리베이스(rebase)하세요.<br/> 전에 현재 마스터 브랜치 리베이스하세요. 마스터 브랜치와 깔끔하게
충돌이 발생하면 기여자가 직접 해결해야 합니다. 병합되지 않으면 변경 사항을 리베이스하라는 요청을 받을 수 있습니다.
- 커밋은 가능한 한 작고 독립적인 단위로 작성하세요.<br/> - 커밋은 가능한 한 작아야 하지만, 각 커밋이 독립적으로 올바른지 확인
각 커밋은 독립적으로 빌드와 테스트를 통과해야 합니다. 해야 합니다 (즉, 각 커밋은 컴파일되어 테스트를 통과해야 함).
- 커밋에는 반드시 Developer Certificate of Origin (http://developercertificate.org) 서명이 포함되어야 합니다.<br/> - 커밋에는 개발자 출처 증명서 (http://developercertificate.org)
이는 기여자(및 소속된 고용주가 있을 경우) 가 [프로젝트 라이선스](../LICENCE) 에 동의함을 나타냅니다.<br/> 서명이 첨부되어야 하며, 이는 귀하 (및 해당되는 경우 고용주)가
Git에서는 `git commit` 명령어에 `-s` 옵션을 사용해 서명을 추가할 수 있습니다. [프로젝트 라이선스](../LICENCE). 조건에 구속되는 데 동의한다는 것을 나타냅니다.
git에서는 `git commit``-s` 옵션입니다
- PR이 검토되지 않거나 특정 리뷰어가 필요하면, - 패치가 검토되지 않거나 특정인이 검토해야 하는 경우, 풀 리퀘스트나
<br/> PR이나 댓글에서 리뷰어를 태그하거나 [이메일](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)을 참조하세요.
## 행동 강령 ## 행동 강령

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

@@ -0,0 +1,31 @@
# Contribuții la RustDesk
RustDesk primește cu plăcere contribuții din partea tuturor. Iată ghidurile dacă te gândești să ne ajuți:
## Contribuții
Contribuțiile la RustDesk sau la dependențele sale ar trebui făcute sub forma de pull request-uri pe GitHub. Fiecare pull request va fi revizuit de un contributor principal (cineva cu permisiunea de a aplica patch-uri) și fie va fi integrat în arborele principal, fie vor fi oferite sugestii pentru modificările necesare. Toate contribuțiile trebuie să urmeze acest format, chiar și cele ale contributorilor principali.
Dacă dorești să lucrezi la o problemă, te rugăm să o revendici mai întâi comentând pe GitHub issue-ul pe care vrei să lucrezi. Aceasta previne eforturi duplicate din partea contributorilor asupra aceleiași probleme.
## Lista de verificare pentru Pull Request
- Creează un branch din branch-ul `master` și, dacă este necesar, fă rebase la branch-ul `master` curent înainte de a trimite pull request-ul. Dacă nu se poate integra curat cu `master`, ți se poate cere să faci rebase la modificările tale.
- Commit-urile ar trebui să fie cât mai mici posibil, asigurând totodată că fiecare commit este corect independent (adică fiecare commit ar trebui să compileze și să treacă testele).
- Commit-urile trebuie să fie însoțite de un semnătura Developer Certificate of Origin (http://developercertificate.org), care indică faptul că tu (și angajatorul tău, dacă este cazul) ești de acord să respecți termenii [licenței proiectului](../LICENCE). În git, aceasta este opțiunea `-s` la `git commit`.
- Dacă patch-ul tău nu este revizuit sau ai nevoie ca o anumită persoană să-l revizuiască, poți @-reply unui reviewer cerând o revizuire în pull request sau într-un comentariu, sau poți solicita o revizuire prin [email](mailto:info@rustdesk.com).
- Adaugă teste relevante pentru bug-ul corectat sau pentru funcționalitatea nouă.
Pentru instrucțiuni specifice git, vezi [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Conduită
[Codul de Conduită RustDesk](https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md)
## Comunicare
Contributorii RustDesk frecventează [Discord](https://discord.gg/nDceKgxnkV).

View File

@@ -5,18 +5,14 @@ RustDesk приветствует вклад каждого.
## Вклад в развитие ## Вклад в развитие
Вклады в развитие RustDesk или его зависимости должны быть Вклады в развитие RustDesk или его зависимости должны быть сделаны в виде `pull request` на GitHub.
сделаны в виде `pull request` на GitHub. Каждый такой Каждый такой `pull request` будет рассмотрен основным участником (кем-то, у кого есть разрешение
`pull request` будет рассмотрен основным участником на влив исправлений) и либо помещен в основное дерево, либо Вам будет дан отзыв о необходимых правках.
(кем-то, у кого есть разрешение на влив исправлений) Все материалы должны соответствовать этому формату, даже те, которые поступают от основных авторов.
и либо помещен в основное дерево, либо Вам будет дан отзыв
о необходимых правках. Все материалы должны соответствовать
этому формату, даже те, которые поступают от основных авторов.
Если вы хотите поработать над какой-либо проблемой, то пожалуйста, Если вы хотите поработать над какой-либо проблемой, то пожалуйста, сначала напишите об этом,
сначала напишите об этом, создав тикет на GitHub, и описав, создав `issue` на GitHub, и описав, над чем вы хотите поработать. Это делается для того,
над чем вы хотите поработать. Это делается для того, чтобы чтобы предотвратить дублирование усилий участников по одному и тому же вопросу.
предотвратить дублирование усилий участников по одному и тому же вопросу.
## Контрольный список для Ваших `pull request` ## Контрольный список для Ваших `pull request`
@@ -24,13 +20,13 @@ RustDesk приветствует вклад каждого.
ветку перед отправкой `pull request`. При наличии конфликтов слияния вам будет ветку перед отправкой `pull request`. При наличии конфликтов слияния вам будет
предложено их устранить, возможно при помощи того же `rebase`. предложено их устранить, возможно при помощи того же `rebase`.
- Коммиты должны быть, по возможности, небольшим, при этом гарантируя, что каждаый - Коммиты должны быть, по возможности, небольшими, при этом гарантируя, что каждый
коммит является независимо правильным (т.е., каждый коммит должен компилироваться и проходить тесты). коммит является независимо правильным (т.е., каждый коммит должен компилироваться и проходить тесты).
- Коммиты должны сопровождаться `Developer Certificate of Origin` - Коммиты должны сопровождаться подписью `Developer Certificate of Origin`
(http://developercertificate.org) подписью, которая укажет на то, что вы (и (http://developercertificate.org), которая укажет на то, что вы (и ваш работодатель,
ваш работодатель, если это применимо) согласны соблюдать условия если это применимо) согласны соблюдать условия [лицензии проекта](../LICENCE).
[лицензии проекта](../LICENCE). В `git` это флаг `-s` при использовании `git commit` В `git` это флаг `-s` при использовании `git commit`
- Если ваш патч не проходит рецензирование или вам нужно, - Если ваш патч не проходит рецензирование или вам нужно,
чтобы его проверил конкретный человек, Вы можете ответить рецензенту через `@`, чтобы его проверил конкретный человек, Вы можете ответить рецензенту через `@`,
@@ -40,7 +36,7 @@ RustDesk приветствует вклад каждого.
Для получения конкретных инструкций `git` см. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). Для получения конкретных инструкций `git` см. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow).
## Кодекс поведения участников и вкладчиков ## Правила поведения участников и вкладчиков
Нормы поведения внутри сообщества подробно описаны [здесь](CODE_OF_CONDUCT-RU.md). Нормы поведения внутри сообщества подробно описаны [здесь](CODE_OF_CONDUCT-RU.md).

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

@@ -9,9 +9,9 @@
<b> لغتك الأم, <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> و <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>, README نحن بحاجة إلى مساعدتك لترجمة هذا </b> <b> لغتك الأم, <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> و <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>, README نحن بحاجة إلى مساعدتك لترجمة هذا </b>
</p> </p>
[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) :تواصل معنا عبر [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برنامج آخر لسطح المكتب عن بعد، مكتوب بـ .Rustبرنامج آخر لسطح المكتب عن بعد، مكتوب بـ
يعمل خارج الصندوق، لا حاجة إلى إعدادات. لديك سيطرة كاملة على بياناتك، دون مخاوف بشأن الأمن. يمكنك استخدام خادم يعمل خارج الصندوق، لا حاجة إلى إعدادات. لديك سيطرة كاملة على بياناتك، دون مخاوف بشأن الأمن. يمكنك استخدام خادم
@@ -27,6 +27,7 @@
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases) [**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
## التبعيات ## التبعيات
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم

View File

@@ -9,10 +9,10 @@
<b>Potřebujeme Vaši pomoc s překladem tohoto README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b> <b>Potřebujeme Vaši pomoc s překladem tohoto README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
</p> </p>
Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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. 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

@@ -9,13 +9,13 @@
<b>Vi har brug for din hjælp til at oversætte denne README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href=" https://github.com/rustdesk/doc.rustdesk.com">Dokument</a> til dit modersmål</b> <b>Vi har brug for din hjælp til at oversætte denne README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href=" https://github.com/rustdesk/doc.rustdesk.com">Dokument</a> til dit modersmål</b>
</p> </p>
Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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).
RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang. RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) for at få hjælp til at komme i gang.
[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)

View File

@@ -14,9 +14,9 @@
> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. > Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung.
Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,9 +9,9 @@
<b>Ni bezonas helpon traduki tiun README kaj <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">la interfacon</a> al via denaska lingvo</b> <b>Ni bezonas helpon traduki tiun README kaj <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">la interfacon</a> al via denaska lingvo</b>
</p> </p>
Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -13,9 +13,9 @@
> **Descargo de responsabilidad por mal uso:** <br> > **Descargo de responsabilidad por mal uso:** <br>
> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El mal uso, como el acceso no autorizado, el control o la invasión de la privacidad, va estrictamente en contra de nuestras directrices. Los autores no se hacen responsables de ningún uso indebido de la aplicación. > Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El mal uso, como el acceso no autorizado, el control o la invasión de la privacidad, va estrictamente en contra de nuestras directrices. Los autores no se hacen responsables de ningún uso indebido de la aplicación.
Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,10 +9,10 @@
<p align="center" dir="auto">[<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-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-GR.md">Ελληνικά</a>]</p> <p align="center" dir="auto">[<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-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-GR.md">Ελληνικά</a>]</p>
<p dir="rtl" align="center"><b>برای ترجمه این سند (README)، <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang" dir="rtl">رابط کاربری RustDesk</a>، <a href="https://github.com/rustdesk/doc.rustdesk.com" dir="rtl">و مستندات آن</a> به زبان مادری شما به کمکتان نیازمندیم. </b></p> <p dir="rtl" align="center"><b>برای ترجمه این سند (README)، <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang" dir="rtl">رابط کاربری RustDesk</a>، <a href="https://github.com/rustdesk/doc.rustdesk.com" dir="rtl">و مستندات آن</a> به زبان مادری شما به کمکتان نیازمندیم. </b></p>
با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) با ما گفتگو کنید: [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 نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. راست‌دسک (RustDesk) نرم‌افزاری برای کارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید.

View File

@@ -9,9 +9,9 @@
<b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b> <b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b>
</p> </p>
Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,9 +9,9 @@
<b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>. <b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>.
</p> </p>
Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,15 +9,15 @@
<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">Doc</a> στη μητρική σας γλώσσα</b> <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">Doc</a> στη μητρική σας γλώσσα</b>
</p> </p>
Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Επικοινωνήστε μαζί μας μέσω: [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). Ένα λογισμικό απομακρυσμένης επιφάνειας εργασίας, γραμμένο σε γλώσσα Rust. Δεν χρειάζεται κάποια παραμετροποίηση, λειτουργεί αμέσως μετά την εγκατάσταση. Έχετε τον πλήρη έλεγχο των δεδομένων σας, χωρίς να ανησυχείτε για την ασφάλειά τους. Μπορείτε να χρησιμοποιήσετε τους προκαθορισμένους διακομιστές rendezvous/αναμετάδοσης, [να εγκαταστήσετε τον δικό σας διακομιστή](https://rustdesk.com/server), ή [να αναπτύξετε ένα δικό σας διακομιστή rendezvous/αναμετάδοσης](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
[**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ)

View File

@@ -9,9 +9,9 @@
<b>Kell a segítséged, hogy lefordítsuk ezt a README-t, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">a RustDesk UI-t</a> és a <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentációt</a> az anyanyelvedre</b> <b>Kell a segítséged, hogy lefordítsuk ezt a README-t, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">a RustDesk UI-t</a> és a <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentációt</a> az anyanyelvedre</b>
</p> </p>
Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,9 +9,9 @@
<b>Kami membutuhkan bantuanmu untuk menterjemahkan file README dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke Bahasa Indonesia</b> <b>Kami membutuhkan bantuanmu untuk menterjemahkan file README dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke Bahasa Indonesia</b>
</p> </p>
Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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) [![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

@@ -9,9 +9,9 @@
<b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b> <b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b>
</p> </p>
Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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) [![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

@@ -5,20 +5,20 @@
<a href="#how-to-build-with-docker">Docker</a> <a href="#how-to-build-with-docker">Docker</a>
<a href="#file-structure">Structure</a> <a href="#file-structure">Structure</a>
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br> [<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>]<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 Doc</a>の翻訳者を歓迎します!</b> <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 Doc</a>の翻訳者を歓迎します!</b>
</p> </p>
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 私たちと話す: [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)こともできます。 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) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDeskは皆さんの貢献を歓迎します。 RustDeskは皆さんの貢献を歓迎します。
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。 貢献の方法については[CONTRIBUTING.md](CONTRIBUTING.md)をご確認ください。
[**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)

View File

@@ -1,64 +1,84 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Servers</a> <a href="#빌드를 위한 원시 단계">빌드</a>
<a href="#raw-steps-to-build">Build</a> <a href="#Docker로 빌드하는 방법">Docker</a>
<a href="#how-to-build-with-docker">Docker</a> <a href="#파일 구조">구조</a>
<a href="#file-structure">Structure</a> <a href="#스크린샷">스냇샷</a><br>
<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-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>
[<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-GR.md">Ελληνικά</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>
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
</p> </p>
채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) > [!Caution]
> **오용 면책 조항:** <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) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요. RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움이 필요하면 [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조세요.
[**RustDesk는 어떻게 작동하는가?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**자주 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**바이너리 다운로드**](https://github.com/rustdesk/rustdesk/releases)
## 의존관계 [**개발자 빌드**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
데스크탑판에는 GUI에 [sciter](https://sciter.com/)가 사용되었습니다. sciter dynamic library 를 다운로드해주세요. [<img src="https://f-droid.org/badge/get-it-on.png"
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="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)을 확인하세요.
Sciter 동적 라이브러리를 직접 다운로드하세요.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [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) [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
모바일 버전은 Flutter를 사용합니다. 데스크탑 또한 Sciter에서 Flutter로 마이그레이션할 예정입니다. ## 빌드를 위한 원시 단계
## 빌드 순서 - 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 - 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/) ## [빌드](https://rustdesk.com/docs/en/dev/build/)
## Linux에서 빌드 순서 ## Linux에서 빌드하는 방법
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
```sh ```sh
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
```sh ```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -79,7 +99,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
``` ```
### libvpx 수정 (For Fedora용) ### libvpx 수정 (Fedora용)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src
@@ -97,7 +117,7 @@ cd
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env source $HOME/.cargo/env
git clone https://github.com/rustdesk/rustdesk git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk cd rustdesk
mkdir -p target/debug mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
@@ -105,60 +125,58 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
## Docker 빌드하는 방법 ## Docker 빌드하는 방법
리포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. 먼저 리포지토리를 복제하고 Docker 컨테이너를 빌드합니다:
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
cd rustdesk cd rustdesk
git submodule update --init --recursive
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다. 그런 다음 응용 프로그램을 빌드해야 할 때마다 다음 명령을 실행합니다:
```sh ```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 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` 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다. 번째 빌드는 종속성이 캐시되기까지 시간이 오래 걸릴 수 있으며, 이후 빌드는 빨라집니다. 또한 빌드 명령에 다른 인수를 지정해야 하는 경우 명령 끝 `<OPTIONAL-ARGS>` 위치에 인수를 지정할 수 있습니다. 예를 들어 최적화된 릴리스 버전을 빌드하려면 위의 명령 뒤에 `--release`추가하면 됩니다. 결과 실행 파일은 시스템의 대상 폴더에서 사용할 수 있으며 실행할 수 있습니다::
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
``` ```
혹은 출시용 실행 파일을 실행할 수도 있습니다. 또는 릴리스 실행 파일을 실행하는 경우:
```sh ```sh
target/release/rustdesk 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/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/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫폼 고유 키보드/마우스 컨트롤 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫폼 키보드/마우스 제어
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows, Linux, macOS용 파일 복사 및 붙여넣기 구현
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, 클립보드, 입력, 비디오 서비스 그리고 네트워크 연결 - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 더 이상 사용되지 않는 Sciter UI (지원 중단)
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 접속 시작 - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오/클립보드/입력/비디오 서비스 및 네트워크 연결
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트 (TCP hole punching) 혹은 relayed 접속 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 연결 시작
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드 - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트 - **[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
> [!주의] ## 스크린샷
> **오용에 대한 면책 조항:** <br>
> RustDesk의 개발자들은 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용인하거나 지원하지 않습니다. 무단 접근, 제어 또는 개인정보 침해와 같은 오용은 우리의 지침을 엄격히 위반하는 것입니다. 개발자들은 애플리케이션의 오용에 대해 책임을 지지 않습니다.
## 스냅샷 ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)

View File

@@ -9,9 +9,9 @@
<b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b> <b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b>
</p> </p>
ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [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). റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ. ബോക്‌സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/server), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo).

View File

@@ -9,9 +9,9 @@
<b>Wij hebben uw hulp nodig om dit README bestand te vertalen, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> en <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> naar uw moedertaal</b> <b>Wij hebben uw hulp nodig om dit README bestand te vertalen, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> en <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> naar uw moedertaal</b>
</p> </p>
Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,15 +9,15 @@
<b>Vi trenger din hjelp til å oversette denne README-en, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> tid ditt morsmål</b> <b>Vi trenger din hjelp til å oversette denne README-en, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> tid ditt morsmål</b>
</p> </p>
Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](docs/CONTRIBUTING-NO.md) for hjelp med oppstart. RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](CONTRIBUTING-NO.md) for hjelp med oppstart.
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)

View File

@@ -9,9 +9,9 @@
<b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b> <b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b>
</p> </p>
Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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

@@ -9,9 +9,9 @@
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b> <b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
</p> </p>
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) 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). 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).

181
docs/README-RO.md Normal file
View File

@@ -0,0 +1,181 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - desktopul tău la distanță"><br>
<a href="../README.md#raw-steps-to-build">Construire</a>
<a href="../README.md#how-to-build-with-docker">Docker</a>
<a href="../README.md#file-structure">Structură</a>
<a href="../README.md#snapshot">Capturi</a><br>
[<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>] | [<a href="README-RO.md">Română</a>]<br>
<b>Avem nevoie de ajutorul tău pentru a traduce acest README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> și <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> în limba ta maternă</b>
</p>
> [!Atenție]
> **Declinare de responsabilitate privind utilizarea abuzivă:** <br>
> Dezvoltatorii RustDesk nu susțin sau aprobă utilizarea neetică sau ilegală a acestui software. Utilizarea abuzivă, cum ar fi accesul neautorizat, controlul sau invadarea intimității, este strict împotriva regulilor noastre. Autorii nu sunt responsabili pentru utilizarea necorespunzătoare a aplicației.
Conversați cu noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Advanced%20Features-blue)](https://rustdesk.com/pricing.html)
Încă o soluție de desktop la distanță scrisă în Rust. Funcționează imediat, fără configurare necesară. Ai control total asupra datelor tale, fără probleme de securitate. Poți folosi serverul nostru de rendezvous/relay, [să-ți configurezi propriul server](https://rustdesk.com/server) sau [să scrii propriul server de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
![imagine](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk primește contribuții de la oricine. Vezi [CONTRIBUTING.md](../docs/CONTRIBUTING.md) pentru ajutor la început.
[**ÎNTREBĂRI FRECVENTE (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**DESCĂRCARE BINARE**](https://github.com/rustdesk/rustdesk/releases)
[**BUILD NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://f-droid.org/badge/get-it-on.png"
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="Get it on Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
## Dependențe
Versiunile desktop folosesc Flutter sau Sciter (depreciat) pentru interfață; acest ghid este pentru Sciter doar, deoarece este mai ușor și mai prietenos pentru început. Vezi [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) pentru construire cu Flutter.
Te rugăm să descarci singur librăria dinamică Sciter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Pași pentru construire (Raw Steps to build)
- Pregătește mediul de dezvoltare Rust și mediul de construire C++
- Instalează [vcpkg](https://github.com/microsoft/vcpkg) și setează corect variabila de mediu `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
- rulează `cargo run`
## [Construire](https://rustdesk.com/docs/en/dev/build/)
## Cum se construiește pe Linux
### Ubuntu 18 (Debian 10)
```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
```
### Arch (Manjaro)
```sh
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Instalează vcpkg
```sh
git clone https://github.com/microsoft/vcpkg
cd vcpkg
git checkout 2023.04.15
cd ..
vcpkg/bootstrap-vcpkg.sh
export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Repară libvpx (Pentru Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
cd *
./configure
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
make
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Build
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Cum să construiești cu Docker
Începe prin clonarea repository-ului și construirea imaginii Docker:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
Apoi, de fiecare dată când trebuie să construiești aplicația, rulează comanda următoare:
```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
```
Reține că prima construire poate dura mai mult până când dependențele sunt în cache; construirile ulterioare vor fi mai rapide. De asemenea, dacă trebuie să specifici argumente diferite comenzii de build, le poți adăuga la finalul comenzii în poziția `<OPTIONAL-ARGS>`. De exemplu, pentru a construi o versiune optimizată de release, adaugă `--release`. Executabilul rezultat va fi disponibil în folderul `target` pe sistemul tău, și poate fi rulat cu:
```sh
target/debug/rustdesk
```
Sau, dacă rulezi un executabil release:
```sh
target/release/rustdesk
```
Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, altfel aplicația poate să nu găsească resursele necesare. De asemenea, reține că alte subcomenzi cargo, cum ar fi `install` sau `run`, nu sunt acceptate în prezent prin această metodă, deoarece ar instala sau rula programul în interiorul containerului în loc de gazdă.
## Structura fișierelor
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funcții fs pentru transfer de fișiere și alte funcții utilitare
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: capturare ecran
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control tastatură/mouse specific platformei
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementare copy/paste pentru fișiere pentru Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interfață Sciter învechită (depreciată)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servicii audio/clipboard/input/video și conexiuni de rețea
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inițiază o conexiune peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
## Capturi de ecran
![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -1,42 +1,52 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Ваш удаленый рабочий стол"><br> <img src="../res/logo-header.svg" alt="RustDesk - Ваш удаленый рабочий стол"><br>
<a href="#free-public-servers">Servers</a> <a href="#первичные-шаги-для-сборки">Первичные шаги для сборки</a>
<a href="#raw-steps-to-build">Build</a> <a href="#как-собрать-с-помощью-Docker">Как собрать с помощью Docker</a>
<a href="#how-to-build-with-docker">Docker</a> <a href="#структура-файлов">Структура файлов</a>
<a href="#file-structure">Structure</a> <a href="#скриншоты">Скриншоты</a><br>
<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-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-GR.md">Ελληνικά</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-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-GR.md">Ελληνικά</a>]<br>
<b>Нам нужна ваша помощь для перевода этого README <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> <b>Нам нужна ваша помощь в переводе этого README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">интерфейса RustDesk</a>
и документацию RustDesk на ваш родной язык. <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a></b> и <a href="https://github.com/rustdesk/doc.rustdesk.com">документации RustDesk</a> на ваш родной язык.</b>
</p> </p>
Общение с нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) > [!Caution]
> **Отказ от ответственности за неправомерное использование** <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. Работает из коробки, не требует настройки. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой](https://github.com/rustdesk/rustdesk-server-demo). [![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).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk приветствует вклад каждого. Ознакомьтесь с [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы для понимания. RustDesk приветствует вклад каждого. Ознакомьтесь с [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы для понимания.
[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) (Документация на английском языке)
[**Часто задаваемые вопросы**](https://github.com/rustdesk/rustdesk/wiki/FAQ) (Страница на английском языке)
[**СКАЧАТЬ ПРИЛОЖЕНИЕ**](https://github.com/rustdesk/rustdesk/releases) [**СКАЧАТЬ ПРИЛОЖЕНИЕ**](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://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) [<img src="https://f-droid.org/badge/get-it-on.png"
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="Get it on Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
## Зависимости ## Зависимости
Настольные версии используют [sciter](https://sciter.com/) для графического интерфейса, загрузите динамическую библиотеку sciter самостоятельно. Для ПК-версии используются библиотеки Flutter или Sciter (устаревшее) для графического интерфейса. Данное руководство подразумевает работу с Sciter, так как он более простой в использовании и с ним легче начать работу. Вы можете также посмотреть на механизм нашего [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для сборок на Flutter.
Загрузите динамическую библиотеку Flutter самостоятельно.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [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) [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
Мобильные версии используют Flutter. В будущем мы перенесем настольную версию со Sciter на Flutter.
## Первичные шаги для сборки ## Первичные шаги для сборки
@@ -45,22 +55,32 @@ RustDesk приветствует вклад каждого. Ознакомьт
- Установите [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 - 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/ru/dev/build/)
## Как собрать на Linux ## Как собрать на Linux
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
```sh ```sh
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### openSUSE Tumbleweed
```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
```sh ```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -99,7 +119,7 @@ cd
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env source $HOME/.cargo/env
git clone https://github.com/rustdesk/rustdesk git clone --recurse-submodules https://github.com/rustdesk/rustdesk
cd rustdesk cd rustdesk
mkdir -p target/debug mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
@@ -114,16 +134,17 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
cd rustdesk cd rustdesk
git submodule update --init --recursive
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
Затем каждый раз, когда вам нужно собрать приложение, запускайте следующую команду: Затем при каждой сборке приложения выполняйте следующую команду:
```sh ```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 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`. Полученный исполняемый файл будет доступен в целевой папке вашей системы и может быть запущен с помощью: Обратите внимание, что первая сборка может занять больше времени, прежде чем зависимости будут кэшированы, но последующие сборки будут выполняться быстрее. Кроме того, если вам нужно указать другие аргументы для команды сборки, вы можете сделать это в конце команды в переменной `<OPTIONAL-ARGS>`. Например, если вы хотите создать оптимизированную версию, вы должны выполнить приведенную выше команду и в конце строки добавить `--release`. Полученный исполняемый файл будет доступен в целевой папке вашей системы и может быть запущен с помощью следующей команды:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
@@ -135,29 +156,28 @@ target/debug/rustdesk
target/release/rustdesk target/release/rustdesk
``` ```
Пожалуйста, убедитесь, что вы запускаете эти команды из корня репозитория RustDesk, иначе приложение не сможет найти необходимые ресурсы. Также обратите внимание, что другие cargo подкоманды, такие как `install` или `run`, в настоящее время не поддерживаются этим методом, поскольку они будут устанавливать или запускать программу внутри контейнера, а не на хосте. Пожалуйста, убедитесь, что вы запускаете эти команды из корня репозитория RustDesk, иначе приложение не сможет найти необходимые ресурсы. Также обратите внимание, что другие подкоманды Cargo, такие как `install` или `run`, в настоящее время не поддерживаются этим методом, поскольку они будут устанавливать или запускать программу внутри контейнера, а не на хосте.
## Структура файлов ## Структура файлов
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфиг, обертка tcp/udp, protobuf, функции fs для передачи файлов и некоторые другие служебные функции - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфигурация, враппер TCP/UDP, protobuf, функции файловой системы для передачи файлов и некоторые другие служебные функции
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захват экрана - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захват экрана
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: специфичное для платформы управление клавиатурой/мышью - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: специфичное для платформы управление клавиатурой/мышью
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: функционал буфера обмена файлами для Windows, Linux, и macOS
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио/буфера обмена/ввода/видео и сетевых подключений - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее)
- **[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/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: свяжитесь с [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитесь удаленного прямого (обход TCP NAT) или ретранслируемого соединения - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код - **[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)**: JavaScript для Web-клиента Flutter
> **Отказ от ответственности за неправомерное использование:** <br>
> Разработчики RustDesk не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование, такое как несанкционированный доступ, контроль или вторжение в частную жизнь, строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения.
## Скриншоты ## Скриншоты
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Менеджер соединений](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Подключение к удалённому рабочему столу на Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![Передача файлов](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![TCP-туннелирование](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)

View File

@@ -7,34 +7,37 @@
<a href="#file-structure">Dosya Yapısı</a> <a href="#file-structure">Dosya Yapısı</a>
<a href="#snapshot">Ekran Görüntüleri</a><br> <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> [<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> </p>
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) > [!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.
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). 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)
[![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)
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) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın. 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) [**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" [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="F-Droid'de Alın" alt="F-Droid'de Alın"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb) 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 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.
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
Lütfen Sciter dinamik kütüphanesini kendiniz indirin. 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. - 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 - 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
@@ -123,7 +126,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
## Docker ile Derleme Nasıl Yapılır ## 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 ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
@@ -131,44 +134,40 @@ cd rustdesk
docker build -t "rustdesk-builder" . 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 ```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 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 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:
komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
```sh ```sh
target/debug/rustdesk 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 ```sh
target/release/rustdesk 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ı ## 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/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ü - **[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 - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: platforma özgü kopyala/yapıştır implementasyonları.
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Eski Sciter UI (kaldırılacak)
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pano/input/video servisleri ve ağ bağlantıları
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler - **[src/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 - **[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](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/js)**: Flutter web istemcisi için JavaScript - **[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 ## Ekran Görüntüleri

View File

@@ -9,15 +9,15 @@
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B> <b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
</p> </p>
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Спілкування з нами: [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). Ще один застосунок для віддаленого керування стільницею, написаний на 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) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі. RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
[**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ)

View File

@@ -11,9 +11,9 @@
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b> <b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
</p> </p>
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) 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). 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

@@ -8,13 +8,13 @@
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</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-GR.md">Ελληνικά</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</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-GR.md">Ελληνικά</a>]<br>
</p> </p>
> [!警告] > [!CAUTION]
> **免责声明:** <br> > **免责声明:** <br>
> RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。 > RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) 与我们交流: [知乎](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) 或者[自己设置](https://rustdesk.com/server)

View File

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

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

@@ -0,0 +1,9 @@
# Politica de Securitate
## Raportarea unei Vulnerabilități
Acordăm o mare importanță securității proiectului. Încurajăm toți utilizatorii să ne raporteze orice vulnerabilități pe care le descoperă.
Dacă găsești o vulnerabilitate de securitate în proiectul RustDesk, te rugăm să o raportezi responsabil trimițând un e-mail la info@rustdesk.com.
În acest moment, nu avem un program de recompense pentru descoperirea de bug-uri. Suntem o echipă mică care încearcă să rezolve o problemă mare.
Te rugăm să raportezi orice vulnerabilitate în mod responsabil, astfel încât să putem continua să construim o aplicație sigură pentru întreaga comunitate.

View File

@@ -55,7 +55,7 @@
], ],
"finish-args": [ "finish-args": [
"--share=ipc", "--share=ipc",
"--socket=fallback-x11", "--socket=x11",
"--socket=wayland", "--socket=wayland",
"--share=network", "--share=network",
"--filesystem=home", "--filesystem=home",

View File

@@ -1,4 +1,6 @@
import com.google.protobuf.gradle.* import com.google.protobuf.gradle.*
import groovy.json.JsonSlurper
plugins { plugins {
id "com.google.protobuf" version "0.9.4" id "com.google.protobuf" version "0.9.4"
id "com.android.application" id "com.android.application"
@@ -30,8 +32,37 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
dependencies { // Add rustls-platform-verifier Android support
implementation 'com.google.protobuf:protobuf-javalite:3.20.1' String findRustlsPlatformVerifierMavenDir() {
def dependencyText = providers.exec {
it.workingDir = new File("../..")
commandLine("cargo", "metadata", "--format-version", "1")
}.standardOutput.asText.get()
def dependencyJson = new JsonSlurper().parseText(dependencyText)
def pkg = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }
if (pkg == null) {
throw new GradleException("rustls-platform-verifier-android package not found in cargo metadata!")
}
def manifestPath = file(pkg.manifest_path)
def mavenDir = new File(manifestPath.parentFile, "maven")
if (!mavenDir.exists()) {
throw new GradleException("Maven directory not found at: ${mavenDir.path}")
}
println("✓ Found rustls-platform-verifier maven repo at: ${mavenDir.path}")
return mavenDir.path
}
repositories {
maven {
url = findRustlsPlatformVerifierMavenDir()
metadataSources.artifact()
}
} }
protobuf { protobuf {
@@ -67,7 +98,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb" applicationId "com.carriez.flutter_hbb"
minSdkVersion 21 minSdkVersion 22
targetSdkVersion 33 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@@ -97,8 +128,10 @@ flutter {
} }
dependencies { dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5' implementation 'com.github.getActivity:XXPermissions:18.5'
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } } implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.caverock:androidsvg-aar:1.4'
implementation "rustls:rustls-platform-verifier:0.1.1"
} }

View File

@@ -1,4 +1,7 @@
# Keep class members from protobuf generated code. # Keep class members from protobuf generated code.
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
<fields>; <fields>;
} }
# Keep rustls-platform-verifier classes for JNI
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }

View File

@@ -23,6 +23,7 @@
</queries> </queries>
<application <application
android:name=".MainApplication"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="RustDesk" android:label="RustDesk"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"

View File

@@ -70,7 +70,7 @@ class InputService : AccessibilityService() {
private val logTag = "input service" private val logTag = "input service"
private var leftIsDown = false private var leftIsDown = false
private val touchPath = Path() private var touchPath = Path()
private var stroke: GestureDescription.StrokeDescription? = null private var stroke: GestureDescription.StrokeDescription? = null
private var lastTouchGestureStartTime = 0L private var lastTouchGestureStartTime = 0L
private var mouseX = 0 private var mouseX = 0
@@ -278,7 +278,11 @@ class InputService : AccessibilityService() {
} }
private fun startGesture(x: Int, y: Int) { private fun startGesture(x: Int, y: Int) {
touchPath.reset() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
touchPath.reset()
} else {
touchPath = Path()
}
touchPath.moveTo(x.toFloat(), y.toFloat()) touchPath.moveTo(x.toFloat(), y.toFloat())
lastTouchGestureStartTime = System.currentTimeMillis() lastTouchGestureStartTime = System.currentTimeMillis()
lastX = x lastX = x
@@ -294,14 +298,31 @@ class InputService : AccessibilityService() {
} }
try { try {
if (stroke == null) { if (stroke == null) {
stroke = GestureDescription.StrokeDescription( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
touchPath, stroke = GestureDescription.StrokeDescription(
0, touchPath,
duration, 0,
willContinue duration,
) willContinue
)
} else {
stroke = GestureDescription.StrokeDescription(
touchPath,
0,
duration
)
}
} else { } else {
stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue)
} else {
stroke = null
stroke = GestureDescription.StrokeDescription(
touchPath,
0,
duration
)
}
} }
stroke?.let { stroke?.let {
val builder = GestureDescription.Builder() val builder = GestureDescription.Builder()
@@ -316,19 +337,49 @@ class InputService : AccessibilityService() {
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun continueGesture(x: Int, y: Int) { private fun continueGesture(x: Int, y: Int) {
doDispatchGesture(x, y, true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
touchPath.reset() doDispatchGesture(x, y, true)
touchPath.moveTo(x.toFloat(), y.toFloat()) touchPath.reset()
lastTouchGestureStartTime = System.currentTimeMillis() touchPath.moveTo(x.toFloat(), y.toFloat())
lastX = x lastTouchGestureStartTime = System.currentTimeMillis()
lastY = y lastX = x
lastY = y
} else {
touchPath.lineTo(x.toFloat(), y.toFloat())
}
}
@RequiresApi(Build.VERSION_CODES.N)
private fun endGestureBelowO(x: Int, y: Int) {
try {
touchPath.lineTo(x.toFloat(), y.toFloat())
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
if (duration <= 0) {
duration = 1
}
val stroke = GestureDescription.StrokeDescription(
touchPath,
0,
duration
)
val builder = GestureDescription.Builder()
builder.addStroke(stroke)
Log.d(logTag, "end gesture x:$x y:$y time:$duration")
dispatchGesture(builder.build(), null, null)
} catch (e: Exception) {
Log.e(logTag, "endGesture error:$e")
}
} }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun endGesture(x: Int, y: Int) { private fun endGesture(x: Int, y: Int) {
doDispatchGesture(x, y, false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
touchPath.reset() doDispatchGesture(x, y, false)
stroke = null touchPath.reset()
stroke = null
} else {
endGestureBelowO(x, y)
}
} }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)

View File

@@ -62,7 +62,13 @@ class MainActivity : FlutterActivity() {
channelTag channelTag
) )
initFlutterChannel(flutterMethodChannel!!) initFlutterChannel(flutterMethodChannel!!)
thread { setCodecInfo() } thread {
try {
setCodecInfo()
} catch (e: Exception) {
Log.e("MainActivity", "Failed to setCodecInfo: ${e.message}", e)
}
}
} }
override fun onResume() { override fun onResume() {
@@ -316,7 +322,7 @@ class MainActivity : FlutterActivity() {
codecObject.put("mime_type", mime_type) codecObject.put("mime_type", mime_type)
val caps = codec.getCapabilitiesForType(mime_type) val caps = codec.getCapabilitiesForType(mime_type)
if (codec.isEncoder) { if (codec.isEncoder) {
// Encoders max_height and max_width are interchangeable // Encoder's max_height and max_width are interchangeable
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) { if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
return@forEach return@forEach
} }

View File

@@ -0,0 +1,17 @@
package com.carriez.flutter_hbb
import android.app.Application
import android.util.Log
import ffi.FFI
class MainApplication : Application() {
companion object {
private const val TAG = "MainApplication"
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "App start")
FFI.onAppStart(applicationContext)
}
}

View File

@@ -122,9 +122,9 @@ class MainService : Service() {
val authorized = jsonObject["authorized"] as Boolean val authorized = jsonObject["authorized"] as Boolean
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
val type = if (isFileTransfer) { val type = if (isFileTransfer) {
translate("File Connection") translate("Transfer file")
} else { } else {
translate("Screen Connection") translate("Share screen")
} }
if (authorized) { if (authorized) {
if (!isFileTransfer && !isStart) { if (!isFileTransfer && !isStart) {

View File

@@ -13,6 +13,7 @@ object FFI {
} }
external fun init(ctx: Context) external fun init(ctx: Context)
external fun onAppStart(ctx: Context)
external fun setClipboardManager(clipboardManager: RdClipboardManager) external fun setClipboardManager(clipboardManager: RdClipboardManager)
external fun startServer(app_dir: String, custom_client_config: String) external fun startServer(app_dir: String, custom_client_config: String)
external fun startService() external fun startService()

View File

@@ -18,8 +18,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false id "com.android.application" version "7.3.1" apply false
id "org.jetbrains.kotlin.android" version "1.9.10" apply false id "org.jetbrains.kotlin.android" version "2.1.21" apply false
} }
include ":app" include ":app"

View File

@@ -43,6 +43,8 @@
<true/> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -60,6 +62,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</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/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:uni_links/uni_links.dart'; import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -30,6 +32,7 @@ import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart'; import 'mobile/pages/remote_page.dart';
import 'mobile/pages/view_camera_page.dart'; import 'mobile/pages/view_camera_page.dart';
import 'mobile/pages/terminal_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'desktop/pages/view_camera_page.dart' as desktop_view_camera; import 'desktop/pages/view_camera_page.dart' as desktop_view_camera;
@@ -41,6 +44,7 @@ import 'package:flutter_hbb/native/win32.dart'
if (dart.library.html) 'package:flutter_hbb/web/win32.dart'; if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
import 'package:flutter_hbb/native/common.dart' import 'package:flutter_hbb/native/common.dart'
if (dart.library.html) 'package:flutter_hbb/web/common.dart'; if (dart.library.html) 'package:flutter_hbb/web/common.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
final globalKey = GlobalKey<NavigatorState>(); final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey(); final navigationBarKey = GlobalKey();
@@ -73,6 +77,9 @@ bool _ignoreDevicePixelRatio = true;
int windowsBuildNumber = 0; int windowsBuildNumber = 0;
DesktopType? desktopType; DesktopType? desktopType;
// Tolerance used for floating-point position comparisons to avoid precision errors.
const double _kPositionEpsilon = 1e-6;
bool get isMainDesktopWindow => bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm; desktopType == DesktopType.main || desktopType == DesktopType.cm;
@@ -99,10 +106,15 @@ enum DesktopType {
remote, remote,
fileTransfer, fileTransfer,
viewCamera, viewCamera,
terminal,
cm, cm,
portForward, portForward,
} }
bool isDoubleEqual(double a, double b) {
return (a - b).abs() < _kPositionEpsilon;
}
class IconFont { class IconFont {
static const _family1 = 'Tabbar'; static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar'; static const _family2 = 'PeerSearchbar';
@@ -1571,7 +1583,9 @@ bool option2bool(String option, String value) {
String bool2option(String option, bool b) { String bool2option(String option, bool b) {
String res; String res;
if (option.startsWith('enable-')) { if (option.startsWith('enable-') &&
option != kOptionEnableUdpPunch &&
option != kOptionEnableIpv6Punch) {
res = b ? defaultOptionYes : 'N'; res = b ? defaultOptionYes : 'N';
} else if (option.startsWith('allow-') || } else if (option.startsWith('allow-') ||
option == kOptionStopService || option == kOptionStopService ||
@@ -1579,7 +1593,9 @@ String bool2option(String option, bool b) {
option == kOptionForceAlwaysRelay) { option == kOptionForceAlwaysRelay) {
res = b ? 'Y' : defaultOptionNo; res = b ? 'Y' : defaultOptionNo;
} else { } else {
assert(false); if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) {
assert(false);
}
res = b ? 'Y' : 'N'; res = b ? 'Y' : 'N';
} }
return res; return res;
@@ -1615,7 +1631,8 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead. // Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
// Because all session options use `Y` and `<Empty>` as values. // 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) { if (searchText.isEmpty) {
return true; return true;
} }
@@ -1626,11 +1643,14 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
peer.username.toLowerCase().contains(searchText)) { peer.username.toLowerCase().contains(searchText)) {
return true; return true;
} }
final alias = peer.alias; if (peer.alias.toLowerCase().contains(searchText)) {
if (alias.isEmpty) { return true;
return false;
} }
return alias.toLowerCase().contains(searchText); if (peerTabShowNote(peerTabIndex) &&
peer.note.toLowerCase().contains(searchText)) {
return true;
}
return false;
} }
/// Get the image for the current [platform]. /// Get the image for the current [platform].
@@ -1660,6 +1680,15 @@ class LastWindowPosition {
LastWindowPosition(this.width, this.height, this.offsetWidth, LastWindowPosition(this.width, this.height, this.offsetWidth,
this.offsetHeight, this.isMaximized, this.isFullscreen); 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() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
"width": width, "width": width,
@@ -1699,24 +1728,36 @@ String get windowFramePrefix =>
? "incoming_" ? "incoming_"
: (bind.isOutgoingOnly() ? "outgoing_" : "")); : (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 /// Save window position and size on exit
/// Note that windowId must be provided if it's subwindow /// 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) { if (type != WindowType.Main && windowId == null) {
debugPrint( debugPrint(
"Error: windowId cannot be null when saving positions for sub window"); "Error: windowId cannot be null when saving positions for sub window");
} }
late Offset position; Offset? position;
late Size sz; Size? sz;
late bool isMaximized; late bool isMaximized;
bool isFullscreen = stateGlobal.fullscreen.isTrue; bool isFullscreen = stateGlobal.fullscreen.isTrue;
setPreFrame() { setPreFrame() {
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name); final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
var lpos = LastWindowPosition.loadFromString(pos); var lpos = LastWindowPosition.loadFromString(pos);
position = Offset( if (lpos != null) {
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy); if (lpos.offsetWidth != null && lpos.offsetHeight != null) {
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height); position = Offset(lpos.offsetWidth!, lpos.offsetHeight!);
}
if (lpos.width != null && lpos.height != null) {
sz = Size(lpos.width!, lpos.height!);
}
}
} }
switch (type) { switch (type) {
@@ -1756,30 +1797,56 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
} }
break; break;
} }
if (isWindows) { if (isWindows && position != null) {
const kMinOffset = -10000; const kMinOffset = -10000;
const kMaxOffset = 10000; const kMaxOffset = 10000;
if (position.dx < kMinOffset || if (position!.dx < kMinOffset ||
position.dy < kMinOffset || position!.dy < kMinOffset ||
position.dx > kMaxOffset || position!.dx > kMaxOffset ||
position.dy > kMaxOffset) { position!.dy > kMaxOffset) {
debugPrint("Invalid position: $position, ignore saving position"); debugPrint("Invalid position: $position, ignore saving position");
return; return;
} }
} }
final pos = LastWindowPosition( final pos = LastWindowPosition(sz?.width, sz?.height, position?.dx,
sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen); 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( final WindowKey key = (type: type, windowId: windowId);
k: windowFramePrefix + type.name, v: pos.toString());
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && final bool haveNewWindowPosition =
windowId != null) { (_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
await _saveSessionWindowPosition( final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
type, windowId, isMaximized, isFullscreen, pos);
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);
}
} }
} }
@@ -1845,6 +1912,8 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
return Size(restoreWidth, restoreHeight); return Size(restoreWidth, restoreHeight);
} }
// Consider using Rect.contains() instead,
// though the implementation is not exactly the same.
bool isPointInRect(Offset point, Rect rect) { bool isPointInRect(Offset point, Rect rect) {
return point.dx >= rect.left && return point.dx >= rect.left &&
point.dx <= rect.right && point.dx <= rect.right &&
@@ -1942,8 +2011,24 @@ Future<bool> restoreWindowPosition(WindowType type,
var lpos = LastWindowPosition.loadFromString(pos); var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) { if (lpos == null) {
debugPrint("no window position saved, ignoring position restoration"); debugPrint("No window position saved, trying to center the window.");
return false; 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 (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
if (!isRemotePeerPos && windowId != null) { if (!isRemotePeerPos && windowId != null) {
@@ -2117,6 +2202,11 @@ enum UriLinkType {
viewCamera, viewCamera,
portForward, portForward,
rdp, rdp,
terminal,
}
setEnvTerminalAdmin() {
bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y');
} }
// uri link handler // uri link handler
@@ -2181,6 +2271,17 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
id = args[i + 1]; id = args[i + 1];
i++; i++;
break; break;
case '--terminal':
type = UriLinkType.terminal;
id = args[i + 1];
i++;
break;
case '--terminal-admin':
setEnvTerminalAdmin();
type = UriLinkType.terminal;
id = args[i + 1];
i++;
break;
case '--password': case '--password':
password = args[i + 1]; password = args[i + 1];
i++; i++;
@@ -2230,6 +2331,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
password: password, forceRelay: forceRelay); password: password, forceRelay: forceRelay);
}); });
break; break;
case UriLinkType.terminal:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newTerminal(id!,
password: password, forceRelay: forceRelay);
});
break;
} }
return true; return true;
@@ -2247,7 +2354,9 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
"file-transfer", "file-transfer",
"view-camera", "view-camera",
"port-forward", "port-forward",
"rdp" "rdp",
"terminal",
"terminal-admin",
]; ];
if (uri.authority.isEmpty && if (uri.authority.isEmpty &&
uri.path.split('').every((char) => char == '/')) { uri.path.split('').every((char) => char == '/')) {
@@ -2276,21 +2385,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
} }
} }
} else if (options.contains(uri.authority)) { } else if (options.contains(uri.authority)) {
final optionIndex = options.indexOf(uri.authority);
command = '--${uri.authority}'; command = '--${uri.authority}';
if (uri.path.length > 1) { if (uri.path.length > 1) {
id = uri.path.substring(1); id = uri.path.substring(1);
} }
if (isMobile && id != null) {
if (optionIndex == 0 || optionIndex == 1) {
connect(Get.context!, id);
} else if (optionIndex == 2) {
connect(Get.context!, id, isFileTransfer: true);
} else if (optionIndex == 3) {
connect(Get.context!, id, isViewCamera: true);
}
return null;
}
} else if (uri.authority.length > 2 && } else if (uri.authority.length > 2 &&
(uri.path.length <= 1 || (uri.path.length <= 1 ||
(uri.path == '/r' || uri.path.startsWith('/r@')))) { (uri.path == '/r' || uri.path.startsWith('/r@')))) {
@@ -2314,12 +2412,29 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
} }
} }
if (isMobile) { if (isMobile && id != null) {
if (id != null) { final forceRelay = queryParameters["relay"] != null;
final forceRelay = queryParameters["relay"] != null; final password = queryParameters["password"];
connect(Get.context!, id, forceRelay: forceRelay);
return null; // Determine connection type based on command
if (command == '--file-transfer') {
connect(Get.context!, id,
isFileTransfer: true, forceRelay: forceRelay, password: password);
} else if (command == '--view-camera') {
connect(Get.context!, id,
isViewCamera: true, forceRelay: forceRelay, password: password);
} 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);
} }
return null;
} }
List<String> args = List.empty(growable: true); List<String> args = List.empty(growable: true);
@@ -2341,6 +2456,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
connectMainDesktop(String id, connectMainDesktop(String id,
{required bool isFileTransfer, {required bool isFileTransfer,
required bool isViewCamera, required bool isViewCamera,
required bool isTerminal,
required bool isTcpTunneling, required bool isTcpTunneling,
required bool isRDP, required bool isRDP,
bool? forceRelay, bool? forceRelay,
@@ -2365,6 +2481,12 @@ connectMainDesktop(String id,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
connToken: connToken, connToken: connToken,
forceRelay: forceRelay); forceRelay: forceRelay);
} else if (isTerminal) {
await rustDeskWinManager.newTerminal(id,
password: password,
isSharedPassword: isSharedPassword,
connToken: connToken,
forceRelay: forceRelay);
} else { } else {
await rustDeskWinManager.newRemoteDesktop(id, await rustDeskWinManager.newRemoteDesktop(id,
password: password, password: password,
@@ -2381,6 +2503,7 @@ connectMainDesktop(String id,
connect(BuildContext context, String id, connect(BuildContext context, String id,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isViewCamera = false, bool isViewCamera = false,
bool isTerminal = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false, bool isRDP = false,
bool forceRelay = false, bool forceRelay = false,
@@ -2403,7 +2526,7 @@ connect(BuildContext context, String id,
id = id.replaceAll(' ', ''); id = id.replaceAll(' ', '');
final oldId = id; final oldId = id;
id = await bind.mainHandleRelayId(id: id); id = await bind.mainHandleRelayId(id: id);
final forceRelay2 = id != oldId || forceRelay; forceRelay = id != oldId || forceRelay;
assert(!(isFileTransfer && isTcpTunneling && isRDP), assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type"); "more than one connect type");
@@ -2413,17 +2536,19 @@ connect(BuildContext context, String id,
id, id,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera, isViewCamera: isViewCamera,
isTerminal: isTerminal,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP, isRDP: isRDP,
password: password, password: password,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
forceRelay: forceRelay2, forceRelay: forceRelay,
); );
} else { } else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id, 'id': id,
'isFileTransfer': isFileTransfer, 'isFileTransfer': isFileTransfer,
'isViewCamera': isViewCamera, 'isViewCamera': isViewCamera,
'isTerminal': isTerminal,
'isTcpTunneling': isTcpTunneling, 'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP, 'isRDP': isRDP,
'password': password, 'password': password,
@@ -2457,7 +2582,10 @@ connect(BuildContext context, String id,
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage( builder: (BuildContext context) => FileManagerPage(
id: id, password: password, isSharedPassword: isSharedPassword), id: id,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay),
), ),
); );
} }
@@ -2472,7 +2600,6 @@ connect(BuildContext context, String id,
id: id, id: id,
toolbarState: ToolbarState(), toolbarState: ToolbarState(),
password: password, password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
), ),
), ),
@@ -2482,10 +2609,25 @@ connect(BuildContext context, String id,
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (BuildContext context) => ViewCameraPage( builder: (BuildContext context) => ViewCameraPage(
id: id, password: password, isSharedPassword: isSharedPassword), id: id,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay),
), ),
); );
} }
} else if (isTerminal) {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => TerminalPage(
id: id,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay,
),
),
);
} else { } else {
if (isWeb) { if (isWeb) {
Navigator.push( Navigator.push(
@@ -2496,7 +2638,6 @@ connect(BuildContext context, String id,
id: id, id: id,
toolbarState: ToolbarState(), toolbarState: ToolbarState(),
password: password, password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
), ),
), ),
@@ -2506,7 +2647,10 @@ connect(BuildContext context, String id,
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (BuildContext context) => RemotePage( builder: (BuildContext context) => RemotePage(
id: id, password: password, isSharedPassword: isSharedPassword), id: id,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay),
), ),
); );
} }
@@ -2688,7 +2832,7 @@ class ServerConfig {
} catch (err) { } catch (err) {
final input = msg.split('').reversed.join(''); final input = msg.split('').reversed.join('');
final bytes = base64Decode(base64.normalize(input)); final bytes = base64Decode(base64.normalize(input));
json = jsonDecode(utf8.decode(bytes)); json = jsonDecode(utf8.decode(bytes, allowMalformed: true));
} }
idServer = json['host'] ?? ''; idServer = json['host'] ?? '';
relayServer = json['relay'] ?? ''; relayServer = json['relay'] ?? '';
@@ -2805,7 +2949,7 @@ Future<void> updateSystemWindowTheme() async {
/// ///
/// Note: not found a general solution for rust based AVFoundation bingding. /// Note: not found a general solution for rust based AVFoundation bingding.
/// [AVFoundation] crate has compile error. /// [AVFoundation] crate has compile error.
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos"); const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host");
enum PermissionAuthorizeType { enum PermissionAuthorizeType {
undetermined, undetermined,
@@ -2878,6 +3022,7 @@ Future<bool> canBeBlocked() async {
return access_mode == 'view' || (access_mode.isEmpty && !option); return access_mode == 'view' || (access_mode.isEmpty && !option);
} }
// to-do: web not implemented
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async { Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
if (use != null && !await use()) { if (use != null && !await use()) {
block.value = false; block.value = false;
@@ -3445,6 +3590,9 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
} }
Widget loadPowered(BuildContext context) { Widget loadPowered(BuildContext context) {
if (bind.mainGetBuildinOption(key: "hide-powered-by-me") == 'Y') {
return SizedBox.shrink();
}
return MouseRegion( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
@@ -3841,3 +3989,39 @@ String get appName {
} }
return _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']), "platform": _platform(p.info['os']),
"hostname": p.info['device_name'], "hostname": p.info['device_name'],
"device_group_name": p.device_group_name, "device_group_name": p.device_group_name,
"note": p.note,
}); });
} }
@@ -248,15 +249,17 @@ class AbProfile {
String name; String name;
String owner; String owner;
String? note; String? note;
dynamic info;
int rule; 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) AbProfile.fromJson(Map<String, dynamic> json)
: guid = json['guid'] ?? '', : guid = json['guid'] ?? '',
name = json['name'] ?? '', name = json['name'] ?? '',
owner = json['owner'] ?? '', owner = json['owner'] ?? '',
note = json['note'] ?? '', note = json['note'] ?? '',
info = json['info'],
rule = json['rule'] ?? 0; rule = json['rule'] ?? 0;
} }

View File

@@ -77,9 +77,11 @@ class CurrentDisplayState {
class ConnectionType { class ConnectionType {
final Rx<String> _secure = kInvalidValueStr.obs; final Rx<String> _secure = kInvalidValueStr.obs;
final Rx<String> _direct = kInvalidValueStr.obs; final Rx<String> _direct = kInvalidValueStr.obs;
final Rx<String> _stream_type = kInvalidValueStr.obs;
Rx<String> get secure => _secure; Rx<String> get secure => _secure;
Rx<String> get direct => _direct; Rx<String> get direct => _direct;
Rx<String> get stream_type => _stream_type;
static String get strSecure => 'secure'; static String get strSecure => 'secure';
static String get strInsecure => 'insecure'; static String get strInsecure => 'insecure';
@@ -94,9 +96,14 @@ class ConnectionType {
_direct.value = v ? strDirect : strIndirect; _direct.value = v ? strDirect : strIndirect;
} }
void setStreamType(String v) {
_stream_type.value = v;
}
bool isValid() { bool isValid() {
return _secure.value != kInvalidValueStr && 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: ''); IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: ''); TextEditingController aliasController = TextEditingController(text: '');
TextEditingController passwordController = TextEditingController(text: ''); TextEditingController passwordController = TextEditingController(text: '');
TextEditingController noteController = TextEditingController(text: '');
final tags = List.of(gFFI.abModel.currentAbTags); final tags = List.of(gFFI.abModel.currentAbTags);
var selectedTag = List<dynamic>.empty(growable: true).obs; var selectedTag = List<dynamic>.empty(growable: true).obs;
final style = TextStyle(fontSize: 14.0); final style = TextStyle(fontSize: 14.0);
@@ -494,7 +495,11 @@ class _AddressBookState extends State<AddressBook> {
password = passwordController.text; password = passwordController.text;
} }
String? errMsg2 = await gFFI.abModel.addIdToCurrent( String? errMsg2 = await gFFI.abModel.addIdToCurrent(
id, aliasController.text.trim(), password, selectedTag); id,
aliasController.text.trim(),
password,
selectedTag,
noteController.text);
if (errMsg2 != null) { if (errMsg2 != null) {
setState(() { setState(() {
isInProgress = false; isInProgress = false;
@@ -600,6 +605,24 @@ class _AddressBookState extends State<AddressBook> {
), ),
).workaroundFreezeLinuxMint(), ).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) if (gFFI.abModel.currentAbTags.isNotEmpty)
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,

View File

@@ -0,0 +1,156 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/utils/scale.dart';
import 'package:flutter_hbb/common.dart';
/// Base class providing shared custom scale control logic for both mobile and desktop widgets.
/// Implementations must provide [ffi] and [onScaleChanged] getters.
abstract class CustomScaleControls<T extends StatefulWidget> extends State<T> {
/// FFI instance for session interaction
FFI get ffi;
/// Callback invoked when scale value changes
ValueChanged<int>? get onScaleChanged;
late int _scaleValue;
late final Debouncer<int> _debouncerScale;
// Normalized slider position in [0, 1]. We map it nonlinearly to percent.
double _scalePos = 0.0;
int get scaleValue => _scaleValue;
double get scalePos => _scalePos;
int mapPosToPercent(double p) => _mapPosToPercent(p);
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 _clampScale(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 _clampScale(v.round());
} else {
final q = (p - pivotPos) / (1.0 - pivotPos); // 0..1
final v = pivotPercent + q * (maxPercent - pivotPercent);
return _clampScale(v.round());
}
}
// Map percent [5,1000] → normalized position [0,1]
double _mapPercentToPos(int percent) {
final p = _clampScale(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();
_scaleValue = 100;
_debouncerScale = Debouncer<int>(
kDebounceCustomScaleDuration,
onChanged: (v) async {
await _applyScale(v);
},
initialValue: _scaleValue,
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
final v = await getSessionCustomScalePercent(ffi.sessionId);
if (mounted) {
setState(() {
_scaleValue = v;
_scalePos = _mapPercentToPos(v);
});
}
} catch (e, st) {
debugPrint('[CustomScale] Failed to get initial value: $e');
debugPrintStack(stackTrace: st);
}
});
}
Future<void> _applyScale(int v) async {
v = clampCustomScalePercent(v);
setState(() {
_scaleValue = v;
});
try {
await bind.sessionSetFlutterOption(
sessionId: ffi.sessionId,
k: kCustomScalePercentKey,
v: v.toString());
final curStyle = await bind.sessionGetViewStyle(sessionId: ffi.sessionId);
if (curStyle != kRemoteViewStyleCustom) {
await bind.sessionSetViewStyle(
sessionId: ffi.sessionId, value: kRemoteViewStyleCustom);
}
await ffi.canvasModel.updateViewStyle();
if (isMobile) {
HapticFeedback.selectionClick();
}
onScaleChanged?.call(v);
} catch (e, st) {
debugPrint('[CustomScale] Apply failed: $e');
debugPrintStack(stackTrace: st);
}
}
void nudgeScale(int delta) {
final next = _clampScale(_scaleValue + delta);
setState(() {
_scaleValue = next;
_scalePos = _mapPercentToPos(next);
});
onScaleChanged?.call(next);
_debouncerScale.value = next;
}
@override
void dispose() {
_debouncerScale.cancel();
super.dispose();
}
void onSliderChanged(double v) {
final snapped = _snapNormalizedPos(v);
final next = _mapPosToPercent(snapped);
if (next != _scaleValue || snapped != _scalePos) {
setState(() {
_scalePos = snapped;
_scaleValue = next;
});
onScaleChanged?.call(next);
_debouncerScale.value = next;
}
}
}

View File

@@ -7,20 +7,29 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
import '../../common.dart'; import '../../common.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import 'address_book.dart'; import 'address_book.dart';
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) { void clientClose(SessionID sessionId, FFI ffi) async {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?', if (allowAskForNoteAtEndOfConnection(ffi, true)) {
'', dialogManager); if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
return;
}
closeConnection();
} else {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
'', ffi.dialogManager);
}
} }
abstract class ValidationRule { abstract class ValidationRule {
@@ -819,23 +828,33 @@ void enterPasswordDialog(
} }
void enterUserLoginDialog( void enterUserLoginDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async { SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
await _connectDialog( await _connectDialog(
sessionId, sessionId,
dialogManager, dialogManager,
osUsernameController: TextEditingController(), osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(), osPasswordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
); );
} }
void enterUserLoginAndPasswordDialog( void enterUserLoginAndPasswordDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async { SessionID sessionId,
OverlayDialogManager dialogManager,
String osAccountDescTip,
bool canRememberAccount) async {
await _connectDialog( await _connectDialog(
sessionId, sessionId,
dialogManager, dialogManager,
osUsernameController: TextEditingController(), osUsernameController: TextEditingController(),
osPasswordController: TextEditingController(), osPasswordController: TextEditingController(),
passwordController: TextEditingController(), passwordController: TextEditingController(),
osAccountDescTip: osAccountDescTip,
canRememberAccount: canRememberAccount,
); );
} }
@@ -845,17 +864,28 @@ _connectDialog(
TextEditingController? osUsernameController, TextEditingController? osUsernameController,
TextEditingController? osPasswordController, TextEditingController? osPasswordController,
TextEditingController? passwordController, TextEditingController? passwordController,
String? osAccountDescTip,
bool canRememberAccount = true,
}) async { }) async {
final errUsername = ''.obs;
var rememberPassword = false; var rememberPassword = false;
if (passwordController != null) { if (passwordController != null) {
rememberPassword = rememberPassword =
await bind.sessionGetRemember(sessionId: sessionId) ?? false; await bind.sessionGetRemember(sessionId: sessionId) ?? false;
} }
var rememberAccount = false; var rememberAccount = false;
if (osUsernameController != null) { if (canRememberAccount && osUsernameController != null) {
rememberAccount = rememberAccount =
await bind.sessionGetRemember(sessionId: sessionId) ?? false; await bind.sessionGetRemember(sessionId: sessionId) ?? false;
} }
if (osUsernameController != null) {
osUsernameController.addListener(() {
if (errUsername.value.isNotEmpty) {
errUsername.value = '';
}
});
}
dialogManager.dismissAll(); dialogManager.dismissAll();
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
cancel() { cancel() {
@@ -864,6 +894,13 @@ _connectDialog(
} }
submit() { submit() {
if (osUsernameController != null) {
if (osUsernameController.text.trim().isEmpty) {
errUsername.value = translate('Empty Username');
setState(() {});
return;
}
}
final osUsername = osUsernameController?.text.trim() ?? ''; final osUsername = osUsernameController?.text.trim() ?? '';
final osPassword = osPasswordController?.text.trim() ?? ''; final osPassword = osPasswordController?.text.trim() ?? '';
final password = passwordController?.text.trim() ?? ''; final password = passwordController?.text.trim() ?? '';
@@ -927,26 +964,39 @@ _connectDialog(
} }
return Column( return Column(
children: [ children: [
descWidget(translate('login_linux_tip')), if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)),
DialogTextField( DialogTextField(
title: translate(DialogTextField.kUsernameTitle), title: translate(DialogTextField.kUsernameTitle),
controller: osUsernameController, controller: osUsernameController,
prefixIcon: DialogTextField.kUsernameIcon, prefixIcon: DialogTextField.kUsernameIcon,
errorText: null, 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( PasswordWidget(
controller: osPasswordController, controller: osPasswordController,
autoFocus: false, autoFocus: false,
), ),
rememberWidget( if (canRememberAccount)
translate('remember_account_tip'), rememberWidget(
rememberAccount, translate('remember_account_tip'),
(v) { rememberAccount,
if (v != null) { (v) {
setState(() => rememberAccount = v); if (v != null) {
} setState(() => rememberAccount = v);
}, }
), },
),
], ],
); );
} }
@@ -1136,7 +1186,7 @@ void showRequestElevationDialog(
DialogTextField( DialogTextField(
controller: userController, controller: userController,
title: translate('Username'), title: translate('Username'),
hintText: translate('eg: admin'), hintText: translate('elevation_username_tip'),
prefixIcon: DialogTextField.kUsernameIcon, prefixIcon: DialogTextField.kUsernameIcon,
errorText: errUser.isEmpty ? null : errUser.value, errorText: errUser.isEmpty ? null : errUser.value,
), ),
@@ -1468,56 +1518,71 @@ showSetOSAccount(
}); });
} }
Widget buildNoteTextField({
required TextEditingController controller,
required VoidCallback onEscape,
}) {
final focusNode = FocusNode(
onKey: (FocusNode node, RawKeyEvent evt) {
if (evt.logicalKey.keyLabel == 'Enter') {
if (evt is RawKeyDownEvent) {
int pos = controller.selection.base.offset;
controller.text =
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
controller.selection =
TextSelection.fromPosition(TextPosition(offset: pos + 1));
}
return KeyEventResult.handled;
}
if (evt.logicalKey.keyLabel == 'Esc') {
if (evt is RawKeyDownEvent) {
onEscape();
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
return TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: translate('input note here'),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.all(12),
),
minLines: 5,
maxLines: null,
maxLength: 256,
controller: controller,
focusNode: focusNode,
).workaroundFreezeLinuxMint();
}
showAuditDialog(FFI ffi) async { showAuditDialog(FFI ffi) async {
final controller = TextEditingController(text: ffi.auditNote); final controller = TextEditingController(
text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId));
ffi.dialogManager.show((setState, close, context) { ffi.dialogManager.show((setState, close, context) {
submit() { submit() {
var text = controller.text; var text = controller.text;
bind.sessionSendNote(sessionId: ffi.sessionId, note: text); bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
ffi.auditNote = text;
close(); close();
} }
late final focusNode = FocusNode(
onKey: (FocusNode node, RawKeyEvent evt) {
if (evt.logicalKey.keyLabel == 'Enter') {
if (evt is RawKeyDownEvent) {
int pos = controller.selection.base.offset;
controller.text =
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
controller.selection =
TextSelection.fromPosition(TextPosition(offset: pos + 1));
}
return KeyEventResult.handled;
}
if (evt.logicalKey.keyLabel == 'Esc') {
if (evt is RawKeyDownEvent) {
close();
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('Note')), title: Text(translate('Note')),
content: SizedBox( content: SizedBox(
width: 250, width: 250,
height: 120, height: 120,
child: TextField( child: buildNoteTextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration.collapsed(
hintText: 'input note here',
),
maxLines: null,
maxLength: 256,
controller: controller, controller: controller,
focusNode: focusNode, onEscape: close,
).workaroundFreezeLinuxMint()), )),
actions: [ actions: [
dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit) dialogButton('OK', onPressed: submit)
@@ -1528,6 +1593,223 @@ showAuditDialog(FFI ffi) async {
}); });
} }
bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) {
if (ffi == null) {
return false;
}
return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) &&
bind
.sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn")
.isNotEmpty &&
bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty &&
bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty &&
(!closedByControlling ||
bind.willSessionCloseCloseSession(sessionId: ffi.sessionId));
}
// return value: close canceled
// true: return
// false: go on
Future<bool> desktopTryShowTabAuditDialogCloseCancelled(
{required String id, required DesktopTabController tabController}) async {
try {
final page =
tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page;
final ffi = (page as dynamic).ffi;
final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi);
return res;
} catch (e) {
debugPrint('Failed to show audit dialog: $e');
return false;
}
}
// return value:
// true: return
// false: go on
Future<bool> showConnEndAuditDialogCloseCanceled(
{required FFI ffi, String? type, String? title, String? text}) async {
final res = await _showConnEndAuditDialogCloseCanceled(
ffi: ffi, type: type, title: title, text: text);
if (res == true) {
return true;
}
return false;
}
// return value:
// true: return
// false / null: go on
Future<bool?> _showConnEndAuditDialogCloseCanceled({
required FFI ffi,
String? type,
String? title,
String? text,
}) async {
final closedByControlling = type == null;
final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling);
if (!showDialog) {
return false;
}
ffi.dialogManager.dismissAll();
Future<void> updateAuditNoteByGuid(String auditGuid, String note) async {
debugPrint('Updating audit note for GUID: $auditGuid, note: $note');
try {
final apiServer = await bind.mainGetApiServer();
if (apiServer.isEmpty) {
debugPrint('API server is empty, cannot update audit note');
return;
}
final url = '$apiServer/api/audit';
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
final body = jsonEncode({
'guid': auditGuid,
'note': note,
});
final response = await http.put(
Uri.parse(url),
headers: headers,
body: body,
);
if (response.statusCode == 200) {
debugPrint('Successfully updated audit note for GUID: $auditGuid');
} else {
debugPrint(
'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}');
}
} catch (e) {
debugPrint('Error updating audit note: $e');
}
}
final controller = TextEditingController();
bool askForNote =
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection);
bool isInProgress = false;
return await ffi.dialogManager.show<bool>((setState, close, context) {
cancel() {
close(true);
}
set() async {
if (isInProgress) return;
setState(() {
isInProgress = true;
});
var text = controller.text;
if (text.isNotEmpty) {
await updateAuditNoteByGuid(
bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text)
.timeout(const Duration(seconds: 6), onTimeout: () {
debugPrint('updateAuditNoteByGuid timeout after 6s');
});
}
// Save the "ask for note" preference
if (!isOptFixed) {
await mainSetLocalBoolOption(
kOptionAllowAskForNoteAtEndOfConnection, askForNote);
}
}
submit() async {
await set();
close(false);
}
final buttons = [
dialogButton('OK', onPressed: isInProgress ? null : submit)
];
if (type == 'relay-hint' || type == 'relay-hint2') {
buttons.add(dialogButton('Retry', onPressed: () async {
await set();
close(true);
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false);
}));
if (type == 'relay-hint2') {
buttons.add(dialogButton('Connect via relay', onPressed: () async {
await set();
close(true);
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true);
}));
}
}
if (closedByControlling) {
buttons.add(dialogButton('Cancel',
onPressed: isInProgress ? null : cancel, isOutline: true));
}
Widget content;
if (closedByControlling) {
content = SelectionArea(
child: msgboxContent(
'info', 'Close', 'Are you sure to close the connection?'));
} else {
content =
SelectionArea(child: msgboxContent(type, title ?? '', text ?? ''));
}
return CustomAlertDialog(
title: null,
content: SizedBox(
width: 350,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
content,
const SizedBox(height: 16),
SizedBox(
height: 120,
child: buildNoteTextField(
controller: controller,
onEscape: cancel,
),
),
if (!isOptFixed) ...[
const SizedBox(height: 8),
InkWell(
onTap: () {
setState(() {
askForNote = !askForNote;
});
},
child: Row(
children: [
Checkbox(
value: askForNote,
onChanged: (value) {
setState(() {
askForNote = value ?? false;
});
},
),
Expanded(
child: Text(
translate('note-at-conn-end-tip'),
style: const TextStyle(fontSize: 13),
),
),
],
),
),
],
if (isInProgress)
const LinearProgressIndicator().marginOnly(top: 4),
],
)),
actions: buttons,
onSubmit: submit,
onCancel: cancel,
);
});
}
void showConfirmSwitchSidesDialog( void showConfirmSwitchSidesDialog(
SessionID sessionId, String id, OverlayDialogManager dialogManager) async { SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
@@ -1742,6 +2024,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( void renameDialog(
{required String oldName, {required String oldName,
FormFieldValidator<String>? validator, FormFieldValidator<String>? validator,
@@ -2037,15 +2362,20 @@ void showWindowsSessionsDialog(
return CustomAlertDialog( return CustomAlertDialog(
title: null, title: null,
content: msgboxContent(type, title, text), content: Column(
mainAxisSize: MainAxisSize.min,
children: [
msgboxContent(type, title, text).marginOnly(bottom: 12),
ComboBox(
keys: sids,
values: names,
initialKey: selectedUserValue,
onChanged: (value) {
selectedUserValue = value;
}),
],
),
actions: [ actions: [
ComboBox(
keys: sids,
values: names,
initialKey: selectedUserValue,
onChanged: (value) {
selectedUserValue = value;
}),
dialogButton('Connect', onPressed: submit, isOutline: false), dialogButton('Connect', onPressed: submit, isOutline: false),
], ],
); );

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
enum GestureState { enum GestureState {
none, none,
@@ -96,6 +97,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
if (onTwoFingerScaleEnd != null) { if (onTwoFingerScaleEnd != null) {
onTwoFingerScaleEnd!(d); 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; break;
case GestureState.threeFingerVerticalDrag: case GestureState.threeFingerVerticalDrag:
debugPrint("ThreeFingerState.vertical onEnd"); debugPrint("ThreeFingerState.vertical onEnd");

View File

@@ -166,10 +166,13 @@ class _WidgetOPState extends State<WidgetOP> {
final String stateMsg = resultMap['state_msg']; final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg']; String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url']; final String? url = resultMap['url'];
final bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false;
final authBody = resultMap['auth_body']; final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _failedMsg != failedMsg) { if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) { if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); if (!urlLaunched) {
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
}
_url = url; _url = url;
} }
if (authBody != null) { if (authBody != null) {
@@ -397,6 +400,8 @@ Future<bool?> loginDialog() async {
String? passwordMsg; String? passwordMsg;
var isInProgress = false; var isInProgress = false;
final RxString curOP = ''.obs; final RxString curOP = ''.obs;
// Track hover state for the close icon
bool isCloseHovered = false;
final loginOptions = [].obs; final loginOptions = [].obs;
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
@@ -455,10 +460,14 @@ Future<bool?> loginDialog() async {
resp.user, resp.secret, isEmailVerification); resp.user, resp.secret, isEmailVerification);
} else { } else {
setState(() => isInProgress = false); setState(() => isInProgress = false);
// Workaround for web, close the dialog first, then show the verification code dialog.
// Otherwise, the text field will keep selecting the text and we can't input the code.
// Not sure why this happens.
if (isWeb && close != null) close(null);
final res = await verificationCodeDialog( final res = await verificationCodeDialog(
resp.user, resp.secret, isEmailVerification); resp.user, resp.secret, isEmailVerification);
if (res == true) { if (res == true) {
if (close != null) close(false); if (!isWeb && close != null) close(false);
return; return;
} }
} }
@@ -550,21 +559,27 @@ Future<bool?> loginDialog() async {
Text( Text(
translate('Login'), translate('Login'),
).marginOnly(top: MyTheme.dialogPadding), ).marginOnly(top: MyTheme.dialogPadding),
InkWell( MouseRegion(
child: Icon( onEnter: (_) => setState(() => isCloseHovered = true),
Icons.close, onExit: (_) => setState(() => isCloseHovered = false),
size: 25, child: InkWell(
// No need to handle the branch of null. child: Icon(
// Because we can ensure the color is not null when debug. Icons.close,
color: Theme.of(context) size: 25,
.textTheme // No need to handle the branch of null.
.titleLarge // Because we can ensure the color is not null when debug.
?.color color: isCloseHovered
?.withOpacity(0.55), ? Colors.white
: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.55),
),
onTap: onDialogCancel,
hoverColor: Colors.red,
borderRadius: BorderRadius.circular(5),
), ),
onTap: onDialogCancel,
hoverColor: Colors.red,
borderRadius: BorderRadius.circular(5),
).marginOnly(top: 10, right: 15), ).marginOnly(top: 10, right: 15),
], ],
); );

View File

@@ -50,6 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
) )
: Draggable( : Draggable(
checkKeyboard: true, checkKeyboard: true,
checkScreenSize: true,
position: draggablePositions.chatWindow, position: draggablePositions.chatWindow,
width: width, width: width,
height: height, height: height,
@@ -395,7 +396,10 @@ class _DraggableState extends State<Draggable> {
_chatModel?.setChatWindowPosition(position); _chatModel?.setChatWindowPosition(position);
} }
checkScreenSize() {} checkScreenSize() {
// Ensure the draggable always stays within current screen bounds
widget.position.tryAdjust(widget.width, widget.height, 1);
}
checkKeyboard() { checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
@@ -517,6 +521,12 @@ class IOSDraggableState extends State<IOSDraggable> {
_lastBottomHeight = bottomHeight; _lastBottomHeight = bottomHeight;
} }
@override
void initState() {
super.initState();
position.tryAdjust(_width, _height, 1);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
checkKeyboard(); checkKeyboard();

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) { makeChild(bool isPortrait, Peer peer) {
final name = hideUsernameOnCard == true final name = hideUsernameOnCard == true
? peer.hostname ? peer.hostname
@@ -134,6 +138,8 @@ class _PeerCardState extends State<_PeerCard>
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final showNote = _showNote(peer);
return Row( return Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
@@ -185,14 +191,44 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).marginOnly(top: isPortrait ? 0 : 2), ]).marginOnly(top: isPortrait ? 0 : 2),
Align( Row(
alignment: Alignment.centerLeft, children: [
child: Text( Flexible(
name, child: Tooltip(
style: isPortrait ? null : greyStyle, message: name,
textAlign: TextAlign.start, waitDuration: const Duration(seconds: 1),
overflow: TextOverflow.ellipsis, 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), ).marginOnly(top: 2),
@@ -278,7 +314,7 @@ class _PeerCardState extends State<_PeerCard>
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: child:
getPlatformImage(peer.platform, size: 60), getPlatformImage(peer.platform, size: 60),
).marginOnly(top: 4), ),
Row( Row(
children: [ children: [
Expanded( 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),
), ),
], ],
), ),
@@ -491,6 +545,8 @@ abstract class BasePeerCard extends StatelessWidget {
bool isViewCamera = false, bool isViewCamera = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false, bool isRDP = false,
bool isTerminal = false,
bool isTerminalRunAsAdmin = false,
}) { }) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text( childBuilder: (TextStyle? style) => Text(
@@ -498,6 +554,9 @@ abstract class BasePeerCard extends StatelessWidget {
style: style, style: style,
), ),
proc: () { proc: () {
if (isTerminalRunAsAdmin) {
setEnvTerminalAdmin();
}
connectInPeerTab( connectInPeerTab(
context, context,
peer, peer,
@@ -506,6 +565,7 @@ abstract class BasePeerCard extends StatelessWidget {
isViewCamera: isViewCamera, isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP, isRDP: isRDP,
isTerminal: isTerminal || isTerminalRunAsAdmin,
); );
}, },
padding: menuPadding, padding: menuPadding,
@@ -541,6 +601,24 @@ abstract class BasePeerCard extends StatelessWidget {
); );
} }
@protected
MenuEntryBase<String> _terminalAction(BuildContext context) {
return _connectCommonAction(
context,
'${translate('Terminal')} (beta)',
isTerminal: true,
);
}
@protected
MenuEntryBase<String> _terminalRunAsAdminAction(BuildContext context) {
return _connectCommonAction(
context,
'${translate('Terminal (Run as administrator)')} (beta)',
isTerminalRunAsAdmin: true,
);
}
@protected @protected
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) { MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
return _connectCommonAction( return _connectCommonAction(
@@ -892,8 +970,13 @@ class RecentPeerCard extends BasePeerCard {
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context), _viewCameraAction(context),
_terminalAction(context),
]; ];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
@@ -952,7 +1035,13 @@ class FavoritePeerCard extends BasePeerCard {
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context), _viewCameraAction(context),
_terminalAction(context),
]; ];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
} }
@@ -1006,8 +1095,13 @@ class DiscoveredPeerCard extends BasePeerCard {
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context), _viewCameraAction(context),
_terminalAction(context),
]; ];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
@@ -1060,12 +1154,20 @@ class AddressBookPeerCard extends BasePeerCard {
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context), _viewCameraAction(context),
_terminalAction(context),
]; ];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
// menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) { if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
@@ -1086,6 +1188,7 @@ class AddressBookPeerCard extends BasePeerCard {
if (gFFI.abModel.currentAbTags.isNotEmpty) { if (gFFI.abModel.currentAbTags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id)); menuItems.add(_editTagAction(peer.id));
} }
menuItems.add(_editNoteAction(peer.id));
} }
final addressbooks = gFFI.abModel.addressBooksCanWrite(); final addressbooks = gFFI.abModel.addressBooksCanWrite();
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) { if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
@@ -1125,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 @protected
@override @override
Future<String> _getAlias(String id) async => Future<String> _getAlias(String id) async =>
@@ -1193,12 +1311,20 @@ class MyGroupPeerCard extends BasePeerCard {
_connectAction(context), _connectAction(context),
_transferFileAction(context), _transferFileAction(context),
_viewCameraAction(context), _viewCameraAction(context),
_terminalAction(context),
]; ];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
} }
// menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _openNewConnInOptAction(peer.id));
// menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) { if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
@@ -1416,7 +1542,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isViewCamera = false, bool isViewCamera = false,
bool isTcpTunneling = false, bool isTcpTunneling = false,
bool isRDP = false}) async { bool isRDP = false,
bool isTerminal = false}) async {
var password = ''; var password = '';
bool isSharedPassword = false; bool isSharedPassword = false;
if (tab == PeerTabIndex.ab) { if (tab == PeerTabIndex.ab) {
@@ -1434,12 +1561,20 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
password = peer.password; password = peer.password;
isSharedPassword = true; isSharedPassword = true;
} }
if (password.isEmpty) {
final abPassword = gFFI.abModel.getdefaultSharedPassword();
if (abPassword != null) {
password = abPassword;
isSharedPassword = true;
}
}
} }
} }
connect(context, peer.id, connect(context, peer.id,
password: password, password: password,
isSharedPassword: isSharedPassword, isSharedPassword: isSharedPassword,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isTerminal: isTerminal,
isViewCamera: isViewCamera, isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
isRDP: isRDP); isRDP: isRDP);

View File

@@ -71,10 +71,12 @@ class _PeersView extends StatefulWidget {
final Peers peers; final Peers peers;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final PeerTabIndex peerTabIndex;
const _PeersView( const _PeersView(
{required this.peers, {required this.peers,
required this.peerCardBuilder, required this.peerCardBuilder,
required this.peerTabIndex,
this.peerFilter, this.peerFilter,
Key? key}) Key? key})
: super(key: key); : super(key: key);
@@ -395,8 +397,8 @@ class _PeersViewState extends State<_PeersView>
return peers; return peers;
} }
searchText = searchText.toLowerCase(); searchText = searchText.toLowerCase();
final matches = final matches = await Future.wait(
await Future.wait(peers.map((peer) => matchPeer(searchText, peer))); peers.map((peer) => matchPeer(searchText, peer, widget.peerTabIndex)));
final filteredList = List<Peer>.empty(growable: true); final filteredList = List<Peer>.empty(growable: true);
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (matches[i]) { if (matches[i]) {
@@ -441,7 +443,10 @@ abstract class BasePeersView extends StatelessWidget {
break; break;
} }
return _PeersView( 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 { class RawTouchGestureDetectorRegion extends StatefulWidget {
final Widget child; final Widget child;
final FFI ffi; final FFI ffi;
@@ -97,6 +104,10 @@ class _RawTouchGestureDetectorRegionState
bool _touchModePanStarted = false; bool _touchModePanStarted = false;
Offset _doubleFinerTapPosition = Offset.zero; 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; FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel; FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel; InputModel get inputModel => widget.inputModel;
@@ -111,22 +122,36 @@ class _RawTouchGestureDetectorRegionState
); );
} }
bool isNotTouchBasedDevice() {
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 { onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
_lastPosOfDoubleTapDown = d.localPosition; _lastPosOfDoubleTapDown = d.localPosition;
// Desktop or mobile "Touch mode" // Desktop or mobile "Touch mode"
_lastTapDownDetails = d; _lastTapDownDetails = d;
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
} }
} }
onTapUp(TapUpDetails d) async { onTapUp(TapUpDetails d) async {
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null; _lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
@@ -142,10 +167,15 @@ class _RawTouchGestureDetectorRegionState
} }
onTap() async { onTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
// Using `_lastTapDownPositionForMouseMode` instead.
if (shouldBlockMouseModeEvent()) {
return;
}
// Mobile, "Mouse mode" // Mobile, "Mouse mode"
await inputModel.tap(MouseButtons.left); await inputModel.tap(MouseButtons.left);
} }
@@ -153,17 +183,19 @@ class _RawTouchGestureDetectorRegionState
onDoubleTapDown(TapDownDetails d) async { onDoubleTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
_lastPosOfDoubleTapDown = d.localPosition; _lastPosOfDoubleTapDown = d.localPosition;
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
} }
} }
onDoubleTap() async { onDoubleTap() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
@@ -173,13 +205,19 @@ class _RawTouchGestureDetectorRegionState
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) { !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
return; 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);
await inputModel.tap(MouseButtons.left); await inputModel.tap(MouseButtons.left);
} }
onLongPressDown(LongPressDownDetails d) async { onLongPressDown(LongPressDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
@@ -194,11 +232,13 @@ class _RawTouchGestureDetectorRegionState
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
await inputModel.tapDown(MouseButtons.left); await inputModel.tapDown(MouseButtons.left);
} }
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
} }
} }
onLongPressUp() async { onLongPressUp() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
@@ -208,7 +248,7 @@ class _RawTouchGestureDetectorRegionState
// for mobiles // for mobiles
onLongPress() async { onLongPress() async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (!ffi.ffiModel.isPeerMobile) { if (!ffi.ffiModel.isPeerMobile) {
@@ -218,6 +258,10 @@ class _RawTouchGestureDetectorRegionState
if (!isMoved) { if (!isMoved) {
return; return;
} }
} else {
if (shouldBlockMouseModeEvent()) {
return;
}
} }
await inputModel.tap(MouseButtons.right); await inputModel.tap(MouseButtons.right);
} else { } else {
@@ -228,7 +272,7 @@ class _RawTouchGestureDetectorRegionState
} }
onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async { onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async {
if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) { if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
@@ -241,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
onDoubleFinerTapDown(TapDownDetails d) async { onDoubleFinerTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
_doubleFinerTapPosition = d.localPosition; _doubleFinerTapPosition = d.localPosition;
@@ -250,7 +294,7 @@ class _RawTouchGestureDetectorRegionState
onDoubleFinerTap(TapDownDetails d) async { onDoubleFinerTap(TapDownDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
@@ -266,25 +310,27 @@ class _RawTouchGestureDetectorRegionState
onHoldDragStart(DragStartDetails d) async { onHoldDragStart(DragStartDetails d) async {
lastDeviceKind = d.kind; lastDeviceKind = d.kind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
if (isSpecialHoldDragActive) return;
await inputModel.sendMouse('down', MouseButtons.left); await inputModel.sendMouse('down', MouseButtons.left);
} }
} }
onHoldDragUpdate(DragUpdateDetails d) async { onHoldDragUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
if (isSpecialHoldDragActive) return;
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
} }
onHoldDragEnd(DragEndDetails d) async { onHoldDragEnd(DragEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
@@ -296,7 +342,7 @@ class _RawTouchGestureDetectorRegionState
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
_lastTapDownDetails = null; _lastTapDownDetails = null;
lastDeviceKind = d.kind ?? lastDeviceKind; lastDeviceKind = d.kind ?? lastDeviceKind;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (handleTouch) { if (handleTouch) {
@@ -342,7 +388,7 @@ class _RawTouchGestureDetectorRegionState
} }
onOneFingerPanUpdate(DragUpdateDetails d) async { onOneFingerPanUpdate(DragUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
@@ -356,7 +402,7 @@ class _RawTouchGestureDetectorRegionState
onOneFingerPanEnd(DragEndDetails d) async { onOneFingerPanEnd(DragEndDetails d) async {
_touchModePanStarted = false; _touchModePanStarted = false;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (isDesktop || isWebDesktop) { if (isDesktop || isWebDesktop) {
@@ -370,15 +416,29 @@ class _RawTouchGestureDetectorRegionState
// scale + pan event // scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) { onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null; _lastTapDownDetails = null;
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if (isSpecialHoldDragActive) {
// Initialize the last focal point to calculate deltas manually.
_lastSpecialHoldDragFocalPoint = d.focalPoint;
}
} }
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async { onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; 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)) { if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt(); final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale; _scale = d.scale;
@@ -401,7 +461,7 @@ class _RawTouchGestureDetectorRegionState
} }
onTwoFingerScaleEnd(ScaleEndDetails d) async { onTwoFingerScaleEnd(ScaleEndDetails d) async {
if (lastDeviceKind != PointerDeviceKind.touch) { if (isNotTouchBasedDevice()) {
return; return;
} }
if ((isDesktop || isWebDesktop)) { if ((isDesktop || isWebDesktop)) {
@@ -416,7 +476,9 @@ class _RawTouchGestureDetectorRegionState
// No idea why we need to set the view style to "" here. // No idea why we need to set the view style to "" here.
// bind.sessionSetViewStyle(sessionId: sessionId, value: ""); // bind.sessionSetViewStyle(sessionId: sessionId, value: "");
} }
await inputModel.sendMouse('up', MouseButtons.left); if (!isSpecialHoldDragActive) {
await inputModel.sendMouse('up', MouseButtons.left);
}
} }
get onHoldDragCancel => null; get onHoldDragCancel => null;

View File

@@ -230,7 +230,6 @@ List<(String, String)> otherDefaultSettings() {
('Disable clipboard', kOptionDisableClipboard), ('Disable clipboard', kOptionDisableClipboard),
('Lock after session end', kOptionLockAfterSessionEnd), ('Lock after session end', kOptionLockAfterSessionEnd),
('Privacy mode', kOptionPrivacyMode), ('Privacy mode', kOptionPrivacyMode),
if (isMobile) ('Touch mode', kOptionTouchMode),
('True color (4:4:4)', kOptionI444), ('True color (4:4:4)', kOptionI444),
('Reverse mouse wheel', kKeyReverseMouseWheel), ('Reverse mouse wheel', kKeyReverseMouseWheel),
('swap-left-right-mouse', kOptionSwapLeftRightMouse), ('swap-left-right-mouse', kOptionSwapLeftRightMouse),
@@ -243,7 +242,8 @@ List<(String, String)> otherDefaultSettings() {
( (
'Use all my displays for the remote session', 'Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession kKeyUseAllMyDisplaysForTheRemoteSession
) ),
('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
]; ];
return v; return v;

View File

@@ -154,36 +154,38 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: () => ffi.cursorModel.reset())); onPressed: () => ffi.cursorModel.reset()));
} }
// https://github.com/rustdesk/rustdesk/pull/9731
// Does not work for connection established by "accept".
connectWithToken( connectWithToken(
{bool isFileTransfer = false, {bool isFileTransfer = false,
bool isViewCamera = false, bool isViewCamera = false,
bool isTcpTunneling = false}) { bool isTcpTunneling = false,
bool isTerminal = false}) {
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
connect(context, id, connect(context, id,
isFileTransfer: isFileTransfer, isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera, isViewCamera: isViewCamera,
isTerminal: isTerminal,
isTcpTunneling: isTcpTunneling, isTcpTunneling: isTcpTunneling,
connToken: connToken); connToken: connToken);
} }
// transferFile
if (isDefaultConn && isDesktop) { if (isDefaultConn && isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('Transfer file')), child: Text(translate('Transfer file')),
onPressed: () => connectWithToken(isFileTransfer: true)), onPressed: () => connectWithToken(isFileTransfer: true)),
); );
}
// viewCamera
if (isDefaultConn && isDesktop) {
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('View camera')), child: Text(translate('View camera')),
onPressed: () => connectWithToken(isViewCamera: true)), onPressed: () => connectWithToken(isViewCamera: true)),
); );
} v.add(
// tcpTunneling TTextMenu(
if (isDefaultConn && isDesktop) { child: Text('${translate('Terminal')} (beta)'),
onPressed: () => connectWithToken(isTerminal: true)),
);
v.add( v.add(
TTextMenu( TTextMenu(
child: Text(translate('TCP tunneling')), child: Text(translate('TCP tunneling')),
@@ -361,6 +363,11 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
child: Text(translate('Scale adaptive')), child: Text(translate('Scale adaptive')),
value: kRemoteViewStyleAdaptive, value: kRemoteViewStyleAdaptive,
groupValue: groupValue, groupValue: groupValue,
onChanged: onChanged),
TRadioMenu<String>(
child: Text(translate('Scale custom')),
value: kRemoteViewStyleCustom,
groupValue: groupValue,
onChanged: onChanged) onChanged: onChanged)
]; ];
} }

View File

@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@@ -27,7 +28,6 @@ const String kPlatformAdditionsAmyuniVirtualDisplays =
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl"; "supported_privacy_mode_impl";
const String kPlatformAdditionsSupportViewCamera = "support_view_camera";
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
@@ -47,6 +47,7 @@ const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopViewCamera = "view camera"; const String kAppTypeDesktopViewCamera = "view camera";
const String kAppTypeDesktopPortForward = "port forward"; const String kAppTypeDesktopPortForward = "port forward";
const String kAppTypeDesktopTerminal = "terminal";
const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info"; const String kWindowGetWindowInfo = "get_window_info";
@@ -57,11 +58,14 @@ const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide"; const String kWindowEventHide = "hide";
const String kWindowEventShow = "show"; const String kWindowEventShow = "show";
const String kWindowConnect = "connect"; const String kWindowConnect = "connect";
const String kWindowBumpMouse = "bump_mouse";
const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewViewCamera = "new_view_camera";
const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventNewTerminal = "new_terminal";
const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions";
const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetRemoteList = "get_remote_list";
@@ -75,6 +79,7 @@ const String kWindowEventOpenMonitorSession = "open_monitor_session";
const String kOptionViewStyle = "view_style"; const String kOptionViewStyle = "view_style";
const String kOptionScrollStyle = "scroll_style"; const String kOptionScrollStyle = "scroll_style";
const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
const String kOptionImageQuality = "image_quality"; const String kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render"; const String kOptionTextureRender = "use-texture-render";
@@ -103,6 +108,8 @@ const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio"; const String kOptionEnableAudio = "enable-audio";
const String kOptionEnableCamera = "enable-camera"; const String kOptionEnableCamera = "enable-camera";
const String kOptionEnableTerminal = "enable-terminal";
const String kOptionTerminalPersistent = "terminal-persistent";
const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input"; const String kOptionEnableBlockInput = "enable-block-input";
@@ -110,6 +117,8 @@ const String kOptionAllowRemoteConfigModification =
"allow-remote-config-modification"; "allow-remote-config-modification";
const String kOptionVerificationMethod = "verification-method"; const String kOptionVerificationMethod = "verification-method";
const String kOptionApproveMode = "approve-mode"; const String kOptionApproveMode = "approve-mode";
const String kOptionAllowNumericOneTimePassword =
"allow-numeric-one-time-password";
const String kOptionCollapseToolbar = "collapse_toolbar"; const String kOptionCollapseToolbar = "collapse_toolbar";
const String kOptionShowRemoteCursor = "show_remote_cursor"; const String kOptionShowRemoteCursor = "show_remote_cursor";
const String kOptionFollowRemoteCursor = "follow_remote_cursor"; const String kOptionFollowRemoteCursor = "follow_remote_cursor";
@@ -145,12 +154,21 @@ const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service"; const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture"; const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification"; 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 kOptionEnableTrustedDevices = "enable-trusted-devices";
const String kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
// network options // network options
const String kOptionAllowWebSocket = "allow-websocket"; const String kOptionAllowWebSocket = "allow-websocket";
const String kOptionAllowInsecureTLSFallback = "allow-insecure-tls-fallback";
const String kOptionDisableUdp = "disable-udp";
const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
// buildin opitons // builtin options
const String kOptionHideServerSetting = "hide-server-settings"; const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings"; const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideWebSocketSetting = "hide-websocket-settings"; const String kOptionHideWebSocketSetting = "hide-websocket-settings";
@@ -163,6 +181,7 @@ const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards"; const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionToggleViewOnly = "view-only"; const String kOptionToggleViewOnly = "view-only";
const String kOptionToggleShowMyCursor = "show-my-cursor";
const String kOptionDisableFloatingWindow = "disable-floating-window"; const String kOptionDisableFloatingWindow = "disable-floating-window";
@@ -303,12 +322,18 @@ const kRemoteViewStyleOriginal = 'original';
/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor. /// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor.
const kRemoteViewStyleAdaptive = 'adaptive'; const kRemoteViewStyleAdaptive = 'adaptive';
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
const kRemoteViewStyleCustom = 'custom';
/// [kRemoteScrollStyleAuto] Scroll image auto by position. /// [kRemoteScrollStyleAuto] Scroll image auto by position.
const kRemoteScrollStyleAuto = 'scrollauto'; const kRemoteScrollStyleAuto = 'scrollauto';
/// [kRemoteScrollStyleBar] Scroll image with scroll bar. /// [kRemoteScrollStyleBar] Scroll image with scroll bar.
const kRemoteScrollStyleBar = 'scrollbar'; const kRemoteScrollStyleBar = 'scrollbar';
/// [kRemoteScrollStyleEdge] Scroll image auto at edges.
const kRemoteScrollStyleEdge = 'scrolledge';
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode. /// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
const kScrollModeDefault = 'default'; const kScrollModeDefault = 'default';
@@ -329,6 +354,23 @@ const kRemoteImageQualityCustom = 'custom';
const kIgnoreDpi = true; const kIgnoreDpi = true;
const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
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 ================================ // ================================ mobile ================================
// Magic numbers, maybe need to avoid it or use a better way to get them. // Magic numbers, maybe need to avoid it or use a better way to get them.

View File

@@ -327,10 +327,15 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Callback for the connect button. /// Callback for the connect button.
/// Connects to the selected peer. /// Connects to the selected peer.
void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) { void onConnect(
{bool isFileTransfer = false,
bool isViewCamera = false,
bool isTerminal = false}) {
var id = _idController.id; var id = _idController.id;
connect(context, id, connect(context, id,
isFileTransfer: isFileTransfer, isViewCamera: isViewCamera); isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTerminal: isTerminal);
} }
/// UI for the remote ID TextField. /// UI for the remote ID TextField.
@@ -369,6 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
rdpUsername: '', rdpUsername: '',
loginName: '', loginName: '',
device_group_name: '', device_group_name: '',
note: '',
); );
_autocompleteOpts = [emptyPeer]; _autocompleteOpts = [emptyPeer];
} else { } else {
@@ -527,64 +533,74 @@ class _ConnectionPageState extends State<ConnectionPage>
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Center( child: Center(
child: Obx(() { child: StatefulBuilder(
var offset = Offset(0, 0); builder: (context, setState) {
return InkWell( var offset = Offset(0, 0);
child: _menuOpen.value return Obx(() => InkWell(
? Transform.rotate( child: _menuOpen.value
angle: pi, ? Transform.rotate(
child: Icon(IconFont.more, size: 14), angle: pi,
) child: Icon(IconFont.more, size: 14),
: Icon(IconFont.more, size: 14), )
onTapDown: (e) { : Icon(IconFont.more, size: 14),
offset = e.globalPosition; onTapDown: (e) {
}, offset = e.globalPosition;
onTap: () async { },
_menuOpen.value = true; onTap: () async {
final x = offset.dx; _menuOpen.value = true;
final y = offset.dy; final x = offset.dx;
await mod_menu final y = offset.dy;
.showMenu( await mod_menu
context: context, .showMenu(
position: RelativeRect.fromLTRB(x, y, x, y), context: context,
items: [ position: RelativeRect.fromLTRB(x, y, x, y),
( items: [
'Transfer file', (
() => onConnect(isFileTransfer: true) 'Transfer file',
), () => onConnect(isFileTransfer: true)
( ),
'View camera', (
() => onConnect(isViewCamera: true) 'View camera',
), () => onConnect(isViewCamera: true)
] ),
.map((e) => MenuEntryButton<String>( (
childBuilder: (TextStyle? style) => Text( '${translate('Terminal')} (beta)',
translate(e.$1), () => onConnect(isTerminal: true)
style: style, ),
), ]
proc: () => e.$2(), .map((e) => MenuEntryButton<String>(
padding: EdgeInsets.symmetric( childBuilder: (TextStyle? style) =>
horizontal: kDesktopMenuPadding.left), Text(
dismissOnClicked: true, translate(e.$1),
)) style: style,
.map((e) => e.build( ),
context, proc: () => e.$2(),
const MenuConfig( padding: EdgeInsets.symmetric(
commonColor: horizontal:
CustomPopupMenuTheme.commonColor, kDesktopMenuPadding.left),
height: CustomPopupMenuTheme.height, dismissOnClicked: true,
dividerHeight: CustomPopupMenuTheme ))
.dividerHeight))) .map((e) => e.build(
.expand((i) => i) context,
.toList(), const MenuConfig(
elevation: 8, commonColor: CustomPopupMenuTheme
) .commonColor,
.then((_) { height:
_menuOpen.value = false; CustomPopupMenuTheme.height,
}); dividerHeight:
}, CustomPopupMenuTheme
); .dividerHeight)))
}), .expand((i) => i)
.toList(),
elevation: 8,
)
.then((_) {
_menuOpen.value = false;
});
},
));
},
),
), ),
), ),
]), ]),

View File

@@ -18,6 +18,7 @@ import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/ui_manager.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -434,7 +435,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
!isCardClosed && !isCardClosed &&
bind.mainUriPrefixSync().contains('rustdesk')) { bind.mainUriPrefixSync().contains('rustdesk')) {
final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled();
String btnText = isToUpdate ? 'Click to update' : 'Click to download'; String btnText = isToUpdate ? 'Update' : 'Download';
GestureTapCallback onPressed = () async { GestureTapCallback onPressed = () async {
final Uri url = Uri.parse('https://rustdesk.com/download'); final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url); await launchUrl(url);
@@ -760,9 +761,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
'scaleFactor': screen.scaleFactor, 'scaleFactor': screen.scaleFactor,
}; };
bool isChattyMethod(String methodName) {
switch (methodName) {
case kWindowBumpMouse: return true;
}
return false;
}
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint( if (!isChattyMethod(call.method)) {
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
}
if (call.method == kWindowMainWindowOnTop) { if (call.method == kWindowMainWindowOnTop) {
windowOnTop(null); windowOnTop(null);
} else if (call.method == kWindowGetWindowInfo) { } else if (call.method == kWindowGetWindowInfo) {
@@ -786,12 +797,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
call.arguments['id'], call.arguments['id'],
isFileTransfer: call.arguments['isFileTransfer'], isFileTransfer: call.arguments['isFileTransfer'],
isViewCamera: call.arguments['isViewCamera'], isViewCamera: call.arguments['isViewCamera'],
isTerminal: call.arguments['isTerminal'],
isTcpTunneling: call.arguments['isTcpTunneling'], isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'], isRDP: call.arguments['isRDP'],
password: call.arguments['password'], password: call.arguments['password'],
forceRelay: call.arguments['forceRelay'], forceRelay: call.arguments['forceRelay'],
connToken: call.arguments['connToken'], connToken: call.arguments['connToken'],
); );
} else if (call.method == kWindowBumpMouse) {
return RdPlatformChannel.instance.bumpMouse(
dx: call.arguments['dx'],
dy: call.arguments['dy']);
} else if (call.method == kWindowEventMoveTabToNewWindow) { } else if (call.method == kWindowEventMoveTabToNewWindow) {
final args = call.arguments.split(','); final args = call.arguments.split(',');
int? windowId; int? windowId;

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/printer_model.dart';
@@ -540,12 +541,32 @@ class _GeneralState extends State<_General> {
'Capture screen using DirectX', 'Capture screen using DirectX',
kOptionDirectxCapture, kOptionDirectxCapture,
), ),
if (!bind.isIncomingOnly()) ...[
_OptionCheckBox(
context,
'Enable UDP hole punching',
kOptionEnableUdpPunch,
isServer: false,
),
_OptionCheckBox(
context,
'Enable IPv6 P2P connection',
kOptionEnableIpv6Punch,
isServer: false,
),
],
], ],
]; ];
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
children.add(_OptionCheckBox( children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless)); context, 'Allow linux headless', kOptionAllowLinuxHeadless));
} }
children.add(_OptionCheckBox(
context,
'note-at-conn-end-tip',
kOptionAllowAskForNoteAtEndOfConnection,
isServer: false,
));
return _Card(title: 'Other', children: children); return _Card(title: 'Other', children: children);
} }
@@ -997,6 +1018,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable camera', kOptionEnableCamera, _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox( _OptionCheckBox(
context, 'Enable TCP tunneling', kOptionEnableTunnel, context, 'Enable TCP tunneling', kOptionEnableTunnel,
enabled: enabled, fakeValue: fakeValue), enabled: enabled, fakeValue: fakeValue),
@@ -1097,6 +1120,34 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
)) ))
.toList(); .toList();
final isOptFixedNumOTP =
isOptionFixed(kOptionAllowNumericOneTimePassword);
final isNumOPTChangable = !isOptFixedNumOTP && tmpEnabled && !locked;
final numericOneTimePassword = GestureDetector(
child: InkWell(
child: Row(
children: [
Checkbox(
value: model.allowNumericOneTimePassword,
onChanged: isNumOPTChangable
? (bool? v) {
model.switchAllowNumericOneTimePassword();
}
: null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Numeric one-time password'),
style: TextStyle(
color: disabledTextColor(context, isNumOPTChangable)),
))
],
)),
onTap: isNumOPTChangable
? () => model.switchAllowNumericOneTimePassword()
: null,
).marginOnly(left: _kContentHSubMargin - 5);
final modeKeys = <String>[ final modeKeys = <String>[
'password', 'password',
'click', 'click',
@@ -1133,6 +1184,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
], ],
), ),
enabled: tmpEnabled && !locked), enabled: tmpEnabled && !locked),
if (usePassword) numericOneTimePassword,
if (usePassword) radios[1], if (usePassword) radios[1],
if (usePassword) if (usePassword)
_SubButton('Set permanent password', setPasswordDialog, _SubButton('Set permanent password', setPasswordDialog,
@@ -1477,9 +1529,8 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
final hideProxy = final hideProxy =
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
// final hideWebSocket = isWeb || final hideWebSocket = isWeb ||
// bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y'; bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y';
final hideWebSocket = true;
if (hideServer && hideProxy && hideWebSocket) { if (hideServer && hideProxy && hideWebSocket) {
return Offstage(); return Offstage();
@@ -1541,6 +1592,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
); );
} }
Widget switchWidget(IconData icon, String title, String tooltipMessage,
String optionKey) =>
listTile(
icon: icon,
title: title,
showTooltip: true,
tooltipMessage: tooltipMessage,
trailing: Switch(
value: mainGetBoolOptionSync(optionKey),
onChanged: locked || isOptionFixed(optionKey)
? null
: (value) {
mainSetBoolOption(optionKey, value);
setState(() {});
},
),
);
final outgoingOnly = bind.isOutgoingOnly();
final divider = const Divider(height: 1, indent: 16, endIndent: 16);
return _Card( return _Card(
title: 'Network', title: 'Network',
children: [ children: [
@@ -1552,33 +1624,65 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
listTile( listTile(
icon: Icons.dns_outlined, icon: Icons.dns_outlined,
title: 'ID/Relay Server', title: 'ID/Relay Server',
onTap: () => showServerSettings(gFFI.dialogManager), onTap: () => showServerSettings(gFFI.dialogManager, setState),
), ),
if (!hideServer && (!hideProxy || !hideWebSocket)) if (!hideProxy && !hideServer) divider,
Divider(height: 1, indent: 16, endIndent: 16),
if (!hideProxy) if (!hideProxy)
listTile( listTile(
icon: Icons.network_ping_outlined, icon: Icons.network_ping_outlined,
title: 'Socks5/Http(s) Proxy', title: 'Socks5/Http(s) Proxy',
onTap: changeSocks5Proxy, onTap: changeSocks5Proxy,
), ),
if (!hideProxy && !hideWebSocket) if (!hideWebSocket && (!hideServer || !hideProxy)) divider,
Divider(height: 1, indent: 16, endIndent: 16),
if (!hideWebSocket) if (!hideWebSocket)
listTile( switchWidget(
icon: Icons.web_asset_outlined, Icons.web_asset_outlined,
title: 'Use WebSocket', 'Use WebSocket',
showTooltip: true, '${translate('websocket_tip')}\n\n${translate('server-oss-not-support-tip')}',
tooltipMessage: 'websocket_tip', kOptionAllowWebSocket),
trailing: Switch( if (!isWeb)
value: mainGetBoolOptionSync(kOptionAllowWebSocket), futureBuilder(
onChanged: locked future: bind.mainIsUsingPublicServer(),
? null hasData: (isUsingPublicServer) {
: (value) { if (isUsingPublicServer) {
mainSetBoolOption(kOptionAllowWebSocket, value); return Offstage();
setState(() {}); } else {
}, return Column(
), children: [
if (!hideServer || !hideProxy || !hideWebSocket)
divider,
switchWidget(
Icons.no_encryption_outlined,
'Allow insecure TLS fallback',
'allow-insecure-tls-fallback-tip',
kOptionAllowInsecureTLSFallback),
if (!outgoingOnly) divider,
if (!outgoingOnly)
listTile(
icon: Icons.lan_outlined,
title: 'Disable UDP',
showTooltip: true,
tooltipMessage:
'${translate('disable-udp-tip')}\n\n${translate('server-oss-not-support-tip')}',
trailing: Switch(
value: bind.mainGetOptionSync(
key: kOptionDisableUdp) ==
'Y',
onChanged:
locked || isOptionFixed(kOptionDisableUdp)
? null
: (value) async {
await bind.mainSetOption(
key: kOptionDisableUdp,
value: value ? 'Y' : 'N');
setState(() {});
},
),
),
],
);
}
},
), ),
], ],
), ),
@@ -1641,6 +1745,13 @@ class _DisplayState extends State<_Display> {
} }
final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle); final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
onEdgeScrollEdgeThicknessChanged(double value) async {
await bind.mainSetUserDefaultOption(
key: kOptionEdgeScrollEdgeThickness, value: value.round().toString());
setState(() {});
}
return _Card(title: 'Default Scroll Style', children: [ return _Card(title: 'Default Scroll Style', children: [
_Radio(context, _Radio(context,
value: kRemoteScrollStyleAuto, value: kRemoteScrollStyleAuto,
@@ -1652,6 +1763,23 @@ class _DisplayState extends State<_Display> {
groupValue: groupValue, groupValue: groupValue,
label: 'Scrollbar', label: 'Scrollbar',
onChanged: isOptFixed ? null : onChanged), onChanged: isOptFixed ? null : onChanged),
if (!isWeb) ...[
_Radio(context,
value: kRemoteScrollStyleEdge,
groupValue: groupValue,
label: 'ScrollEdge',
onChanged: isOptFixed ? null : onChanged),
Offstage(
offstage: groupValue != kRemoteScrollStyleEdge,
child: EdgeThicknessControl(
value: double.tryParse(bind.mainGetUserDefaultOption(
key: kOptionEdgeScrollEdgeThickness)) ??
100.0,
onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness)
? null
: onEdgeScrollEdgeThicknessChanged,
)),
],
]); ]);
} }
@@ -1693,9 +1821,9 @@ class _DisplayState extends State<_Display> {
} }
Widget trackpadSpeed(BuildContext context) { Widget trackpadSpeed(BuildContext context) {
final initSpeed = (int.tryParse( final initSpeed =
bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ?? (int.tryParse(bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
kDefaultTrackpadSpeed); kDefaultTrackpadSpeed);
final curSpeed = SimpleWrapper(initSpeed); final curSpeed = SimpleWrapper(initSpeed);
void onDebouncer(int v) { void onDebouncer(int v) {
bind.mainSetUserDefaultOption( bind.mainSetUserDefaultOption(

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:extended_text/extended_text.dart'; import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@@ -52,7 +53,7 @@ enum MouseFocusScope {
} }
class FileManagerPage extends StatefulWidget { class FileManagerPage extends StatefulWidget {
const FileManagerPage( FileManagerPage(
{Key? key, {Key? key,
required this.id, required this.id,
required this.password, required this.password,
@@ -67,9 +68,16 @@ class FileManagerPage extends StatefulWidget {
final bool? forceRelay; final bool? forceRelay;
final String? connToken; final String? connToken;
final DesktopTabController? tabController; final DesktopTabController? tabController;
final SimpleWrapper<State<FileManagerPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi;
@override @override
State<StatefulWidget> createState() => _FileManagerPageState(); State<StatefulWidget> createState() {
final state = _FileManagerPageState();
_lastState.value = state;
return state;
}
} }
class _FileManagerPageState extends State<FileManagerPage> class _FileManagerPageState extends State<FileManagerPage>
@@ -139,12 +147,26 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
} }
Widget willPopScope(Widget child) {
if (isWeb) {
return WillPopScope(
onWillPop: () async {
clientClose(_ffi.sessionId, _ffi);
return false;
},
child: child,
);
} else {
return child;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Overlay(key: _overlayKeyState.key, initialEntries: [ return Overlay(key: _overlayKeyState.key, initialEntries: [
OverlayEntry(builder: (_) { OverlayEntry(builder: (_) {
return Scaffold( return willPopScope(Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row( body: Row(
children: [ children: [
@@ -160,7 +182,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Flexible(flex: 2, child: statusList()) Flexible(flex: 2, child: statusList())
], ],
), ),
); ));
}) })
]); ]);
} }

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
@@ -40,7 +41,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: params['id'], label: params['id'],
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(params['id']), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: params['id'],
tabController: tabController,
)) {
return;
}
tabController.closeBy(params['id']);
},
page: FileManagerPage( page: FileManagerPage(
key: ValueKey(params['id']), key: ValueKey(params['id']),
id: params['id'], id: params['id'],
@@ -69,7 +78,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: FileManagerPage( page: FileManagerPage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
@@ -132,6 +149,14 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
Future<bool> handleWindowCloseButton() async { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length; final connLength = tabController.state.value.tabs.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) { if (connLength <= 1) {
tabController.clear(); tabController.clear();
return true; return true;

View File

@@ -25,7 +25,7 @@ class _PortForward {
} }
class PortForwardPage extends StatefulWidget { class PortForwardPage extends StatefulWidget {
const PortForwardPage({ PortForwardPage({
Key? key, Key? key,
required this.id, required this.id,
required this.password, required this.password,
@@ -42,9 +42,16 @@ class PortForwardPage extends StatefulWidget {
final bool? forceRelay; final bool? forceRelay;
final bool? isSharedPassword; final bool? isSharedPassword;
final String? connToken; final String? connToken;
final SimpleWrapper<State<PortForwardPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi;
@override @override
State<PortForwardPage> createState() => _PortForwardPageState(); State<PortForwardPage> createState() {
final state = _PortForwardPageState();
_lastState.value = state;
return state;
}
} }
class _PortForwardPageState extends State<PortForwardPage> class _PortForwardPageState extends State<PortForwardPage>

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
@@ -72,7 +73,10 @@ class RemotePage extends StatefulWidget {
} }
class _RemotePageState extends State<RemotePage> class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin, MultiWindowListener { with
AutomaticKeepAliveClientMixin,
MultiWindowListener,
TickerProviderStateMixin {
Timer? _timer; Timer? _timer;
String keyboardMode = "legacy"; String keyboardMode = "legacy";
bool _isWindowBlur = false; bool _isWindowBlur = false;
@@ -112,11 +116,13 @@ class _RemotePageState extends State<RemotePage>
_ffi = FFI(widget.sessionId); _ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id); Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) { _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
_ffi.canvasModel.activateLocalCursor();
showKBLayoutTypeChooserIfNeeded( showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel _ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
}); });
_ffi.canvasModel.initializeEdgeScrollFallback(this);
_ffi.start( _ffi.start(
widget.id, widget.id,
password: widget.password, password: widget.password,
@@ -395,7 +401,7 @@ class _RemotePageState extends State<RemotePage>
super.build(context); super.build(context);
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager); clientClose(sessionId, _ffi);
return false; return false;
}, },
child: MultiProvider(providers: [ child: MultiProvider(providers: [
@@ -408,6 +414,8 @@ class _RemotePageState extends State<RemotePage>
} }
void enterView(PointerEnterEvent evt) { void enterView(PointerEnterEvent evt) {
_ffi.canvasModel.rearmEdgeScroll();
_cursorOverImage.value = true; _cursorOverImage.value = true;
_firstEnterImage.value = true; _firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Toolbar != null) { if (_onEnterOrLeaveImage4Toolbar != null) {
@@ -427,6 +435,8 @@ class _RemotePageState extends State<RemotePage>
} }
void leaveView(PointerExitEvent evt) { void leaveView(PointerExitEvent evt) {
_ffi.canvasModel.disableEdgeScroll();
if (_ffi.ffiModel.keyboard) { if (_ffi.ffiModel.keyboard) {
_ffi.inputModel.tryMoveEdgeOnExit(evt.position); _ffi.inputModel.tryMoveEdgeOnExit(evt.position);
} }
@@ -625,7 +635,7 @@ class _ImagePaintState extends State<ImagePaint> {
onHover: (evt) {}, onHover: (evt) {},
child: child); child: child);
}); });
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
final paintWidth = c.getDisplayWidth() * s; final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s; final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight); final paintSize = Size(paintWidth, paintHeight);
@@ -680,9 +690,20 @@ class _ImagePaintState extends State<ImagePaint> {
Widget _buildScrollAutoNonTextureRender( Widget _buildScrollAutoNonTextureRender(
ImageModel m, CanvasModel c, double s) { ImageModel m, CanvasModel c, double s) {
double sizeScale = s;
if (widget.ffi.ffiModel.isPeerLinux) {
final displays = widget.ffi.ffiModel.pi.getCurDisplays();
if (displays.isNotEmpty) {
sizeScale = s / displays[0].scale;
}
}
return CustomPaint( return CustomPaint(
size: Size(c.size.width, c.size.height), size: Size(c.size.width, c.size.height),
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), painter: ImagePainter(
image: m.image,
x: c.x / sizeScale,
y: c.y / sizeScale,
scale: sizeScale),
); );
} }
@@ -695,17 +716,19 @@ class _ImagePaintState extends State<ImagePaint> {
if (rect == null) { if (rect == null) {
return Container(); return Container();
} }
final isPeerLinux = ffiModel.isPeerLinux;
final curDisplay = ffiModel.pi.currentDisplay; final curDisplay = ffiModel.pi.currentDisplay;
for (var i = 0; i < displays.length; i++) { for (var i = 0; i < displays.length; i++) {
final textureId = widget.ffi.textureModel final textureId = widget.ffi.textureModel
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay); .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
if (true) { if (true) {
// both "textureId.value != -1" and "true" seems ok // both "textureId.value != -1" and "true" seems ok
final sizeScale = isPeerLinux ? s / displays[i].scale : s;
children.add(Positioned( children.add(Positioned(
left: (displays[i].x - rect.left) * s + offset.dx, left: (displays[i].x - rect.left) * s + offset.dx,
top: (displays[i].y - rect.top) * s + offset.dy, top: (displays[i].y - rect.top) * s + offset.dy,
width: displays[i].width * s, width: displays[i].width * sizeScale,
height: displays[i].height * s, height: displays[i].height * sizeScale,
child: Obx(() => Texture( child: Obx(() => Texture(
textureId: textureId.value, textureId: textureId.value,
filterQuality: filterQuality:

View File

@@ -80,7 +80,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: peerId!, label: peerId!,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: peerId!,
tabController: tabController,
)) {
return;
}
tabController.closeBy(peerId!);
},
page: RemotePage( page: RemotePage(
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId!, id: peerId!,
@@ -146,16 +154,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
connectionType.secure.value == ConnectionType.strSecure; connectionType.secure.value == ConnectionType.strSecure;
bool direct = bool direct =
connectionType.direct.value == ConnectionType.strDirect; connectionType.direct.value == ConnectionType.strDirect;
String msgConn; String msgConn = getConnectionText(
if (secure && direct) { secure, direct, connectionType.stream_type.value);
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");
}
var msgFingerprint = '${translate('Fingerprint')}:\n'; var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value; var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) { if (fingerprint.isEmpty) {
@@ -324,7 +324,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
translate('Close'), translate('Close'),
style: style, style: style,
), ),
proc: () { proc: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: key,
tabController: tabController,
)) {
return;
}
tabController.closeBy(key); tabController.closeBy(key);
cancelFunc(); cancelFunc();
}, },
@@ -377,6 +383,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
Future<bool> handleWindowCloseButton() async { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length; final connLength = tabController.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) { if (connLength <= 1) {
tabController.clear(); tabController.clear();
return true; return true;
@@ -431,7 +445,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: RemotePage( page: RemotePage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,

View File

@@ -355,6 +355,7 @@ Widget buildConnectionCard(Client client) {
_CmHeader(client: client), _CmHeader(client: client),
client.type_() == ClientType.file || client.type_() == ClientType.file ||
client.type_() == ClientType.portForward || client.type_() == ClientType.portForward ||
client.type_() == ClientType.terminal ||
client.disconnected client.disconnected
? Offstage() ? Offstage()
: _PrivilegeBoard(client: client), : _PrivilegeBoard(client: client),
@@ -499,7 +500,36 @@ class _CmHeaderState extends State<_CmHeader>
"(${client.peerId})", "(${client.peerId})",
style: TextStyle(color: Colors.white, fontSize: 14), style: TextStyle(color: Colors.white, fontSize: 14),
), ),
).marginOnly(bottom: 10.0), ),
if (client.type_() == ClientType.terminal)
FittedBox(
child: Text(
translate("Terminal"),
style: TextStyle(color: Colors.white70, fontSize: 12),
),
),
if (client.type_() == ClientType.file)
FittedBox(
child: Text(
translate("File Transfer"),
style: TextStyle(color: Colors.white70, fontSize: 12),
),
),
if (client.type_() == ClientType.camera)
FittedBox(
child: Text(
translate("View Camera"),
style: TextStyle(color: Colors.white70, fontSize: 12),
),
),
if (client.portForward.isNotEmpty)
FittedBox(
child: Text(
"Port Forward: ${client.portForward}",
style: TextStyle(color: Colors.white70, fontSize: 12),
),
),
SizedBox(height: 10.0),
FittedBox( FittedBox(
child: Row( child: Row(
children: [ children: [

View File

@@ -0,0 +1,98 @@
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import '../../models/model.dart';
/// Manages terminal connections to ensure one FFI instance per peer
class TerminalConnectionManager {
static final Map<String, FFI> _connections = {};
static final Map<String, int> _connectionRefCount = {};
// Track service IDs per peer
static final Map<String, String> _serviceIds = {};
/// Get or create an FFI instance for a peer
static FFI getConnection({
required String peerId,
required String? password,
required bool? isSharedPassword,
required bool? forceRelay,
required String? connToken,
}) {
final existingFfi = _connections[peerId];
if (existingFfi != null && !existingFfi.closed) {
// Increment reference count
_connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1;
debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}');
return existingFfi;
}
// Create new FFI instance for first terminal
debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId');
final ffi = FFI(null);
ffi.start(
peerId,
password: password,
isSharedPassword: isSharedPassword,
forceRelay: forceRelay,
connToken: connToken,
isTerminal: true,
);
_connections[peerId] = ffi;
_connectionRefCount[peerId] = 1;
// Register the FFI instance with Get for dependency injection
Get.put<FFI>(ffi, tag: 'terminal_$peerId');
debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}');
return ffi;
}
/// Release a connection reference
static void releaseConnection(String peerId) {
final refCount = _connectionRefCount[peerId] ?? 0;
debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount');
if (refCount <= 1) {
// Last reference, close the connection
final ffi = _connections[peerId];
if (ffi != null) {
debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)');
ffi.close();
_connections.remove(peerId);
_connectionRefCount.remove(peerId);
Get.delete<FFI>(tag: 'terminal_$peerId');
}
} else {
// Decrement reference count
_connectionRefCount[peerId] = refCount - 1;
debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}');
}
}
/// Check if a connection exists for a peer
static bool hasConnection(String peerId) {
final ffi = _connections[peerId];
return ffi != null && !ffi.closed;
}
/// Get existing connection without creating new one
static FFI? getExistingConnection(String peerId) {
return _connections[peerId];
}
/// Get connection count for debugging
static int getConnectionCount() => _connections.length;
/// Get terminal count for a peer
static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0;
/// Get service ID for a peer
static String? getServiceId(String peerId) => _serviceIds[peerId];
/// Set service ID for a peer
static void setServiceId(String peerId, String serviceId) {
_serviceIds[peerId] = serviceId;
debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId');
}
}

View File

@@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:xterm/xterm.dart';
import 'terminal_connection_manager.dart';
class TerminalPage extends StatefulWidget {
TerminalPage({
Key? key,
required this.id,
required this.password,
required this.tabController,
required this.isSharedPassword,
required this.terminalId,
this.forceRelay,
this.connToken,
}) : super(key: key);
final String id;
final String? password;
final DesktopTabController tabController;
final bool? forceRelay;
final bool? isSharedPassword;
final String? connToken;
final int terminalId;
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
@override
State<TerminalPage> createState() {
final state = _TerminalPageState();
_lastState.value = state;
return state;
}
}
class _TerminalPageState extends State<TerminalPage>
with AutomaticKeepAliveClientMixin {
late FFI _ffi;
late TerminalModel _terminalModel;
double? _cellHeight;
@override
void initState() {
super.initState();
// Use shared FFI instance from connection manager
_ffi = TerminalConnectionManager.getConnection(
peerId: widget.id,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
connToken: widget.connToken,
);
// Create terminal model with specific terminal ID
_terminalModel = TerminalModel(_ffi, widget.terminalId);
debugPrint(
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
_terminalModel.onResizeExternal = (w, h, pw, ph) {
_cellHeight = ph * 1.0;
// Schedule the setState for the next frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
};
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
// Check if this is a new connection or additional terminal
// Note: When a connection exists, the ref count will be > 1 after this terminal is added
final isExistingConnection =
TerminalConnectionManager.hasConnection(widget.id) &&
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
if (!isExistingConnection) {
// First terminal - show loading dialog, wait for onReady
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
} else {
// Additional terminal - connection already established
// Open the terminal directly
_terminalModel.openTerminal();
}
});
}
@override
void dispose() {
// Unregister terminal model from FFI
_ffi.unregisterTerminalModel(widget.terminalId);
_terminalModel.dispose();
// Release connection reference instead of closing directly
TerminalConnectionManager.releaseConnection(widget.id);
super.dispose();
}
// This method ensures that the number of visible rows is an integer by computing the
// extra space left after dividing the available height by the height of a single
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
EdgeInsets _calculatePadding(double heightPx) {
if (_cellHeight == null) {
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
}
final rows = (heightPx / _cellHeight!).floor();
final extraSpace = heightPx - rows * _cellHeight!;
final topBottom = extraSpace / 2.0;
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: LayoutBuilder(
builder: (context, constraints) {
final heightPx = constraints.maxHeight;
return TerminalView(
_terminalModel.terminal,
controller: _terminalModel.terminalController,
autofocus: true,
backgroundOpacity: 0.7,
padding: _calculatePadding(heightPx),
onSecondaryTapDown: (details, offset) async {
final selection = _terminalModel.terminalController.selection;
if (selection != null) {
final text = _terminalModel.terminal.buffer.getText(selection);
_terminalModel.terminalController.clearSelection();
await Clipboard.setData(ClipboardData(text: text));
} else {
final data = await Clipboard.getData('text/plain');
final text = data?.text;
if (text != null) {
_terminalModel.terminal.paste(text);
}
}
},
);
},
),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -0,0 +1,445 @@
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:get/get.dart';
import '../../models/platform_model.dart';
import 'terminal_page.dart';
import 'terminal_connection_manager.dart';
import '../widgets/material_mod_popup_menu.dart' as mod_menu;
import '../widgets/popup_menu.dart';
import 'package:bot_toast/bot_toast.dart';
class TerminalTabPage extends StatefulWidget {
final Map<String, dynamic> params;
const TerminalTabPage({Key? key, required this.params}) : super(key: key);
@override
State<TerminalTabPage> createState() => _TerminalTabPageState(params);
}
class _TerminalTabPageState extends State<TerminalTabPage> {
DesktopTabController get tabController => Get.find<DesktopTabController>();
static const IconData selectedIcon = Icons.terminal;
static const IconData unselectedIcon = Icons.terminal_outlined;
int _nextTerminalId = 1;
_TerminalTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
tabController.onSelected = (id) {
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
};
tabController.onRemoved = (_, id) => onRemoveId(id);
final terminalId = params['terminalId'] ?? _nextTerminalId++;
tabController.add(_createTerminalTab(
peerId: params['id'],
terminalId: terminalId,
password: params['password'],
isSharedPassword: params['isSharedPassword'],
forceRelay: params['forceRelay'],
connToken: params['connToken'],
));
}
TabInfo _createTerminalTab({
required String peerId,
required int terminalId,
String? password,
bool? isSharedPassword,
bool? forceRelay,
String? connToken,
}) {
final tabKey = '${peerId}_$terminalId';
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
final tabLabel =
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
return TabInfo(
key: tabKey,
label: tabLabel,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabKey,
tabController: tabController,
)) {
return;
}
// Close the terminal session first
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
if (ffi != null) {
final terminalModel = ffi.terminalModels[terminalId];
if (terminalModel != null) {
await terminalModel.closeTerminal();
}
}
// Then close the tab
tabController.closeBy(tabKey);
},
page: TerminalPage(
key: ValueKey(tabKey),
id: peerId,
terminalId: terminalId,
password: password,
isSharedPassword: isSharedPassword,
tabController: tabController,
forceRelay: forceRelay,
connToken: connToken,
),
);
}
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
final List<MenuEntryBase<String>> menu = [];
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
// New tab menu item
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('New tab'),
style: style,
),
proc: () {
_addNewTerminal(peerId);
cancelFunc();
// Also try to close any BotToast overlays
BotToast.cleanAll();
},
padding: padding,
));
menu.add(MenuEntryDivider());
menu.add(MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate('Keep terminal sessions on disconnect'),
getter: () async {
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
return bind.sessionGetToggleOptionSync(
sessionId: ffi.sessionId,
arg: kOptionTerminalPersistent,
);
},
setter: (bool v) async {
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
await bind.sessionToggleOption(
sessionId: ffi.sessionId,
value: kOptionTerminalPersistent,
);
},
padding: padding,
));
return mod_menu.PopupMenu<String>(
items: menu
.map((e) => e.build(
context,
const MenuConfig(
commonColor: CustomPopupMenuTheme.commonColor,
height: CustomPopupMenuTheme.height,
dividerHeight: CustomPopupMenuTheme.dividerHeight,
),
))
.expand((i) => i)
.toList(),
);
}
@override
void initState() {
super.initState();
// Add keyboard shortcut handler
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == kWindowEventNewTerminal) {
final args = jsonDecode(call.arguments);
final id = args['id'];
windowOnTop(windowId());
// Allow multiple terminals for the same connection
final terminalId = args['terminalId'] ?? _nextTerminalId++;
tabController.add(_createTerminalTab(
peerId: id,
terminalId: terminalId,
password: args['password'],
isSharedPassword: args['isSharedPassword'],
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, () {
restoreWindowPosition(WindowType.Terminal, windowId: windowId());
});
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
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
if (event.logicalKey == LogicalKeyboardKey.keyT) {
if (isMacOS &&
HardwareKeyboard.instance.isMetaPressed &&
!HardwareKeyboard.instance.isShiftPressed) {
// macOS: Cmd+T (standard for new tab)
_addNewTerminalForCurrentPeer();
return true;
} else if (!isMacOS &&
HardwareKeyboard.instance.isControlPressed &&
HardwareKeyboard.instance.isShiftPressed) {
// Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal)
_addNewTerminalForCurrentPeer();
return true;
}
}
// Use Cmd+W on macOS, Ctrl+Shift+W on other platforms
if (event.logicalKey == LogicalKeyboardKey.keyW) {
if (isMacOS &&
HardwareKeyboard.instance.isMetaPressed &&
!HardwareKeyboard.instance.isShiftPressed) {
// macOS: Cmd+W (standard for close tab)
final currentTab = tabController.state.value.selectedTabInfo;
if (tabController.state.value.tabs.length > 1) {
tabController.closeBy(currentTab.key);
return true;
}
} else if (!isMacOS &&
HardwareKeyboard.instance.isControlPressed &&
HardwareKeyboard.instance.isShiftPressed) {
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
final currentTab = tabController.state.value.selectedTabInfo;
if (tabController.state.value.tabs.length > 1) {
tabController.closeBy(currentTab.key);
return true;
}
}
}
// Use Alt+Left/Right for tab navigation (avoids conflicts)
if (HardwareKeyboard.instance.isAltPressed) {
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
// Previous tab
final currentIndex = tabController.state.value.selected;
if (currentIndex > 0) {
tabController.jumpTo(currentIndex - 1);
}
return true;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
// Next tab
final currentIndex = tabController.state.value.selected;
if (currentIndex < tabController.length - 1) {
tabController.jumpTo(currentIndex + 1);
}
return true;
}
}
// Check for Cmd/Ctrl + Number (switch to specific tab)
final numberKeys = [
LogicalKeyboardKey.digit1,
LogicalKeyboardKey.digit2,
LogicalKeyboardKey.digit3,
LogicalKeyboardKey.digit4,
LogicalKeyboardKey.digit5,
LogicalKeyboardKey.digit6,
LogicalKeyboardKey.digit7,
LogicalKeyboardKey.digit8,
LogicalKeyboardKey.digit9,
];
for (int i = 0; i < numberKeys.length; i++) {
if (event.logicalKey == numberKeys[i] &&
((isMacOS && HardwareKeyboard.instance.isMetaPressed) ||
(!isMacOS && HardwareKeyboard.instance.isControlPressed))) {
if (i < tabController.length) {
tabController.jumpTo(i);
return true;
}
}
}
}
return false;
}
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 newTerminalId = terminalId ?? _nextTerminalId++;
if (terminalId != null && terminalId >= _nextTerminalId) {
_nextTerminalId = terminalId + 1;
}
tabController.add(_createTerminalTab(
peerId: peerId,
terminalId: newTerminalId,
password: page.password,
isSharedPassword: page.isSharedPassword,
forceRelay: page.forceRelay,
connToken: page.connToken,
));
}
}
void _addNewTerminalForCurrentPeer({int? terminalId}) {
final currentTab = tabController.state.value.selectedTabInfo;
final parts = currentTab.key.split('_');
if (parts.isNotEmpty) {
final peerId = parts[0];
_addNewTerminal(peerId, terminalId: terminalId);
}
}
@override
Widget build(BuildContext context) {
final child = Scaffold(
backgroundColor: Theme.of(context).cardColor,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: _buildAddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter,
tabMenuBuilder: (key) {
// Extract peerId from tab key (format: "peerId_terminalId")
final parts = key.split('_');
if (parts.isEmpty) return Container();
final peerId = parts[0];
return _tabMenuBuilder(peerId, () {});
},
));
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: workaroundWindowBorder(
context,
Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: SubWindowDragToResizeArea(
child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId,
);
}
void onRemoveId(String id) {
if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).close();
}
}
int windowId() {
return widget.params["windowId"];
}
Widget _buildAddButton() {
return ActionIcon(
message: 'New tab',
icon: IconFont.add,
onTap: () {
_addNewTerminalForCurrentPeer();
},
isClose: false,
);
}
Future<bool> handleWindowCloseButton() async {
final connLength = tabController.state.value.tabs.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) {
tabController.clear();
return true;
} else {
final bool res;
if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true;
} else {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
}
return res;
}
}
}

View File

@@ -360,7 +360,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
super.build(context); super.build(context);
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
clientClose(sessionId, _ffi.dialogManager); clientClose(sessionId, _ffi);
return false; return false;
}, },
child: MultiProvider(providers: [ child: MultiProvider(providers: [
@@ -515,8 +515,6 @@ class ImagePaint extends StatefulWidget {
} }
class _ImagePaintState extends State<ImagePaint> { class _ImagePaintState extends State<ImagePaint> {
bool _lastRemoteCursorMoved = false;
String get id => widget.id; String get id => widget.id;
RxBool get cursorOverImage => widget.cursorOverImage; RxBool get cursorOverImage => widget.cursorOverImage;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@@ -529,7 +527,7 @@ class _ImagePaintState extends State<ImagePaint> {
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
final paintWidth = c.getDisplayWidth() * s; final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s; final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight); final paintSize = Size(paintWidth, paintHeight);

View File

@@ -6,6 +6,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@@ -79,7 +80,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
label: peerId!, label: peerId!,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: peerId!,
tabController: tabController,
)) {
return;
}
tabController.closeBy(peerId!);
},
page: ViewCameraPage( page: ViewCameraPage(
key: ValueKey(peerId), key: ValueKey(peerId),
id: peerId!, id: peerId!,
@@ -145,16 +154,8 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
connectionType.secure.value == ConnectionType.strSecure; connectionType.secure.value == ConnectionType.strSecure;
bool direct = bool direct =
connectionType.direct.value == ConnectionType.strDirect; connectionType.direct.value == ConnectionType.strDirect;
String msgConn; String msgConn = getConnectionText(
if (secure && direct) { secure, direct, connectionType.stream_type.value);
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");
}
var msgFingerprint = '${translate('Fingerprint')}:\n'; var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value; var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) { if (fingerprint.isEmpty) {
@@ -295,7 +296,13 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
translate('Close'), translate('Close'),
style: style, style: style,
), ),
proc: () { proc: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: key,
tabController: tabController,
)) {
return;
}
tabController.closeBy(key); tabController.closeBy(key);
cancelFunc(); cancelFunc();
}, },
@@ -348,6 +355,14 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
Future<bool> handleWindowCloseButton() async { Future<bool> handleWindowCloseButton() async {
final connLength = tabController.length; final connLength = tabController.length;
if (connLength == 1) {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabController.state.value.tabs[0].key,
tabController: tabController,
)) {
return false;
}
}
if (connLength <= 1) { if (connLength <= 1) {
tabController.clear(); tabController.clear();
return true; return true;
@@ -401,7 +416,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon, unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id), onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: id,
tabController: tabController,
)) {
return;
}
tabController.closeBy(id);
},
page: ViewCameraPage( page: ViewCameraPage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:provider/provider.dart';
import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart';
class DesktopTerminalScreen extends StatelessWidget {
final Map<String, dynamic> params;
const DesktopTerminalScreen({Key? key, required this.params})
: super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),
],
child: Scaffold(
backgroundColor: isLinux ? Colors.transparent : null,
body: TerminalTabPage(
params: params,
),
),
);
}
}

View File

@@ -25,6 +25,8 @@ import '../../models/platform_model.dart';
import '../../common/shared_state.dart'; import '../../common/shared_state.dart';
import './popup_menu.dart'; import './popup_menu.dart';
import './kb_layout_type_chooser.dart'; import './kb_layout_type_chooser.dart';
import 'package:flutter_hbb/utils/scale.dart';
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
class ToolbarState { class ToolbarState {
late RxBool _pin; late RxBool _pin;
@@ -152,129 +154,6 @@ class _ToolbarTheme {
typedef DismissFunc = void Function(); typedef DismissFunc = void Function();
class RemoteMenuEntry { class RemoteMenuEntry {
static MenuEntryRadios<String> viewStyle(
String remoteId,
FFI ffi,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
RxString? rxViewStyle,
}) {
return MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
dismissCallback: dismissCallback,
),
],
curOptionGetter: () async {
// null means peer id is not found, which there's no need to care about
final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (rxViewStyle != null) {
rxViewStyle.value = viewStyle;
}
return viewStyle;
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(
sessionId: ffi.sessionId, value: newValue);
if (rxViewStyle != null) {
rxViewStyle.value = newValue;
}
ffi.canvasModel.updateViewStyle();
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch2<String> showRemoteCursor(
String remoteId,
SessionID sessionId,
EdgeInsets padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
final state = ShowRemoteCursorState.find(remoteId);
final optKey = 'show-remote-cursor';
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
await bind.sessionToggleOption(sessionId: sessionId, value: optKey);
state.value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: optKey);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> disableClipboard(
SessionID sessionId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return createSwitchMenuEntry(
sessionId,
'Disable clipboard',
'disable-clipboard',
padding,
true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> createSwitchMenuEntry(
SessionID sessionId,
String text,
String option,
EdgeInsets? padding,
bool dismissOnClicked, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate(text),
getter: () async {
return bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(sessionId: sessionId, value: option);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: dismissOnClicked,
dismissCallback: dismissCallback,
);
}
static MenuEntryButton<String> insertLock( static MenuEntryButton<String> insertLock(
SessionID sessionId, SessionID sessionId,
EdgeInsets? padding, { EdgeInsets? padding, {
@@ -632,7 +511,7 @@ class _MonitorMenu extends StatelessWidget {
menuStyle: MenuStyle( menuStyle: MenuStyle(
padding: padding:
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]); menuChildrenGetter: (_) => [buildMonitorSubmenuWidget(context)]);
} }
Widget buildMultiMonitorMenu(BuildContext context) { Widget buildMultiMonitorMenu(BuildContext context) {
@@ -843,7 +722,7 @@ class _ControlMenu extends StatelessWidget {
color: _ToolbarTheme.blueColor, color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor, hoverColor: _ToolbarTheme.hoverBlueColor,
ffi: ffi, ffi: ffi,
menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) { menuChildrenGetter: (_) => toolbarControls(context, id, ffi).map((e) {
if (e.divider) { if (e.divider) {
return Divider(); return Divider();
} else { } else {
@@ -1024,6 +903,7 @@ class _DisplayMenu extends StatefulWidget {
} }
class _DisplayMenuState extends State<_DisplayMenu> { class _DisplayMenuState extends State<_DisplayMenu> {
final RxInt _customPercent = 100.obs;
late final ScreenAdjustor _screenAdjustor = ScreenAdjustor( late final ScreenAdjustor _screenAdjustor = ScreenAdjustor(
id: widget.id, id: widget.id,
ffi: widget.ffi, ffi: widget.ffi,
@@ -1037,14 +917,29 @@ class _DisplayMenuState extends State<_DisplayMenu> {
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
String get id => widget.id; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
_screenAdjustor.updateScreen(); _screenAdjustor.updateScreen();
menuChildrenGetter() { menuChildrenGetter(_IconSubmenuButtonState state) {
final menuChildren = <Widget>[ final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context), _screenAdjustor.adjustWindow(context),
viewStyle(), viewStyle(customPercent: _customPercent),
scrollStyle(), scrollStyle(state, colorScheme),
imageQuality(), imageQuality(),
codec(), codec(),
if (ffi.connType == ConnType.defaultConn) if (ffi.connType == ConnType.defaultConn)
@@ -1108,62 +1003,146 @@ class _DisplayMenuState extends State<_DisplayMenu> {
); );
} }
viewStyle() { viewStyle({required RxInt customPercent}) {
return futureBuilder( return futureBuilder(
future: toolbarViewStyle(context, widget.id, widget.ffi), future: toolbarViewStyle(context, widget.id, widget.ffi),
hasData: (data) { hasData: (data) {
final v = data as List<TRadioMenu<String>>; final v = data as List<TRadioMenu<String>>;
final bool isCustomSelected = v.isNotEmpty
? v.first.groupValue == kRemoteViewStyleCustom
: false;
return Column(children: [ return Column(children: [
...v ...v.map((e) {
.map((e) => RdoMenuButton<String>( final isCustom = e.value == kRemoteViewStyleCustom;
value: e.value, final child =
groupValue: e.groupValue, isCustom ? Text(translate('Scale custom')) : e.child;
onChanged: e.onChanged, // Whether the current selection is already custom
child: e.child, final bool isGroupCustomSelected =
ffi: ffi)) e.groupValue == kRemoteViewStyleCustom;
.toList(), // Keep menu open when switching INTO custom so the slider is visible immediately
Divider(), 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),
]); ]);
}); });
} }
scrollStyle() { 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(_IconSubmenuButtonState state, ColorScheme colorScheme) {
return futureBuilder(future: () async { return futureBuilder(future: () async {
final viewStyle = final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? ''; await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
final visible = viewStyle == kRemoteViewStyleOriginal; final visible = viewStyle == kRemoteViewStyleOriginal ||
viewStyle == kRemoteViewStyleCustom;
final scrollStyle = final scrollStyle =
await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? ''; await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? '';
return {'visible': visible, 'scrollStyle': scrollStyle}; final edgeScrollEdgeThickness = await bind
.sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId);
return {
'visible': visible,
'scrollStyle': scrollStyle,
'edgeScrollEdgeThickness': edgeScrollEdgeThickness,
};
}(), hasData: (data) { }(), hasData: (data) {
final visible = data['visible'] as bool; final visible = data['visible'] as bool;
if (!visible) return Offstage(); if (!visible) return Offstage();
final groupValue = data['scrollStyle'] as String; final groupValue = data['scrollStyle'] as String;
onChange(String? value) async { final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int;
onChangeScrollStyle(String? value) async {
if (value == null) return; if (value == null) return;
await bind.sessionSetScrollStyle( await bind.sessionSetScrollStyle(
sessionId: ffi.sessionId, value: value); sessionId: ffi.sessionId, value: value);
widget.ffi.canvasModel.updateScrollStyle(); widget.ffi.canvasModel.updateScrollStyle();
state.setState(() {});
} }
final enabled = widget.ffi.canvasModel.imageOverflow.value; onChangeEdgeScrollEdgeThickness(double? value) async {
return Column(children: [ if (value == null) return;
RdoMenuButton<String>( final newThickness = value.round();
child: Text(translate('ScrollAuto')), await bind.sessionSetEdgeScrollEdgeThickness(
value: kRemoteScrollStyleAuto, sessionId: ffi.sessionId, value: newThickness);
groupValue: groupValue, widget.ffi.canvasModel.updateEdgeScrollEdgeThickness(newThickness);
onChanged: enabled ? (value) => onChange(value) : null, state.setState(() {});
ffi: widget.ffi, }
),
RdoMenuButton<String>( return Obx(() => Column(children: [
child: Text(translate('Scrollbar')), RdoMenuButton<String>(
value: kRemoteScrollStyleBar, child: Text(translate('ScrollAuto')),
groupValue: groupValue, value: kRemoteScrollStyleAuto,
onChanged: enabled ? (value) => onChange(value) : null, groupValue: groupValue,
ffi: widget.ffi, onChanged: widget.ffi.canvasModel.imageOverflow.value
), ? (value) => onChangeScrollStyle(value)
Divider(), : null,
]); closeOnActivate: groupValue != kRemoteScrollStyleEdge,
ffi: widget.ffi,
),
RdoMenuButton<String>(
child: Text(translate('Scrollbar')),
value: kRemoteScrollStyleBar,
groupValue: groupValue,
onChanged: widget.ffi.canvasModel.imageOverflow.value
? (value) => onChangeScrollStyle(value)
: null,
closeOnActivate: groupValue != kRemoteScrollStyleEdge,
ffi: widget.ffi,
),
if (!isWeb) ...[
RdoMenuButton<String>(
child: Text(translate('ScrollEdge')),
value: kRemoteScrollStyleEdge,
groupValue: groupValue,
closeOnActivate: false,
onChanged: widget.ffi.canvasModel.imageOverflow.value
? (value) => onChangeScrollStyle(value)
: null,
ffi: widget.ffi,
),
Offstage(
offstage: groupValue != kRemoteScrollStyleEdge,
child: EdgeThicknessControl(
value: edgeScrollEdgeThickness.toDouble(),
onChanged: onChangeEdgeScrollEdgeThickness,
colorScheme: colorScheme,
)),
],
Divider(),
]));
}); });
} }
@@ -1245,6 +1224,178 @@ 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 CustomScaleControls<_CustomScaleMenuControls> {
@override
FFI get ffi => widget.ffi;
@override
ValueChanged<int>? get onScaleChanged => widget.onChanged;
@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: '$scaleValue%',
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: CustomScaleControls.minPercent.toDouble(),
max: CustomScaleControls.maxPercent.toDouble(),
width: 52,
height: 24,
radius: 4,
displayValueForNormalized: (t) => mapPosToPercent(t),
),
),
child: Slider(
value: scalePos,
min: 0.0,
max: 1.0,
// Use a wide range of divisions (calculated as (CustomScaleControls.maxPercent - CustomScaleControls.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:
(CustomScaleControls.maxPercent - CustomScaleControls.minPercent)
.round(),
onChanged: onSliderChanged,
),
),
);
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: () => nudgeScale(-1),
),
),
Expanded(child: sliderControl),
Tooltip(
message: translate('Increase'),
child: IconButton(
iconSize: 16,
padding: EdgeInsets.all(1),
constraints: smallBtnConstraints,
icon: const Icon(Icons.add),
onPressed: () => nudgeScale(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;
final String unit;
// 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,
this.unit = '%',
});
@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 value from normalized slider value.
final int displayValue = displayValueForNormalized != null
? displayValueForNormalized!(value)
: (min + value * (max - min)).round();
final TextSpan span = TextSpan(
text: '$displayValue$unit',
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 { class _ResolutionsMenu extends StatefulWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
@@ -1587,12 +1738,15 @@ class _KeyboardMenu extends StatelessWidget {
ffi: ffi, ffi: ffi,
color: _ToolbarTheme.blueColor, color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor, hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildrenGetter: () => [ menuChildrenGetter: (_) => [
keyboardMode(), keyboardMode(),
localKeyboardType(), localKeyboardType(),
inputSource(), inputSource(),
Divider(), Divider(),
viewMode(), viewMode(),
if ([kPeerPlatformWindows, kPeerPlatformMacOS, kPeerPlatformLinux]
.contains(pi.platform))
showMyCursor(),
Divider(), Divider(),
...toolbarToggles(), ...toolbarToggles(),
...mouseSpeed(), ...mouseSpeed(),
@@ -1749,12 +1903,43 @@ class _KeyboardMenu extends StatelessWidget {
final viewOnly = await bind.sessionGetToggleOption( final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly); sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value); ffiModel.setViewOnly(id, viewOnly ?? value);
final showMyCursor = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleShowMyCursor);
ffiModel.setShowMyCursor(showMyCursor ?? value);
} }
: null, : null,
ffi: ffi, ffi: ffi,
child: Text(translate('View Mode'))); 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() { mobileActions() {
if (pi.platform != kPeerPlatformAndroid) return []; if (pi.platform != kPeerPlatformAndroid) return [];
final enabled = versionCmp(pi.version, '1.2.7') >= 0; final enabled = versionCmp(pi.version, '1.2.7') >= 0;
@@ -1818,7 +2003,7 @@ class _ChatMenuState extends State<_ChatMenu> {
ffi: widget.ffi, ffi: widget.ffi,
color: _ToolbarTheme.blueColor, color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor, hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildrenGetter: () => [textChat(), voiceCall()]); menuChildrenGetter: (_) => [textChat(), voiceCall()]);
} }
} }
@@ -1874,7 +2059,7 @@ class _VoiceCallMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
menuChildrenGetter() { menuChildrenGetter(_IconSubmenuButtonState state) {
final audioInput = AudioInput( final audioInput = AudioInput(
builder: (devices, currentDevice, setDevice) { builder: (devices, currentDevice, setDevice) {
return Column( return Column(
@@ -1980,7 +2165,12 @@ class _CloseMenu extends StatelessWidget {
return _IconMenuButton( return _IconMenuButton(
assetName: 'assets/close.svg', assetName: 'assets/close.svg',
tooltip: 'Close', tooltip: 'Close',
onPressed: () => closeConnection(id: id), onPressed: () async {
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
return;
}
closeConnection(id: id);
},
color: _ToolbarTheme.redColor, color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor, hoverColor: _ToolbarTheme.hoverRedColor,
); );
@@ -2074,7 +2264,7 @@ class _IconSubmenuButton extends StatefulWidget {
final Widget? icon; final Widget? icon;
final Color color; final Color color;
final Color hoverColor; final Color hoverColor;
final List<Widget> Function() menuChildrenGetter; final List<Widget> Function(_IconSubmenuButtonState state) menuChildrenGetter;
final MenuStyle? menuStyle; final MenuStyle? menuStyle;
final FFI? ffi; final FFI? ffi;
final double? width; final double? width;
@@ -2099,6 +2289,11 @@ class _IconSubmenuButton extends StatefulWidget {
class _IconSubmenuButtonState extends State<_IconSubmenuButton> { class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
bool hover = false; bool hover = false;
@override // discard @protected
void setState(VoidCallback fn) {
super.setState(fn);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(widget.svg != null || widget.icon != null); assert(widget.svg != null || widget.icon != null);
@@ -2131,7 +2326,7 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
), ),
child: icon))), child: icon))),
menuChildren: widget menuChildren: widget
.menuChildrenGetter() .menuChildrenGetter(this)
.map((e) => _buildPointerTrackWidget(e, widget.ffi)) .map((e) => _buildPointerTrackWidget(e, widget.ffi))
.toList())); .toList()));
return MenuBar(children: [ return MenuBar(children: [
@@ -2232,6 +2427,8 @@ class RdoMenuButton<T> extends StatelessWidget {
final ValueChanged<T?>? onChanged; final ValueChanged<T?>? onChanged;
final Widget? child; final Widget? child;
final FFI? ffi; final FFI? ffi;
// When true, submenu will be dismissed on activate; when false, it stays open.
final bool closeOnActivate;
const RdoMenuButton({ const RdoMenuButton({
Key? key, Key? key,
required this.value, required this.value,
@@ -2239,6 +2436,7 @@ class RdoMenuButton<T> extends StatelessWidget {
required this.child, required this.child,
this.ffi, this.ffi,
this.onChanged, this.onChanged,
this.closeOnActivate = true,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -2247,9 +2445,10 @@ class RdoMenuButton<T> extends StatelessWidget {
value: value, value: value,
groupValue: groupValue, groupValue: groupValue,
child: child, child: child,
closeOnActivate: closeOnActivate,
onChanged: onChanged != null onChanged: onChanged != null
? (T? value) { ? (T? value) {
if (ffi != null) { if (ffi != null && closeOnActivate) {
_menuDismissCallback(ffi!); _menuDismissCallback(ffi!);
} }
onChanged?.call(value); onChanged?.call(value);
@@ -2490,3 +2689,56 @@ Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
), ),
); );
} }
class EdgeThicknessControl extends StatelessWidget {
final double value;
final ValueChanged<double>? onChanged;
final ColorScheme? colorScheme;
const EdgeThicknessControl({
Key? key,
required this.value,
this.onChanged,
this.colorScheme,
}) : super(key: key);
static const double kMin = 20;
static const double kMax = 150;
@override
Widget build(BuildContext context) {
final colorScheme = this.colorScheme ?? Theme.of(context).colorScheme;
final slider = SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: colorScheme.primary,
thumbColor: colorScheme.primary,
overlayColor: colorScheme.primary.withOpacity(0.1),
showValueIndicator: ShowValueIndicator.never,
thumbShape: _RectValueThumbShape(
min: EdgeThicknessControl.kMin,
max: EdgeThicknessControl.kMax,
width: 52,
height: 24,
radius: 4,
unit: 'px',
),
),
child: Semantics(
value: value.toInt().toString(),
child: Slider(
value: value,
min: EdgeThicknessControl.kMin,
max: EdgeThicknessControl.kMax,
divisions:
(EdgeThicknessControl.kMax - EdgeThicknessControl.kMin).round(),
semanticFormatterCallback: (double newValue) =>
"${newValue.round()}px",
onChanged: onChanged,
),
),
);
return slider;
}
}

View File

@@ -54,6 +54,7 @@ enum DesktopTabType {
fileTransfer, fileTransfer,
viewCamera, viewCamera,
portForward, portForward,
terminal,
install, install,
} }
@@ -291,7 +292,6 @@ class DesktopTab extends StatefulWidget {
// ignore: must_be_immutable // ignore: must_be_immutable
class _DesktopTabState extends State<DesktopTab> class _DesktopTabState extends State<DesktopTab>
with MultiWindowListener, WindowListener { with MultiWindowListener, WindowListener {
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
Timer? _macOSCheckRestoreTimer; Timer? _macOSCheckRestoreTimer;
int _macOSCheckRestoreCounter = 0; int _macOSCheckRestoreCounter = 0;
@@ -369,7 +369,7 @@ class _DesktopTabState extends State<DesktopTab>
void _setMaximized(bool maximize) { void _setMaximized(bool maximize) {
stateGlobal.setMaximized(maximize); stateGlobal.setMaximized(maximize);
_saveFrameDebounce.call(_saveFrame); _saveFrame();
setState(() {}); setState(() {});
} }
@@ -404,24 +404,29 @@ class _DesktopTabState extends State<DesktopTab>
super.onWindowUnmaximize(); super.onWindowUnmaximize();
} }
_saveFrame() async { _saveFrame({bool? flush}) async {
if (tabType == DesktopTabType.main) { try {
await saveWindowPosition(WindowType.Main); if (tabType == DesktopTabType.main) {
} else if (kWindowType != null && kWindowId != null) { await saveWindowPosition(WindowType.Main, flush: flush);
await saveWindowPosition(kWindowType!, windowId: kWindowId); } else if (kWindowType != null && kWindowId != null) {
await saveWindowPosition(kWindowType!,
windowId: kWindowId, flush: flush);
}
} catch (e) {
debugPrint('Error saving window position: $e');
} }
} }
@override @override
void onWindowMoved() { void onWindowMoved() {
_saveFrameDebounce.call(_saveFrame); _saveFrame();
super.onWindowMoved(); super.onWindowMoved();
} }
@override @override
void onWindowResized() { void onWindowResized() {
_saveFrameDebounce.call(_saveFrame); _saveFrame();
super.onWindowMoved(); super.onWindowResized();
} }
@override @override
@@ -459,6 +464,8 @@ class _DesktopTabState extends State<DesktopTab>
}); });
} }
await _saveFrame(flush: true);
// hide window on close // hide window on close
if (isMainWindow) { if (isMainWindow) {
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
@@ -1078,11 +1085,12 @@ class _TabState extends State<_Tab> with RestorationMixin {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Tooltip( child: Tooltip(
message: widget.tabType == DesktopTabType.main message:
? '' widget.tabType == DesktopTabType.main ? '' : widget.label.value,
: translate(widget.label.value),
child: Text( child: Text(
translate(widget.label.value), widget.tabType == DesktopTabType.main
? translate(widget.label.value)
: widget.label.value,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: isSelected color: isSelected

View File

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

View File

@@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_terminal_screen.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
@@ -91,6 +92,12 @@ Future<void> main(List<String> args) async {
kAppTypeDesktopPortForward, kAppTypeDesktopPortForward,
); );
break; break;
case WindowType.Terminal:
desktopType = DesktopType.terminal;
runMultiWindow(
argument,
kAppTypeDesktopTerminal,
);
default: default:
break; break;
} }
@@ -140,9 +147,15 @@ void runMainApp(bool startService) async {
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
bool? alwaysOnTop;
if (isDesktop) {
alwaysOnTop =
bind.mainGetBuildinOption(key: "main-window-always-on-top") == 'Y';
}
// Set window option. // Set window option.
WindowOptions windowOptions = WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
getHiddenTitleBarWindowOptions(isMainWindow: true); isMainWindow: true, alwaysOnTop: alwaysOnTop);
windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.waitUntilReadyToShow(windowOptions, () async {
// Restore the location of the main window before window hide or show. // Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main); await restoreWindowPosition(WindowType.Main);
@@ -211,6 +224,11 @@ void runMultiWindow(
params: argument, params: argument,
); );
break; break;
case kAppTypeDesktopTerminal:
widget = DesktopTerminalScreen(
params: argument,
);
break;
default: default:
// no such appType // no such appType
exit(0); exit(0);
@@ -257,6 +275,9 @@ void runMultiWindow(
case kAppTypeDesktopPortForward: case kAppTypeDesktopPortForward:
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
break; break;
case kAppTypeDesktopTerminal:
await restoreWindowPosition(WindowType.Terminal, windowId: kWindowId!);
break;
default: default:
// no such appType // no such appType
exit(0); exit(0);

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