Merge branch 'master' of https://github.com/rustdesk/rustdesk into opt_chat_overlay_and_fix_pageview_2

This commit is contained in:
csf
2023-02-08 22:29:51 +09:00
161 changed files with 3439 additions and 1118 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

1
flutter/assets/chat.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675159173189" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1697" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512.7 797H292.9c-24 0-47.3 4.7-70 12.5-57.3 19.5-108 50.7-155.5 87.4-13 10-28.3 10.9-40.2 1.7-8.2-6.3-12.6-14.7-12.6-25.1 0-133.2-0.2-266.5 0.1-399.7 0.1-36.9 6.7-73.1 17.3-108.6 10.8-36.1 26.1-70.1 47.4-101.2 32.7-47.8 76.2-81.7 131.7-99.5 18-5.8 36.6-9 55.4-10.5 12.2-1 24.4-1.1 36.6-1.1 26.5 0 52.9-0.3 79.4 0.1 72.8 0.9 145.6 0.6 218.5 0.3 41.6-0.2 83.2-0.3 124.9-0.3 28.3 0 56.1 3.9 83 13.1 34.5 11.7 65.3 29.6 92.2 54.3 16 14.8 30.2 31.1 42.6 49 32.4 46.9 52 98.7 61.1 154.8 3.2 19.8 5.1 39.7 4.7 59.8-0.9 50.3-11.7 98.3-33 144-13 27.9-29.5 53.5-49.7 76.6-30.5 34.8-67.3 60.7-110.9 76.7-23.6 8.7-48 13.6-73 15.2-6.2 0.4-12.4 0.5-18.6 0.5H512.7z m-4.6-580.6c0-0.1 0-0.1 0 0-70.5-0.1-141-0.1-211.5-0.1-10.2 0-20.4 0.2-30.6 1.3-26.9 2.8-52.1 10.8-75.3 24.9-26.8 16.2-47.3 38.8-64.1 64.9-15 23.2-25.7 48.3-33.7 74.7-9.3 30.9-15.1 62.6-15.2 94.8-0.3 110.5-0.1 220.9-0.1 331.4 0 1-0.5 2.4 0.5 3 1 0.6 1.9-0.6 2.7-1.1 28.5-18.3 58.1-34.6 89.3-47.9 41.6-17.8 84.6-28.4 130.3-28.3 136.6 0.4 273.2 0.1 409.8 0.2 13.8 0 27.6-0.1 41.3-1.8 20.1-2.5 39.5-7.6 57.9-16.3 36.9-17.4 66.3-43.5 88.8-77.3 40-60.4 55.1-126.7 45.3-198.5-5.3-38.9-17.3-75.7-36.2-110.2-14.1-25.7-31.8-48.7-54.2-67.8-34.8-29.9-75.5-45.2-121.1-45.6-74.6-0.8-149.3-0.3-223.9-0.3z" p-id="1698"></path><path d="M548.2 673.6c-17.5 0.4-34.7-2.3-51.7-6.4-6.4-1.5-11.5-5-16.1-9.6-24.6-24.3-48.9-48.8-72.3-74.3-21.6-23.5-42.6-47.5-61.8-73.1-13.4-17.9-26.4-36.1-35.1-56.9-8.1-19.4-10.5-39.5-7.4-60.4 4.1-27.4 16.7-50.8 33.5-72.3 6.3-8 13.2-15.3 20.8-22 9.3-8.2 20.2-10.3 31.9-5.9 11.8 4.5 18.7 13.4 20.2 26.1 1.2 10.3-2.1 19.1-9.7 26.2-11.8 11.2-21.8 23.7-28.6 38.6-6.7 14.7-8.8 29.7-2.7 45.1 4 10.2 10.3 19.3 16.5 28.4 17.1 24.9 36.8 47.7 56.8 70.2 22.1 24.9 45.6 48.5 68.9 72.3 2.4 2.5 5.1 4.8 7.5 7.3 2.2 2.2 5.1 1.8 7.7 2.1 16.1 2.2 32.1 2.8 48-1.3 13.2-3.4 23.6-10.4 30.9-22.3 12-19.3 38-20.4 51.9-2.7 7.7 9.9 8.5 24.1 1.8 35.4-16 27.1-40.1 43.2-70.2 50.9-4.2 1.1-8.4 1.9-12.7 2.6-9.2 1.5-18.5 2.4-28.1 2zM532.5 315.7c0.1-10.5 10.4-18.2 20.3-15.1 22.8 7.2 43.9 17.5 63.6 31 21.2 14.6 38.1 33.1 51.9 54.6 16.2 25.1 27.7 52.3 34.8 81.2 1 4 1.8 8 2.5 12.1 1.5 8.1-3.6 16.1-11.5 18.2-7.8 2.1-16.1-2-19-9.7-0.8-2.2-1.2-4.7-1.8-7-8-35.7-22.7-68.2-46-96.7-14.3-17.4-32.4-30-52.2-40.2-10.1-5.2-20.6-9.5-31.5-13-7.1-2.2-11.1-8-11.1-15.4zM615.6 513.1c-8.1-0.1-14.1-5.1-15.8-13.6-3.2-15.8-9.1-30.5-17.6-44.1-14.4-23.1-34.1-39.9-59.3-50.2-1.5-0.6-3-0.9-4.5-1.4-8.7-2.9-13.3-11.7-10.6-20.1 2.9-8.8 11.6-13.1 20.5-10.1 38.1 12.8 65.8 38 85.4 72.5 8.5 15 14.3 31.1 17.5 48.2 1.9 9.8-5.5 18.8-15.6 18.8z" p-id="1699"></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 415 415" xml:space="preserve">
<g>
<path d="M174.848,188.711c2.407-2.068,3.577-5.109,3.577-9.297c0-4.133-1.193-7.307-3.647-9.701
c-2.431-2.369-6.148-3.571-11.048-3.571h-15.915v25.73h15.576C168.552,191.872,172.407,190.809,174.848,188.711z"/>
<path d="M0,65.241v284.518h415V65.241H0z M70.675,238.253c-18.293,0-33.123-14.83-33.123-33.123
c0-18.293,14.83-33.123,33.123-33.123s33.123,14.83,33.123,33.123C103.798,223.423,88.968,238.253,70.675,238.253z
M206.768,249.556h-22.422l-0.411-0.328c-2.099-1.679-3.467-4.433-4.067-8.185c-0.552-3.451-0.832-6.769-0.832-9.859v-6.979
c0-4.492-1.212-8.003-3.602-10.435c-2.417-2.456-5.779-3.65-10.28-3.65h-17.337v39.437h-22.787v-101.66h38.701
c11.546,0,20.743,2.699,27.335,8.023c6.688,5.403,10.078,13.012,10.078,22.614c0,5.404-1.442,10.124-4.285,14.028
c-2.217,3.044-5.267,5.647-9.089,7.767c4.433,1.864,7.785,4.556,9.99,8.029c2.696,4.247,4.063,9.533,4.063,15.711v7.25
c0,2.622,0.361,5.407,1.074,8.278c0.66,2.664,1.774,4.637,3.309,5.864l0.563,0.451V249.556z M287.335,249.556h-70.558v-101.66
h70.422v18.246h-47.636v21.801h40.859v18.246h-40.859v25.121h47.771V249.556z M374.201,183.193l-0.552,1.65h-21.824v-1.5
c0-6.092-1.443-10.781-4.29-13.937c-2.805-3.111-7.331-4.688-13.454-4.688c-5.458,0-9.671,2.155-12.879,6.589
c-3.273,4.522-4.933,10.41-4.933,17.501v19.699c0,7.167,1.743,13.091,5.182,17.607c3.39,4.452,7.854,6.617,13.646,6.617
c5.714,0,9.953-1.507,12.602-4.479c2.693-3.022,4.059-7.668,4.059-13.808v-1.5h21.756l0.552,1.65l0.004,0.23
c0.187,11.009-3.245,19.89-10.199,26.396c-6.92,6.473-16.601,9.755-28.772,9.755c-12.247,0-22.344-4.006-30.01-11.906
c-7.655-7.887-11.537-18.155-11.537-30.521v-19.583c0-12.311,3.785-22.573,11.25-30.504c7.488-7.957,17.34-11.991,29.28-11.991
c12.524,0,22.485,3.289,29.605,9.776c7.166,6.531,10.705,15.519,10.519,26.714L374.201,183.193z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675772071409" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5514" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zM584 328c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z" p-id="5515"></path><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-0.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7 0.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c0.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-0.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66 0.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 0.05 0.01 0.1 0.01 0.15 1.07 12.15-0.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-0.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5 0.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l0.36 0.03c7.97 0.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-0.16 0.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l0.18-0.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z" p-id="5516"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675683991720" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4457" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M561 362.67h-98V463h98V362.67z m200.67 0H661.33V463h100.33V362.67zM911 687c-62.22 0-121.33-9.33-177.33-28-20.22-6.22-37.33-2.34-51.33 11.67L572.67 780.34c-70-35.78-133.39-82.06-190.17-138.84S279.45 521.33 243.67 451.33l109.67-109.67c14-14 17.89-31.11 11.67-51.34-18.67-56-28-115.11-28-177.33 0-14-4.67-25.67-14-35-9.33-9.33-21-14-35-14H113c-14 0-25.67 4.67-35 14-9.33 9.33-14 21-14 35 0 112 21.39 220.11 64.17 324.33 42.78 104.22 103.83 196 183.17 275.34 79.33 79.34 171.1 140.4 275.33 183.17C690.89 938.61 799 960 911 960c14 0 25.67-4.67 35-14 9.33-9.33 14-21 14-35V736c0-14-4.67-25.67-14-35-9.33-9.33-21-14-35-14z m-51.33-224H960V362.67H859.67V463z" p-id="4458"></path></svg>

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -3,14 +3,11 @@ import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:win32/win32.dart' as win32;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -19,14 +16,17 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:window_size/window_size.dart' as window_size;
import 'package:url_launcher/url_launcher.dart';
import 'package:win32/win32.dart' as win32;
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
@@ -34,8 +34,6 @@ import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
import '../consts.dart';
final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey();
@@ -702,7 +700,6 @@ void msgBox(String id, String type, String title, String text, String link,
buttons.insert(
0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
}
// TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
@@ -716,8 +713,7 @@ void msgBox(String id, String type, String title, String text, String link,
dialogManager.show(
(setState, close) => CustomAlertDialog(
title: null,
content: SelectionArea(
child: msgboxContent(type, title, text).paddingOnly(bottom: 10)),
content: SelectionArea(child: msgboxContent(type, title, text)),
actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
@@ -782,7 +778,7 @@ Widget msgboxContent(String type, String title, String text) {
),
),
],
);
).marginOnly(bottom: 12);
}
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
@@ -1280,10 +1276,12 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
Future<void> initUniLinks() async {
if (!Platform.isWindows && !Platform.isMacOS) {
return;
Future<bool> initUniLinks() async {
if (Platform.isLinux) {
return false;
}
// Register uni links for Windows. The required info of url scheme is already
// declared in `Info.plist` for macOS.
if (Platform.isWindows) {
registerProtocol('rustdesk');
}
@@ -1291,22 +1289,33 @@ Future<void> initUniLinks() async {
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
return;
return false;
}
parseRustdeskUri(initialLink);
return parseRustdeskUri(initialLink);
} catch (err) {
debugPrintStack(label: "$err");
return false;
}
}
StreamSubscription? listenUniLinks() {
if (!(Platform.isWindows || Platform.isMacOS)) {
/// Listen for uni links.
///
/// * handleByFlutter: Should uni links be handled by Flutter.
///
/// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
if (Platform.isLinux) {
return null;
}
final sub = uriLinkStream.listen((Uri? uri) {
debugPrint("A uri was received: $uri.");
if (uri != null) {
callUniLinksUriHandler(uri);
if (handleByFlutter) {
callUniLinksUriHandler(uri);
} else {
bind.sendUrlScheme(url: uri.toString());
}
} else {
print("uni listen error: uri is empty.");
}
@@ -1316,11 +1325,19 @@ StreamSubscription? listenUniLinks() {
return sub;
}
/// Returns true if we successfully handle the startup arguments.
/// Handle command line arguments
///
/// * Returns true if we successfully handle the startup arguments.
bool checkArguments() {
if (kBootArgs.isNotEmpty) {
final ret = parseRustdeskUri(kBootArgs.first);
if (ret) {
return true;
}
}
// bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
// check connect args
final connectIndex = kBootArgs.indexOf("--connect");
var connectIndex = kBootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
}
@@ -1355,7 +1372,7 @@ bool checkArguments() {
bool parseRustdeskUri(String uriPath) {
final uri = Uri.tryParse(uriPath);
if (uri == null) {
print("uri is not valid: $uriPath");
debugPrint("uri is not valid: $uriPath");
return false;
}
return callUniLinksUriHandler(uri);
@@ -1374,7 +1391,7 @@ bool callUniLinksUriHandler(Uri uri) {
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid);
});
return false;
return true;
}
return false;
}
@@ -1514,8 +1531,12 @@ Future<void> onActiveWindowChanged() async {
} catch (err) {
debugPrintStack(label: "$err");
} finally {
debugPrint("Start closing RustDesk...");
await windowManager.setPreventClose(false);
await windowManager.close();
if (Platform.isMacOS) {
RdPlatformChannel.instance.terminate();
}
}
}
}
@@ -1708,3 +1729,30 @@ Future<void> updateSystemWindowTheme() async {
}
}
}
/// macOS only
///
/// Note: not found a general solution for rust based AVFoundation bingding.
/// [AVFoundation] crate has compile error.
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
enum PermissionAuthorizeType {
undetermined,
authorized,
denied, // and restricted
}
Future<PermissionAuthorizeType> osxCanRecordAudio() async {
int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
print(res);
if (res > 0) {
return PermissionAuthorizeType.authorized;
} else if (res == 0) {
return PermissionAuthorizeType.undetermined;
} else {
return PermissionAuthorizeType.denied;
}
}
Future<bool> osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
}

View File

@@ -1,6 +1,7 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
@@ -11,106 +12,15 @@ import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
const int groupTabIndex = 4;
const String defaultGroupTabname = 'Group';
class StatePeerTab {
final RxInt currentTab = 0.obs;
final RxInt tabHiddenFlag = 0.obs;
final RxList<String> tabNames = [
'Recent Sessions',
'Favorites',
'Discovered',
'Address Book',
defaultGroupTabname,
].obs;
StatePeerTab._() {
tabHiddenFlag.value = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0);
var tabs = _notHiddenTabs();
currentTab.value =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabs.contains(currentTab.value)) {
currentTab.value = 0;
}
}
static final StatePeerTab instance = StatePeerTab._();
check() {
var tabs = _notHiddenTabs();
if (filterGroupCard()) {
if (currentTab.value == groupTabIndex) {
currentTab.value =
tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: currentTab.value.toString());
}
} else {
if (gFFI.userModel.isAdmin.isFalse &&
gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = defaultGroupTabname;
}
if (tabs.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
currentTab.value = groupTabIndex;
}
}
}
List<int> currentTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i) && !_isTabFilter(i)) {
v.add(i);
}
}
return v;
}
bool filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
bool _isTabHidden(int tabindex) {
return tabHiddenFlag & (1 << tabindex) != 0;
}
bool _isTabFilter(int tabIndex) {
if (tabIndex == groupTabIndex) {
return filterGroupCard();
}
return false;
}
List<int> _notHiddenTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i)) {
v.add(i);
}
}
return v;
}
}
final statePeerTab = StatePeerTab.instance;
class PeerTabPage extends StatefulWidget {
const PeerTabPage({Key? key}) : super(key: key);
@override
@@ -156,11 +66,10 @@ class _PeerTabPageState extends State<PeerTabPage>
),
() => {}),
];
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
@override
void initState() {
adjustTab();
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
@@ -172,16 +81,11 @@ class _PeerTabPageState extends State<PeerTabPage>
Future<void> handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
statePeerTab.currentTab.value = tabIndex;
gFFI.peerTabModel.setCurrentTab(tabIndex);
entries[tabIndex].load();
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
@@ -199,6 +103,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
buildScrollJumper(),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
@@ -213,82 +118,115 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget _createSwitchBar(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Obx(() {
var tabs = statePeerTab.currentTabs();
return ListView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
controller: ScrollController(),
children: tabs.map((t) {
return InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: statePeerTab.currentTab.value == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
child: Align(
alignment: Alignment.center,
child: Text(
translatedTabname(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: statePeerTab.currentTab.value == t
? textColor
: textColor
?..withOpacity(0.5)),
),
)),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
final model = Provider.of<PeerTabModel>(context);
int indexCounter = -1;
return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (oldIndex, newIndex) {
model.onReorder(oldIndex, newIndex);
},
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
scrollController: model.sc,
children: model.visibleOrderedTabs.map((t) {
indexCounter++;
return ReorderableDragStartListener(
key: ValueKey(t),
index: indexCounter,
child: VisibilityDetector(
key: ValueKey(t),
onVisibilityChanged: (info) {
final id = (info.key as ValueKey).value;
model.setTabFullyVisible(id, info.visibleFraction > 0.99);
},
);
}).toList());
});
child: Listener(
// handle mouse wheel
onPointerSignal: (e) {
if (e is PointerScrollEvent) {
if (!model.sc.canScroll) return;
_scrollDebounce.call(() {
model.sc.animateTo(model.sc.offset + e.scrollDelta.dy,
duration: Duration(milliseconds: 200),
curve: Curves.ease);
});
}
},
child: InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: model.currentTab == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
child: Align(
alignment: Alignment.center,
child: Text(
model.translatedTabname(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: model.currentTab == t
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor
?..withOpacity(0.5)),
),
)),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
},
),
),
),
);
}).toList());
}
translatedTabname(int index) {
if (index < statePeerTab.tabNames.length) {
final name = statePeerTab.tabNames[index];
if (index == groupTabIndex) {
if (name == defaultGroupTabname) {
return translate(name);
} else {
return name;
}
} else {
return translate(name);
}
}
assert(false);
return index.toString();
Widget buildScrollJumper() {
final model = Provider.of<PeerTabModel>(context);
return Offstage(
offstage: !model.showScrollBtn,
child: Row(
children: [
GestureDetector(
child: Icon(Icons.arrow_left,
size: 22,
color: model.leftFullyVisible
? Theme.of(context).disabledColor
: null),
onTap: model.sc.backward),
GestureDetector(
child: Icon(Icons.arrow_right,
size: 22,
color: model.rightFullyVisible
? Theme.of(context).disabledColor
: null),
onTap: model.sc.forward)
],
));
}
Widget _createPeersView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
return Expanded(
child: Obx(() {
var tabs = statePeerTab.currentTabs();
if (tabs.isEmpty) {
return visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
final model = Provider.of<PeerTabModel>(context);
Widget child;
if (model.visibleOrderedTabs.isEmpty) {
child = visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
} else {
if (model.visibleOrderedTabs.contains(model.currentTab)) {
child = entries[model.currentTab].widget;
} else {
if (tabs.contains(statePeerTab.currentTab.value)) {
return entries[statePeerTab.currentTab.value].widget;
} else {
statePeerTab.currentTab.value = tabs[0];
return entries[statePeerTab.currentTab.value].widget;
}
model.setCurrentTab(model.visibleOrderedTabs[0]);
child = entries[0].widget;
}
}).marginSymmetric(vertical: verticalMargin));
}
return Expanded(
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
@@ -321,13 +259,6 @@ class _PeerTabPageState extends State<PeerTabPage>
);
}
adjustTab() {
var tabs = statePeerTab.currentTabs();
if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
statePeerTab.currentTab.value = tabs[0];
}
}
Widget visibleContextMenuListener(Widget child) {
return Listener(
onPointerDown: (e) {
@@ -347,44 +278,36 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
return Obx(() {
final List<MenuEntryBase> menu = List.empty(growable: true);
for (int i = 0; i < statePeerTab.tabNames.length; i++) {
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
continue;
}
int bitMask = 1 << i;
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translatedTabname(i),
getter: () async {
return statePeerTab.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
if (show) {
statePeerTab.tabHiddenFlag.value &= ~bitMask;
} else {
statePeerTab.tabHiddenFlag.value |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card',
v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
cancelFunc();
adjustTab();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
});
final model = Provider.of<PeerTabModel>(context);
final List<MenuEntryBase> menu = List.empty(growable: true);
final List<int> menuIndex = List.empty(growable: true);
var list = model.orderedNotFilteredTabs();
for (int i = 0; i < list.length; i++) {
int tabIndex = list[i];
int bitMask = 1 << tabIndex;
menuIndex.add(tabIndex);
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: model.translatedTabname(tabIndex),
getter: () async {
return model.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
model.onHideShow(tabIndex, show);
cancelFunc();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
}
}
@@ -421,7 +344,9 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
FocusNode focusNode = FocusNode();
focusNode.addListener(() {
focused.value = focusNode.hasFocus;
peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length);
peerSearchTextController.selection = TextSelection(
baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length);
});
return Container(
width: 120,

View File

@@ -1,17 +1,19 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main";
const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
@@ -24,7 +26,6 @@ const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kUniLinksPrefix = "rustdesk://";
const String kActionNewConnection = "connection/new/";
const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings";
@@ -105,6 +106,12 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom';
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
const kRemoteAudioGuestToHost = 'guest-to-host';
/// [kRemoteAudioDualWay] dual-way audio mode(default).
const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true;
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels

View File

@@ -44,6 +44,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
var watchIsCanRecordAudio = false;
Timer? _updateTimer;
@override
@@ -79,7 +80,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
buildHelpCards(),
FutureBuilder<Widget>(
future: buildHelpCards(),
builder: (_, data) {
if (data.hasData) {
return data.data!;
} else {
return const Offstage();
}
},
),
],
),
),
@@ -302,7 +312,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
Widget buildHelpCards() {
Future<Widget> buildHelpCards() async {
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@@ -349,6 +359,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsInstalledDaemon(prompt: true);
});
}
//// Disable microphone configuration for macOS. We will request the permission when needed.
// else if ((await osxCanRecordAudio() !=
// PermissionAuthorizeType.authorized)) {
// return buildInstallCard("Permissions", "config_microphone", "Configure",
// () async {
// osxRequestAudio();
// watchIsCanRecordAudio = true;
// });
// }
} else if (Platform.isLinux) {
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
@@ -481,6 +500,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
setState(() {});
}
}
if (watchIsCanRecordAudio) {
if (Platform.isMacOS) {
Future.microtask(() async {
if ((await osxCanRecordAudio() ==
PermissionAuthorizeType.authorized)) {
watchIsCanRecordAudio = false;
setState(() {});
}
});
} else {
watchIsCanRecordAudio = false;
setState(() {});
}
}
});
Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
@@ -376,10 +375,10 @@ class _RemotePageState extends State<RemotePage>
class ImagePaint extends StatefulWidget {
final String id;
final Rx<bool> zoomCursor;
final Rx<bool> cursorOverImage;
final Rx<bool> keyboardEnabled;
final Rx<bool> remoteCursorMoved;
final RxBool zoomCursor;
final RxBool cursorOverImage;
final RxBool keyboardEnabled;
final RxBool remoteCursorMoved;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
@@ -402,10 +401,10 @@ class _ImagePaintState extends State<ImagePaint> {
final ScrollController _vertical = ScrollController();
String get id => widget.id;
Rx<bool> get zoomCursor => widget.zoomCursor;
Rx<bool> get cursorOverImage => widget.cursorOverImage;
Rx<bool> get keyboardEnabled => widget.keyboardEnabled;
Rx<bool> get remoteCursorMoved => widget.remoteCursorMoved;
RxBool get zoomCursor => widget.zoomCursor;
RxBool get cursorOverImage => widget.cursorOverImage;
RxBool get keyboardEnabled => widget.keyboardEnabled;
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
@@ -414,27 +413,50 @@ class _ImagePaintState extends State<ImagePaint> {
var c = Provider.of<CanvasModel>(context);
final s = c.scale;
mouseRegion({child}) => Obx(() => MouseRegion(
cursor: cursorOverImage.isTrue
? c.cursorEmbedded
? SystemMouseCursors.none
: keyboardEnabled.isTrue
? (() {
if (remoteCursorMoved.isTrue) {
_lastRemoteCursorMoved = true;
return SystemMouseCursors.none;
} else {
if (_lastRemoteCursorMoved) {
_lastRemoteCursorMoved = false;
_firstEnterImage.value = true;
}
return _buildCustomCursor(context, s);
}
}())
: _buildDisabledCursor(context, s)
: MouseCursor.defer,
onHover: (evt) {},
child: child));
mouseRegion({child}) => Obx(() {
double getCursorScale() {
var c = Provider.of<CanvasModel>(context);
var cursorScale = 1.0;
if (Platform.isWindows) {
// debug win10
final isViewAdaptive =
c.viewStyle.style == kRemoteViewStyleAdaptive;
if (zoomCursor.value && isViewAdaptive) {
cursorScale = s * c.devicePixelRatio;
}
} else {
final isViewOriginal =
c.viewStyle.style == kRemoteViewStyleOriginal;
if (zoomCursor.value || isViewOriginal) {
cursorScale = s;
}
}
return cursorScale;
}
return MouseRegion(
cursor: cursorOverImage.isTrue
? c.cursorEmbedded
? SystemMouseCursors.none
: keyboardEnabled.isTrue
? (() {
if (remoteCursorMoved.isTrue) {
_lastRemoteCursorMoved = true;
return SystemMouseCursors.none;
} else {
if (_lastRemoteCursorMoved) {
_lastRemoteCursorMoved = false;
_firstEnterImage.value = true;
}
return _buildCustomCursor(
context, getCursorScale());
}
}())
: _buildDisabledCursor(context, getCursorScale())
: MouseCursor.defer,
onHover: (evt) {},
child: child);
});
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
@@ -480,7 +502,7 @@ class _ImagePaintState extends State<ImagePaint> {
if (cache == null) {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale, zoomCursor.value);
final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
// [Safety]
@@ -646,7 +668,8 @@ class CursorPaint extends StatelessWidget {
double x = (m.x - hotx) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cy;
double scale = 1.0;
if (zoomCursor.isTrue) {
final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal;
if (zoomCursor.value || isViewOriginal) {
x = m.x - hotx + cx / c.scale;
y = m.y - hoty + cy / c.scale;
scale = c.scale;

View File

@@ -243,96 +243,35 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
padding: padding,
),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
),
],
curOptionGetter: () async =>
// null means peer id is not found, which there's no need to care about
await bind.sessionGetViewStyle(id: key) ?? '',
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(id: key, value: newValue);
ffi.canvasModel.updateViewStyle();
cancelFunc();
},
padding: padding,
RemoteMenuEntry.viewStyle(
key,
ffi,
padding,
dismissFunc: cancelFunc,
),
]);
if (!ffi.canvasModel.cursorEmbedded) {
menu.add(MenuEntryDivider<String>());
menu.add(() {
final state = ShowRemoteCursorState.find(key);
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
state.value = v;
await bind.sessionToggleOption(
id: key, value: 'show-remote-cursor');
cancelFunc();
},
padding: padding,
);
}());
menu.add(RemoteMenuEntry.showRemoteCursor(
key,
padding,
dismissFunc: cancelFunc,
));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
menu.add(MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate('Disable clipboard'),
getter: () async {
return bind.sessionGetToggleOptionSync(
id: key, arg: 'disable-clipboard');
},
setter: (bool v) async {
await bind.sessionToggleOption(id: key, value: 'disable-clipboard');
cancelFunc();
},
padding: padding,
));
menu.add(RemoteMenuEntry.disableClipboard(key, padding,
dismissFunc: cancelFunc));
}
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: key);
cancelFunc();
},
padding: padding,
dismissOnClicked: true,
));
menu.add(
RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc));
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: key);
cancelFunc();
},
padding: padding,
dismissOnClicked: true,
));
menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding,
dismissFunc: cancelFunc));
}
}

View File

@@ -514,6 +514,39 @@ class _CmControlPanel extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Offstage(
offstage: !client.inVoiceCall,
child: buildButton(context,
color: Colors.red,
onClick: () => closeVoiceCall(),
icon: Icon(Icons.phone_disabled_rounded, color: Colors.white),
text: "Stop voice call",
textColor: Colors.white),
),
Offstage(
offstage: !client.incomingVoiceCall,
child: Row(
children: [
Expanded(
child: buildButton(context,
color: MyTheme.accent,
onClick: () => handleVoiceCall(true),
icon: Icon(Icons.phone_enabled, color: Colors.white),
text: "Accept",
textColor: Colors.white),
),
Expanded(
child: buildButton(context,
color: Colors.red,
onClick: () => handleVoiceCall(false),
icon:
Icon(Icons.phone_disabled_rounded, color: Colors.white),
text: "Dismiss",
textColor: Colors.white),
)
],
),
),
Offstage(
offstage: !client.fromSwitch,
child: buildButton(context,
@@ -619,7 +652,7 @@ class _CmControlPanel extends StatelessWidget {
.marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
}
buildButton(
Widget buildButton(
BuildContext context, {
required Color? color,
required Function() onClick,
@@ -685,6 +718,14 @@ class _CmControlPanel extends StatelessWidget {
void handleSwitchBack(BuildContext context) {
bind.cmSwitchBack(connId: client.id);
}
void handleVoiceCall(bool accept) {
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
}
void closeVoiceCall() {
bind.cmCloseVoiceCall(id: client.id);
}
}
void checkClickTime(int id, Function() callback) async {

View File

@@ -790,6 +790,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
_PopupMenuRoute({
required this.position,
required this.items,
this.menuWrapper,
this.initialValue,
this.elevation,
required this.barrierLabel,
@@ -802,6 +803,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final RelativeRect position;
final List<PopupMenuEntry<T>> items;
final MenuWrapper? menuWrapper;
final List<Size?> itemSizes;
final T? initialValue;
final double? elevation;
@@ -844,11 +846,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
}
}
final Widget menu = _PopupMenu<T>(
Widget menu = _PopupMenu<T>(
route: this,
semanticLabel: semanticLabel,
constraints: constraints,
);
if (this.menuWrapper != null) {
menu = this.menuWrapper!(menu);
}
final MediaQueryData mediaQuery = MediaQuery.of(context);
return MediaQuery.removePadding(
context: context,
@@ -1035,6 +1040,7 @@ Future<T?> showMenu<T>({
required BuildContext context,
required RelativeRect position,
required List<PopupMenuEntry<T>> items,
MenuWrapper? menuWrapper,
T? initialValue,
double? elevation,
String? semanticLabel,
@@ -1062,6 +1068,7 @@ Future<T?> showMenu<T>({
return navigator.push(_PopupMenuRoute<T>(
position: position,
items: items,
menuWrapper: menuWrapper,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
@@ -1094,6 +1101,8 @@ typedef PopupMenuCanceled = void Function();
typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(
BuildContext context);
typedef MenuWrapper = Widget Function(Widget child);
/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
/// because an item was selected. The value passed to [onSelected] is the value of
/// the selected menu item.
@@ -1124,6 +1133,7 @@ class PopupMenuButton<T> extends StatefulWidget {
const PopupMenuButton({
Key? key,
required this.itemBuilder,
this.menuWrapper,
this.initialValue,
this.onHover,
this.onSelected,
@@ -1151,6 +1161,9 @@ class PopupMenuButton<T> extends StatefulWidget {
/// Called when the button is pressed to create the items to show in the menu.
final PopupMenuItemBuilder<T> itemBuilder;
/// Menu wrapper.
final MenuWrapper? menuWrapper;
/// The value of the menu item, if any, that should be highlighted when the menu opens.
final T? initialValue;
@@ -1333,6 +1346,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
context: context,
elevation: widget.elevation ?? popupMenuTheme.elevation,
items: items,
menuWrapper: widget.menuWrapper,
initialValue: widget.initialValue,
position: position,
shape: widget.shape ?? popupMenuTheme.shape,

View File

@@ -109,13 +109,17 @@ class MenuConfig {
this.boxWidth});
}
typedef DismissCallback = Function();
abstract class MenuEntryBase<T> {
bool dismissOnClicked;
DismissCallback? dismissCallback;
RxBool? enabled;
MenuEntryBase({
this.dismissOnClicked = false,
this.enabled,
this.dismissCallback,
});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
@@ -146,12 +150,14 @@ class MenuEntryRadioOption {
String value;
bool dismissOnClicked;
RxBool? enabled;
DismissCallback? dismissCallback;
MenuEntryRadioOption({
required this.text,
required this.value,
this.dismissOnClicked = false,
this.enabled,
this.dismissCallback,
});
}
@@ -177,8 +183,13 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
required this.optionSetter,
this.padding,
dismissOnClicked = false,
dismissCallback,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
) {
() async {
_curOption.value = await curOptionGetter();
}();
@@ -249,6 +260,9 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
onPressed() {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (opt.dismissCallback != null) {
opt.dismissCallback!();
}
}
setOption(opt.value);
}
@@ -360,6 +374,9 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (opt.dismissCallback != null) {
opt.dismissCallback!();
}
}
setOption(opt.value);
},
@@ -421,7 +438,12 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
this.textStyle,
this.padding,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
dismissCallback,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
);
RxBool get curOption;
Future<void> setOption(bool? option);
@@ -463,6 +485,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(v);
},
@@ -474,6 +499,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (super.dismissOnClicked &&
Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(v);
},
@@ -485,6 +513,9 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
onPressed: () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
setOption(!curOption.value);
},
@@ -508,6 +539,7 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
switchType: switchType,
text: text,
@@ -515,6 +547,7 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
padding: padding,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
) {
() async {
_curOption.value = await getter();
@@ -551,12 +584,15 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
EdgeInsets? padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
switchType: switchType,
text: text,
textStyle: textStyle,
padding: padding,
dismissOnClicked: dismissOnClicked);
switchType: switchType,
text: text,
textStyle: textStyle,
padding: padding,
dismissOnClicked: dismissOnClicked,
dismissCallback: dismissCallback,
);
@override
RxBool get curOption => getter();
@@ -627,9 +663,11 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
this.padding,
dismissOnClicked = false,
RxBool? enabled,
dismissCallback,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
dismissCallback: dismissCallback,
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
@@ -641,6 +679,9 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
? () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
if (super.dismissCallback != null) {
super.dismissCallback!();
}
}
proc();
}

View File

@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
@@ -99,6 +100,175 @@ class _MenubarTheme {
static const double dividerHeight = 12.0;
}
typedef DismissFunc = void Function();
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(id: remoteId) ?? '';
if (rxViewStyle != null) {
rxViewStyle.value = viewStyle;
}
return viewStyle;
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(id: remoteId, 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,
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(id: remoteId, value: optKey);
state.value =
bind.sessionGetToggleOptionSync(id: remoteId, arg: optKey);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> disableClipboard(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return createSwitchMenuEntry(
remoteId,
'Disable clipboard',
'disable-clipboard',
padding,
true,
dismissCallback: dismissCallback,
);
}
static MenuEntrySwitch<String> createSwitchMenuEntry(
String remoteId,
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(id: remoteId, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(id: remoteId, value: option);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: dismissOnClicked,
dismissCallback: dismissCallback,
);
}
static MenuEntryButton<String> insertLock(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: remoteId);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
static insertCtrlAltDel(
String remoteId,
EdgeInsets? padding, {
DismissFunc? dismissFunc,
DismissCallback? dismissCallback,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: remoteId);
if (dismissFunc != null) {
dismissFunc();
}
},
padding: padding,
dismissOnClicked: true,
dismissCallback: dismissCallback,
);
}
}
class RemoteMenubar extends StatefulWidget {
final String id;
final FFI ffi;
@@ -221,6 +391,18 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
}
Widget _buildPointerTrackWidget(Widget child) {
return Listener(
onPointerHover: (PointerHoverEvent e) =>
widget.ffi.inputModel.lastMousePos = e.position,
child: MouseRegion(
child: child,
),
);
}
_menuDismissCallback() => widget.ffi.inputModel.refreshMousePos();
Widget _buildMenubar(BuildContext context) {
final List<Widget> menubarItems = [];
if (!isWebDesktop) {
@@ -244,6 +426,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
menubarItems.add(_buildKeyboard(context));
if (!isWeb) {
menubarItems.add(_buildChat(context));
menubarItems.add(_buildVoiceCall(context));
}
menubarItems.add(_buildRecording(context));
menubarItems.add(_buildClose(context));
@@ -297,31 +480,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
);
}
final _chatButtonKey = GlobalKey();
Widget _buildChat(BuildContext context) {
return IconButton(
key: _chatButtonKey,
tooltip: translate('Chat'),
onPressed: () {
RenderBox? renderBox =
_chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos;
if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
}
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
},
icon: const Icon(
Icons.message,
color: _MenubarTheme.commonColor,
),
);
}
Widget _buildMonitor(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
return mod_menu.PopupMenuButton(
@@ -375,6 +533,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
@@ -390,13 +549,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
mod_menu.PopupMenuItem<String>(
height: _MenubarTheme.height,
padding: EdgeInsets.zero,
child: Listener(
onPointerHover: (PointerHoverEvent e) =>
widget.ffi.inputModel.lastMousePos = e.position,
child: MouseRegion(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: rowChildren),
child: _buildPointerTrackWidget(
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: rowChildren,
),
),
)
@@ -446,6 +602,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
),
tooltip: translate('Display Settings'),
position: mod_menu.PopupMenuPosition.under,
menuWrapper: _buildPointerTrackWidget,
itemBuilder: (BuildContext context) =>
_getDisplayMenu(snapshot.data!, remoteCount)
.map((entry) => entry.build(
@@ -500,12 +657,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
? translate('Stop session recording')
: translate('Start session recording'),
onPressed: () => value.toggle(),
icon: Icon(
value.start
? Icons.pause_circle_filled
: Icons.videocam_outlined,
color: _MenubarTheme.commonColor,
),
icon: value.start
? Icon(
Icons.pause_circle_filled,
color: _MenubarTheme.commonColor,
)
: SvgPicture.asset(
"assets/record_screen.svg",
color: _MenubarTheme.commonColor,
width: Theme.of(context).iconTheme.size ?? 22.0,
height: Theme.of(context).iconTheme.size ?? 22.0,
),
));
} else {
return Offstage();
@@ -526,6 +688,130 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
);
}
final _chatButtonKey = GlobalKey();
Widget _buildChat(BuildContext context) {
FfiModel ffiModel = Provider.of<FfiModel>(context);
return mod_menu.PopupMenuButton(
key: _chatButtonKey,
padding: EdgeInsets.zero,
icon: SvgPicture.asset(
"assets/chat.svg",
color: _MenubarTheme.commonColor,
width: Theme.of(context).iconTheme.size ?? 24.0,
height: Theme.of(context).iconTheme.size ?? 24.0,
),
tooltip: translate('Chat'),
position: mod_menu.PopupMenuPosition.under,
itemBuilder: (BuildContext context) => _getChatMenu(context)
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
Widget _getVoiceCallIcon() {
switch (widget.ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse:
return IconButton(
onPressed: () {
widget.ffi.chatModel.closeVoiceCall(widget.id);
},
icon: SvgPicture.asset(
"assets/voice_call_waiting.svg",
color: Colors.red,
width: Theme.of(context).iconTheme.size ?? 20.0,
height: Theme.of(context).iconTheme.size ?? 20.0,
));
case VoiceCallStatus.connected:
return IconButton(
onPressed: () {
widget.ffi.chatModel.closeVoiceCall(widget.id);
},
icon: Icon(
Icons.phone_disabled_rounded,
color: Colors.red,
size: Theme.of(context).iconTheme.size ?? 22.0,
),
);
default:
return const Offstage();
}
}
String? _getVoiceCallTooltip() {
switch (widget.ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse:
return "Waiting";
case VoiceCallStatus.connected:
return "Disconnect";
default:
return null;
}
}
Widget _buildVoiceCall(BuildContext context) {
return Obx(
() {
final tooltipText = _getVoiceCallTooltip();
return tooltipText == null
? const Offstage()
: IconButton(
padding: EdgeInsets.zero,
icon: _getVoiceCallIcon(),
tooltip: translate(tooltipText),
onPressed: () => bind.sessionRequestVoiceCall(id: widget.id),
);
},
);
}
List<MenuEntryBase<String>> _getChatMenu(BuildContext context) {
final List<MenuEntryBase<String>> chatMenu = [];
const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
chatMenu.addAll([
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Text chat'),
style: style,
),
proc: () {
RenderBox? renderBox =
_chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos;
if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
}
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
},
padding: padding,
dismissOnClicked: true,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Voice call'),
style: style,
),
proc: () {
// Request a voice call.
bind.sessionRequestVoiceCall(id: widget.id);
},
padding: padding,
dismissOnClicked: true,
),
]);
return chatMenu;
}
List<MenuEntryBase<String>> _getControlMenu(BuildContext context) {
final pi = widget.ffi.ffiModel.pi;
final perms = widget.ffi.ffiModel.permissions;
@@ -554,6 +840,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
showSetOSPassword(
widget.id, false, widget.ffi.dialogManager);
@@ -566,6 +853,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@@ -577,6 +865,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
@@ -588,6 +877,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
connect(context, widget.id, isTcpTunneling: true);
},
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
]);
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
@@ -605,23 +895,15 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
);
}
displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
style: style,
),
proc: () {
bind.sessionCtrlAltDel(id: widget.id);
},
padding: padding,
dismissOnClicked: true,
));
displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding,
dismissCallback: _menuDismissCallback));
}
}
if (perms['restart'] != false &&
@@ -638,21 +920,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (perms['keyboard'] != false) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Insert Lock'),
style: style,
),
proc: () {
bind.sessionLockScreen(id: widget.id);
},
padding: padding,
dismissOnClicked: true,
));
displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding,
dismissCallback: _menuDismissCallback));
if (pi.platform == kPeerPlatformWindows) {
displayMenu.add(MenuEntryButton<String>(
@@ -670,6 +944,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (pi.platform != kPeerPlatformAndroid &&
@@ -684,6 +959,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager),
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@@ -699,6 +975,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
@@ -720,10 +997,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
// },
// padding: padding,
// dismissOnClicked: true,
// dismissCallback: _menuDismissCallback,
// ));
// }
}
return displayMenu;
}
@@ -758,33 +1035,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
final peer_version = widget.ffi.ffiModel.pi.version;
final displayMenu = [
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
MenuEntryRadioOption(
text: translate('Scale original'),
value: kRemoteViewStyleOriginal,
dismissOnClicked: true,
),
MenuEntryRadioOption(
text: translate('Scale adaptive'),
value: kRemoteViewStyleAdaptive,
dismissOnClicked: true,
),
],
curOptionGetter: () async {
// null means peer id is not found, which there's no need to care about
final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? '';
widget.state.viewStyle.value = viewStyle;
return viewStyle;
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetViewStyle(id: widget.id, value: newValue);
widget.state.viewStyle.value = newValue;
widget.ffi.canvasModel.updateViewStyle();
},
padding: padding,
dismissOnClicked: true,
RemoteMenuEntry.viewStyle(
widget.id,
widget.ffi,
padding,
dismissCallback: _menuDismissCallback,
rxViewStyle: widget.state.viewStyle,
),
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
@@ -794,21 +1050,26 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Good image quality'),
value: kRemoteImageQualityBest,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Balanced'),
value: kRemoteImageQualityBalanced,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Optimize reaction time'),
value: kRemoteImageQualityLow,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: translate('Custom'),
value: kRemoteImageQualityCustom,
dismissOnClicked: true),
text: translate('Custom'),
value: kRemoteImageQualityCustom,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
],
curOptionGetter: () async =>
// null means peer id is not found, which there's no need to care about
@@ -973,12 +1234,14 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('ScrollAuto'),
value: kRemoteScrollStyleAuto,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
enabled: widget.ffi.canvasModel.imageOverflow,
),
MenuEntryRadioOption(
text: translate('Scrollbar'),
value: kRemoteScrollStyleBar,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
enabled: widget.ffi.canvasModel.imageOverflow,
),
],
@@ -991,6 +1254,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
displayMenu.insert(3, MenuEntryDivider<String>());
@@ -1061,6 +1325,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
);
}
@@ -1087,11 +1352,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Auto'),
value: 'auto',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
MenuEntryRadioOption(
text: 'VP9',
value: 'vp9',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
),
];
if (codecs[0]) {
@@ -1099,6 +1366,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: 'H264',
value: 'h264',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
if (codecs[1]) {
@@ -1106,6 +1374,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: 'H265',
value: 'h265',
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
return list;
@@ -1122,6 +1391,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@@ -1129,23 +1399,11 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
/// Show remote cursor
if (!widget.ffi.canvasModel.cursorEmbedded) {
displayMenu.add(() {
final state = ShowRemoteCursorState.find(widget.id);
return MenuEntrySwitch2<String>(
switchType: SwitchType.scheckbox,
text: translate('Show remote cursor'),
getter: () {
return state;
},
setter: (bool v) async {
state.value = v;
await bind.sessionToggleOption(
id: widget.id, value: 'show-remote-cursor');
},
padding: padding,
dismissOnClicked: true,
);
}());
displayMenu.add(RemoteMenuEntry.showRemoteCursor(
widget.id,
padding,
dismissCallback: _menuDismissCallback,
));
}
/// Show remote cursor scaling with image
@@ -1160,11 +1418,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
return state;
},
setter: (bool v) async {
state.value = v;
await bind.sessionToggleOption(id: widget.id, value: opt);
state.value =
bind.sessionGetToggleOptionSync(id: widget.id, arg: opt);
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
);
}());
}
@@ -1184,6 +1444,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
final perms = widget.ffi.ffiModel.permissions;
@@ -1192,6 +1453,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (perms['audio'] != false) {
displayMenu
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
displayMenu
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
}
if (Platform.isWindows &&
@@ -1203,8 +1466,11 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
displayMenu.add(_createSwitchMenuEntry(
'Disable clipboard', 'disable-clipboard', padding, true));
displayMenu.add(RemoteMenuEntry.disableClipboard(
widget.id,
padding,
dismissCallback: _menuDismissCallback,
));
}
displayMenu.add(_createSwitchMenuEntry(
'Lock after session end', 'lock-after-session-end', padding, true));
@@ -1221,6 +1487,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
padding: padding,
dismissOnClicked: true,
dismissCallback: _menuDismissCallback,
));
}
}
@@ -1233,25 +1500,29 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('Ratio'),
optionsGetter: () {
List<MenuEntryRadioOption> list = [];
List<String> modes = ["legacy"];
List<KeyboardModeMenu> modes = [
KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'),
KeyboardModeMenu(key: 'map', menu: 'Map mode'),
KeyboardModeMenu(key: 'translate', menu: 'Translate mode'),
];
if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) {
modes.add("map");
}
for (String mode in modes) {
if (mode == "legacy") {
for (KeyboardModeMenu mode in modes) {
if (bind.sessionIsKeyboardModeSupported(
id: widget.id, mode: mode.key)) {
if (mode.key == 'translate') {
if (!Platform.isWindows ||
widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
continue;
}
}
list.add(MenuEntryRadioOption(
text: translate('Legacy mode'), value: 'legacy'));
} else if (mode == "map") {
list.add(MenuEntryRadioOption(
text: translate('Map mode'), value: 'map'));
text: translate(mode.menu), value: mode.key));
}
}
return list;
},
curOptionGetter: () async {
return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy';
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
@@ -1292,6 +1563,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
_menuDismissCallback();
}
showKBLayoutTypeChooser(
localPlatform, widget.ffi.dialogManager);
@@ -1304,6 +1576,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
proc: () {},
padding: EdgeInsets.zero,
dismissOnClicked: false,
dismissCallback: _menuDismissCallback,
),
);
}
@@ -1312,18 +1585,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
MenuEntrySwitch<String> _createSwitchMenuEntry(
String text, String option, EdgeInsets? padding, bool dismissOnClicked) {
return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox,
text: translate(text),
getter: () async {
return bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
},
setter: (bool v) async {
await bind.sessionToggleOption(id: widget.id, value: option);
},
padding: padding,
dismissOnClicked: dismissOnClicked,
);
return RemoteMenuEntry.createSwitchMenuEntry(
widget.id, text, option, padding, dismissOnClicked,
dismissCallback: _menuDismissCallback);
}
}
@@ -1547,3 +1811,10 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
);
}
}
class KeyboardModeMenu {
final String key;
final String menu;
KeyboardModeMenu({required this.key, required this.menu});
}

View File

@@ -1,23 +1,23 @@
import 'dart:io';
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../utils/multi_window_manager.dart';
@@ -545,7 +545,9 @@ class WindowActionPanelState extends State<WindowActionPanel>
void onWindowClose() async {
// hide window on close
if (widget.isMainWindow) {
await rustDeskWinManager.unregisterActiveWindow(0);
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
}
// `hide` must be placed after unregisterActiveWindow, because once all windows are hidden,
// flutter closes the application on macOS. We should ensure the post-run logic has ran successfully.
// e.g.: saving window position.
@@ -976,7 +978,7 @@ class _CloseButton extends StatelessWidget {
offstage: !visible,
child: InkWell(
hoverColor: MyTheme.tabbar(context).closeHoverColor,
customBorder: const RoundedRectangleBorder(),
customBorder: const CircleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
@@ -1099,7 +1101,7 @@ class TabbarTheme extends ThemeExtension<TabbarTheme> {
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
dividerColor: Color.fromARGB(255, 238, 238, 238),
hoverColor: Color.fromARGB(51, 158, 158, 158),
closeHoverColor: Colors.black,
closeHoverColor: Color.fromARGB(255, 224, 224, 224),
selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240));
static const dark = TabbarTheme(

View File

@@ -1,22 +1,23 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_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/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:bot_toast/bot_toast.dart';
// import 'package:window_manager/window_manager.dart';
@@ -31,6 +32,9 @@ int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
/// Uni links.
StreamSubscription? _uniLinkSubscription;
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args");
@@ -114,7 +118,6 @@ Future<void> initEnv(String appType) async {
void runMainApp(bool startService) async {
// register uni links
initUniLinks();
await initEnv(kAppTypeMain);
// trigger connection status updater
await bind.mainCheckConnectStatus();
@@ -130,7 +133,11 @@ void runMainApp(bool startService) async {
// Restore the location of the main window before window hide or show.
await restoreWindowPosition(WindowType.Main);
// Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
if (checkArguments()) {
final handledByUniLinks = await initUniLinks();
final handledByCli = checkArguments();
debugPrint(
"handled by uni links: $handledByUniLinks, handled by cli: $handledByCli");
if (handledByUniLinks || handledByCli) {
windowManager.hide();
} else {
windowManager.show();
@@ -139,8 +146,8 @@ void runMainApp(bool startService) async {
rustDeskWinManager.registerActiveWindow(kWindowMainId);
}
windowManager.setOpacity(1);
windowManager.setTitle(getWindowName());
});
windowManager.setTitle(getWindowName());
}
void runMobileApp() async {
@@ -208,7 +215,8 @@ void runMultiWindow(
}
void runConnectionManagerScreen(bool hide) async {
await initEnv(kAppTypeMain);
await initEnv(kAppTypeConnectionManager);
await bind.cmStartListenIpcThread();
_runApp(
'',
const DesktopServerPage(),
@@ -219,6 +227,8 @@ void runConnectionManagerScreen(bool hide) async {
} else {
showCmWindow();
}
// Start the uni links handler and redirect links to Native, not for Flutter.
_uniLinkSubscription = listenUniLinks(handleByFlutter: false);
}
void showCmWindow() {
@@ -350,6 +360,7 @@ class _AppState extends State<App> {
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
ChangeNotifierProvider.value(value: gFFI.peerTabModel),
],
child: GetMaterialApp(
navigatorKey: globalKey,

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/widgets/button.dart';
import 'package:get/get.dart';
import '../../common.dart';
@@ -371,8 +370,7 @@ void showWaitUacDialog(
tag: '$id-wait-uac',
(setState, close) => CustomAlertDialog(
title: null,
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip')
.marginOnly(bottom: 10),
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
));
}
@@ -645,10 +643,9 @@ class _PasswordWidgetState extends State<PasswordWidget> {
// Here is key idea
suffixIcon: IconButton(
icon: Icon(
// Based on passwordVisible state choose the icon
_passwordVisible ? Icons.visibility : Icons.visibility_off,
color: Theme.of(context).primaryColorDark,
),
// Based on passwordVisible state choose the icon
_passwordVisible ? Icons.visibility : Icons.visibility_off,
color: MyTheme.lightTheme.primaryColor),
onPressed: () {
// Update the state i.e. toggle the state of passwordVisible variable
setState(() {

View File

@@ -5,6 +5,7 @@ import 'package:draggable_float_widget/draggable_float_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import '../consts.dart';
@@ -31,10 +32,14 @@ class ChatModel with ChangeNotifier {
OverlayEntry? chatIconOverlayEntry;
OverlayEntry? chatWindowOverlayEntry;
bool isConnManager = false;
RxBool isWindowFocus = true.obs;
BlockableOverlayState? _blockableOverlayState;
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
final ChatUser me = ChatUser(
id: "",
@@ -312,4 +317,34 @@ class ChatModel with ChangeNotifier {
}
});
}
void onVoiceCallWaiting() {
_voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
}
void onVoiceCallStarted() {
_voiceCallStatus.value = VoiceCallStatus.connected;
}
void onVoiceCallClosed(String reason) {
_voiceCallStatus.value = VoiceCallStatus.notStarted;
}
void onVoiceCallIncoming() {
if (isConnManager) {
_voiceCallStatus.value = VoiceCallStatus.incoming;
}
}
void closeVoiceCall(String id) {
bind.sessionCloseVoiceCall(id: id);
}
}
enum VoiceCallStatus {
notStarted,
waitingForResponse,
connected,
// Connection manager only.
incoming
}

View File

@@ -35,7 +35,7 @@ class GroupModel {
await reset();
if (gFFI.userModel.userName.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
return;
}
userLoading.value = true;
@@ -82,7 +82,7 @@ class GroupModel {
userLoadError.value = err.toString();
} finally {
userLoading.value = false;
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
}
}

View File

@@ -310,7 +310,6 @@ class InputModel {
}
}
int _signOrZero(num x) {
if (x == 0) {
return 0;
@@ -362,7 +361,6 @@ class InputModel {
trackpadScrollDistance = Offset.zero;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage");
if (e.kind != ui.PointerDeviceKind.mouse) {

View File

@@ -13,6 +13,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -197,6 +198,26 @@ class FfiModel with ChangeNotifier {
final peer_id = evt['peer_id'].toString();
await bind.sessionSwitchSides(id: peer_id);
closeConnection(id: peer_id);
} else if (name == "on_url_scheme_received") {
final url = evt['url'].toString();
parseRustdeskUri(url);
} else if (name == "on_voice_call_waiting") {
// Waiting for the response from the peer.
parent.target?.chatModel.onVoiceCallWaiting();
} else if (name == "on_voice_call_started") {
// Voice call is connected.
parent.target?.chatModel.onVoiceCallStarted();
} else if (name == "on_voice_call_closed") {
// Voice call is closed with reason.
final reason = evt['reason'].toString();
parent.target?.chatModel.onVoiceCallClosed(reason);
} else if (name == "on_voice_call_incoming") {
// Voice call is requested by the peer.
parent.target?.chatModel.onVoiceCallIncoming();
} else if (name == "update_voice_call_state") {
parent.target?.serverModel.updateVoiceCallState(evt);
} else {
debugPrint("Unknown event name: $name");
}
};
}
@@ -242,7 +263,6 @@ class FfiModel with ChangeNotifier {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay();
parent.target?.inputModel.refreshMousePos();
notifyListeners();
}
@@ -538,6 +558,7 @@ class CanvasModel with ChangeNotifier {
double _y = 0;
// image scale
double _scale = 1.0;
double _devicePixelRatio = 1.0;
Size _size = Size.zero;
// the tabbar over the image
// double tabBarHeight = 0.0;
@@ -561,6 +582,7 @@ class CanvasModel with ChangeNotifier {
double get x => _x;
double get y => _y;
double get scale => _scale;
double get devicePixelRatio => _devicePixelRatio;
Size get size => _size;
ScrollStyle get scrollStyle => _scrollStyle;
ViewStyle get viewStyle => _lastViewStyle;
@@ -609,13 +631,15 @@ class CanvasModel with ChangeNotifier {
_lastViewStyle = viewStyle;
_scale = viewStyle.scale;
_devicePixelRatio = ui.window.devicePixelRatio;
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / ui.window.devicePixelRatio;
_scale = 1.0 / _devicePixelRatio;
}
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
_imageOverflow.value = _x < 0 || y < 0;
notifyListeners();
parent.target?.inputModel.refreshMousePos();
}
updateScrollStyle() async {
@@ -745,7 +769,7 @@ class CanvasModel with ChangeNotifier {
class CursorData {
final String peerId;
final int id;
final img2.Image? image;
final img2.Image image;
double scale;
Uint8List? data;
final double hotxOrigin;
@@ -770,33 +794,40 @@ class CursorData {
int _doubleToInt(double v) => (v * 10e6).round().toInt();
double _checkUpdateScale(double scale, bool shouldScale) {
double _checkUpdateScale(double scale) {
double oldScale = this.scale;
if (!shouldScale) {
scale = 1.0;
} else {
if (scale != 1.0) {
// Update data if scale changed.
if (Platform.isWindows) {
final tgtWidth = (width * scale).toInt();
final tgtHeight = (width * scale).toInt();
if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
double sw = kMinCursorSize.toDouble() / width;
double sh = kMinCursorSize.toDouble() / height;
scale = sw < sh ? sh : sw;
}
final tgtWidth = (width * scale).toInt();
final tgtHeight = (width * scale).toInt();
if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
double sw = kMinCursorSize.toDouble() / width;
double sh = kMinCursorSize.toDouble() / height;
scale = sw < sh ? sh : sw;
}
}
if (Platform.isWindows) {
if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
if (Platform.isWindows) {
data = img2
.copyResize(
image!,
image,
width: (width * scale).toInt(),
height: (height * scale).toInt(),
interpolation: img2.Interpolation.average,
)
.getBytes(format: img2.Format.bgra);
} else {
data = Uint8List.fromList(
img2.encodePng(
img2.copyResize(
image,
width: (width * scale).toInt(),
height: (height * scale).toInt(),
interpolation: img2.Interpolation.average,
),
),
);
}
}
@@ -806,8 +837,8 @@ class CursorData {
return scale;
}
String updateGetKey(double scale, bool shouldScale) {
scale = _checkUpdateScale(scale, shouldScale);
String updateGetKey(double scale) {
scale = _checkUpdateScale(scale);
return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}';
}
}
@@ -865,7 +896,7 @@ class PredefinedCursor {
_cache = CursorData(
peerId: '',
id: id,
image: _image2?.clone(),
image: _image2!.clone(),
scale: scale,
data: data,
hotxOrigin:
@@ -892,9 +923,10 @@ class CursorModel with ChangeNotifier {
double _hoty = 0;
double _displayOriginX = 0;
double _displayOriginY = 0;
DateTime? _firstUpdateMouseTime;
bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec));
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String id = '';
WeakReference<FFI> parent;
@@ -913,6 +945,15 @@ class CursorModel with ChangeNotifier {
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
kMouseControlTimeoutMSec;
bool isConnIn2Secs() {
if (_firstUpdateMouseTime == null) {
_firstUpdateMouseTime = DateTime.now();
return true;
} else {
return DateTime.now().difference(_firstUpdateMouseTime!).inSeconds < 2;
}
}
CursorModel(this.parent);
Set<String> get cachedKeys => _cacheKeys;
@@ -1065,9 +1106,9 @@ class CursorModel with ChangeNotifier {
Future<bool> _updateCache(
Uint8List rgba, ui.Image image, int id, int w, int h) async {
Uint8List? data;
img2.Image? imgOrigin;
img2.Image imgOrigin =
img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba);
if (Platform.isWindows) {
imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba);
data = imgOrigin.getBytes(format: img2.Format.bgra);
} else {
ByteData? imgBytes =
@@ -1109,8 +1150,10 @@ class CursorModel with ChangeNotifier {
/// Update the cursor position.
updateCursorPosition(Map<String, dynamic> evt, String id) async {
gotMouseControl = false;
_lastPeerMouse = DateTime.now();
if (!isConnIn2Secs()) {
gotMouseControl = false;
_lastPeerMouse = DateTime.now();
}
_x = double.parse(evt['x']);
_y = double.parse(evt['y']);
try {
@@ -1265,8 +1308,9 @@ class FFI {
late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global
late final PeerTabModel peerTabModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording
late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session
FFI() {
@@ -1278,6 +1322,7 @@ class FFI {
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
peerTabModel = PeerTabModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));

View File

@@ -8,6 +8,7 @@ import 'package:external_path/external_path.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win32/win32.dart' as win32;
@@ -46,6 +47,8 @@ class PlatformFFI {
static get localeName => Platform.localeName;
static get isMain => instance._appType == kAppTypeMain;
static Future<String> getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version;
@@ -112,8 +115,15 @@ class PlatformFFI {
}
_ffiBind = RustdeskImpl(dylib);
if (Platform.isLinux) {
// start dbus service, no need to await
await _ffiBind.mainStartDbusServer();
// Start a dbus service, no need to await
_ffiBind.mainStartDbusServer();
} else if (Platform.isMacOS && isMain) {
Future.wait([
// Start dbus service.
_ffiBind.mainStartDbusServer(),
// Start local audio pulseaudio server.
_ffiBind.mainStartPa()
]);
}
_startListenEvent(_ffiBind); // global event
try {

View File

@@ -0,0 +1,275 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:scroll_pos/scroll_pos.dart';
import '../common.dart';
import 'model.dart';
const int groupTabIndex = 4;
const String defaultGroupTabname = 'Group';
class PeerTabModel with ChangeNotifier {
WeakReference<FFI> parent;
int get currentTab => _currentTab;
int _currentTab = 0; // index in tabNames
List<int> get visibleOrderedTabs => _visibleOrderedTabs;
List<int> _visibleOrderedTabs = List.empty(growable: true);
List<int> get tabOrder => _tabOrder;
List<int> _tabOrder = List.from([0, 1, 2, 3, 4]); // constant length
int get tabHiddenFlag => _tabHiddenFlag;
int _tabHiddenFlag = 0;
bool get showScrollBtn => _showScrollBtn;
bool _showScrollBtn = false;
final List<bool> _fullyVisible = List.filled(5, false);
bool get leftFullyVisible => _leftFullyVisible;
bool _leftFullyVisible = false;
bool get rightFullyVisible => _rightFullyVisible;
bool _rightFullyVisible = false;
ScrollPosController sc = ScrollPosController();
List<String> tabNames = [
'Recent Sessions',
'Favorites',
'Discovered',
'Address Book',
defaultGroupTabname,
];
PeerTabModel(this.parent) {
// init tabHiddenFlag
_tabHiddenFlag = int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0;
var tabs = _notHiddenTabs();
// remove dynamic tabs
tabs.remove(groupTabIndex);
// init tabOrder
try {
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
if (conf.isNotEmpty) {
final json = jsonDecode(conf);
if (json is List) {
final List<int> list =
json.map((e) => int.tryParse(e.toString()) ?? -1).toList();
if (list.length == _tabOrder.length &&
_tabOrder.every((e) => list.contains(e))) {
_tabOrder = list;
}
}
}
} catch (e) {
debugPrintStack(label: '$e');
}
// init visibleOrderedTabs
var tempList = _tabOrder.toList();
tempList.removeWhere((e) => !tabs.contains(e));
_visibleOrderedTabs = tempList;
// init currentTab
_currentTab =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabs.contains(_currentTab)) {
if (tabs.isNotEmpty) {
_currentTab = tabs[0];
} else {
_currentTab = 0;
}
}
sc.itemCount = _visibleOrderedTabs.length;
}
check_dynamic_tabs() {
var visible = visibleTabs();
_visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList();
if (_visibleOrderedTabs.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
_currentTab = groupTabIndex;
}
if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = defaultGroupTabname;
}
sc.itemCount = _visibleOrderedTabs.length;
notifyListeners();
}
setCurrentTab(int index) {
if (_currentTab != index) {
_currentTab = index;
notifyListeners();
}
}
setTabFullyVisible(int index, bool visible) {
if (index >= 0 && index < _fullyVisible.length) {
if (visible != _fullyVisible[index]) {
_fullyVisible[index] = visible;
bool changed = false;
bool show = _visibleOrderedTabs.any((e) => !_fullyVisible[e]);
if (show != _showScrollBtn) {
_showScrollBtn = show;
changed = true;
}
if (_visibleOrderedTabs.isNotEmpty && _visibleOrderedTabs[0] == index) {
if (_leftFullyVisible != visible) {
_leftFullyVisible = visible;
changed = true;
}
}
if (_visibleOrderedTabs.isNotEmpty &&
_visibleOrderedTabs.last == index) {
if (_rightFullyVisible != visible) {
_rightFullyVisible = visible;
changed = true;
}
}
if (changed) {
notifyListeners();
}
}
}
}
onReorder(oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var list = _visibleOrderedTabs.toList();
final int item = list.removeAt(oldIndex);
list.insert(newIndex, item);
_visibleOrderedTabs = list;
var tmpTabOrder = _visibleOrderedTabs.toList();
var left = _tabOrder.where((e) => !tmpTabOrder.contains(e)).toList();
for (var t in left) {
_addTabInOrder(tmpTabOrder, t);
}
_tabOrder = tmpTabOrder;
bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder));
notifyListeners();
}
onHideShow(int index, bool show) async {
int bitMask = 1 << index;
if (show) {
_tabHiddenFlag &= ~bitMask;
} else {
_tabHiddenFlag |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card', v: _tabHiddenFlag.toRadixString(2));
var visible = visibleTabs();
_visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList();
if (_visibleOrderedTabs.isNotEmpty &&
!_visibleOrderedTabs.contains(_currentTab)) {
_currentTab = _visibleOrderedTabs[0];
}
notifyListeners();
}
List<int> orderedNotFilteredTabs() {
var list = tabOrder.toList();
if (_filterGroupCard()) {
list.remove(groupTabIndex);
}
return list;
}
// return index array of tabNames
List<int> visibleTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i) && !_isTabFilter(i)) {
v.add(i);
}
}
return v;
}
String translatedTabname(int index) {
if (index >= 0 && index < tabNames.length) {
final name = tabNames[index];
if (index == groupTabIndex) {
if (name == defaultGroupTabname) {
return translate(name);
} else {
return name;
}
} else {
return translate(name);
}
}
assert(false);
return index.toString();
}
bool _isTabHidden(int tabindex) {
return _tabHiddenFlag & (1 << tabindex) != 0;
}
bool _isTabFilter(int tabIndex) {
if (tabIndex == groupTabIndex) {
return _filterGroupCard();
}
return false;
}
// return true if hide group card
bool _filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
List<int> _notHiddenTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i)) {
v.add(i);
}
}
return v;
}
// add tabIndex to list
_addTabInOrder(List<int> list, int tabIndex) {
if (!_tabOrder.contains(tabIndex) || list.contains(tabIndex)) {
return;
}
bool sameOrder = true;
int lastIndex = -1;
for (int i = 0; i < list.length; i++) {
var index = _tabOrder.lastIndexOf(list[i]);
if (index > lastIndex) {
lastIndex = index;
continue;
} else {
sameOrder = false;
break;
}
}
if (sameOrder) {
var indexInTabOrder = _tabOrder.indexOf(tabIndex);
var left = List.empty(growable: true);
for (int i = 0; i < indexInTabOrder; i++) {
left.add(_tabOrder[i]);
}
int insertIndex = list.lastIndexWhere((e) => left.contains(e));
if (insertIndex < 0) {
insertIndex = 0;
} else {
insertIndex += 1;
}
list.insert(insertIndex, tabIndex);
} else {
list.add(tabIndex);
}
}
}

View File

@@ -579,6 +579,26 @@ class ServerModel with ChangeNotifier {
notifyListeners();
}
}
void updateVoiceCallState(Map<String, dynamic> evt) {
try {
final client = Client.fromJson(jsonDecode(evt["client"]));
final index = _clients.indexWhere((element) => element.id == client.id);
if (index != -1) {
_clients[index].inVoiceCall = client.inVoiceCall;
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
if (client.incomingVoiceCall) {
// Has incoming phone call, let's set the window on top.
Future.delayed(Duration.zero, () {
window_on_top(null);
});
}
notifyListeners();
}
} catch (e) {
debugPrint("updateVoiceCallState failed: $e");
}
}
}
enum ClientType {
@@ -602,6 +622,8 @@ class Client {
bool recording = false;
bool disconnected = false;
bool fromSwitch = false;
bool inVoiceCall = false;
bool incomingVoiceCall = false;
RxBool hasUnreadChatMessage = false.obs;
@@ -623,6 +645,8 @@ class Client {
recording = json['recording'];
disconnected = json['disconnected'];
fromSwitch = json['from_switch'];
inVoiceCall = json['in_voice_call'];
incomingVoiceCall = json['incoming_voice_call'];
}
Map<String, dynamic> toJson() {

View File

@@ -62,7 +62,7 @@ class UserModel {
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
statePeerTab.check();
gFFI.peerTabModel.check_dynamic_tabs();
}
Future<void> _parseAndUpdateUser(UserPayload user) async {

View File

@@ -160,6 +160,24 @@ class RustDeskMultiWindowManager {
return null;
}
void clearWindowType(WindowType type) {
switch (type) {
case WindowType.Main:
return;
case WindowType.RemoteDesktop:
_remoteDesktopWindowId = null;
break;
case WindowType.FileTransfer:
_fileTransferWindowId = null;
break;
case WindowType.PortForward:
_portForwardWindowId = null;
break;
case WindowType.Unknown:
break;
}
}
void setMethodHandler(
Future<dynamic> Function(MethodCall call, int fromWindowId)? handler) {
DesktopMultiWindow.setMethodHandler(handler);
@@ -186,8 +204,11 @@ class RustDeskMultiWindowManager {
}
await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close();
} on Error {
} catch (e) {
debugPrint("$e");
return;
} finally {
clearWindowType(type);
}
}
}

View File

@@ -31,4 +31,10 @@ class RdPlatformChannel {
return _osxMethodChannel
.invokeMethod("setWindowTheme", {"themeName": theme.name});
}
/// Terminate .app manually.
Future<void> terminate() {
assert(Platform.isMacOS);
return _osxMethodChannel.invokeMethod("terminate");
}
}

View File

@@ -227,7 +227,7 @@
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
LastSwiftMigration = 1420;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
@@ -463,6 +463,7 @@
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Profile;
@@ -607,6 +608,7 @@
MACOSX_DEPLOYMENT_TARGET = 10.14;
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -643,6 +645,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;
SWIFT_VERSION = 5.0;
};

View File

@@ -3,21 +3,22 @@ import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
var lauched = false;
var launched = false;
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
dummy_method_to_enforce_bundling()
return true
// https://github.com/leanflutter/window_manager/issues/214
return false
}
override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
if (lauched) {
if (launched) {
handle_applicationShouldOpenUntitledFile();
}
return true
}
override func applicationDidFinishLaunching(_ aNotification: Notification) {
lauched = true;
launched = true;
NSApplication.shared.activate(ignoringOtherApps: true);
}
}

View File

@@ -1,68 +1,68 @@
{
"images": [
{
"filename": "app_icon_16.png",
"idiom": "mac",
"scale": "1x",
"size": "16x16"
"info": {
"author": "icons_launcher",
"version": 1
},
{
"filename": "app_icon_32.png",
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"filename": "app_icon_32.png",
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"filename": "app_icon_64.png",
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"filename": "app_icon_128.png",
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"filename": "app_icon_256.png",
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"filename": "app_icon_256.png",
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"filename": "app_icon_512.png",
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"filename": "app_icon_512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"filename": "app_icon_1024.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "icons_launcher",
"version": 1
}
"images": [
{
"size": "16x16",
"idiom": "mac",
"filename": "app_icon_16.png",
"scale": "1x"
},
{
"size": "16x16",
"idiom": "mac",
"filename": "app_icon_32.png",
"scale": "2x"
},
{
"size": "32x32",
"idiom": "mac",
"filename": "app_icon_32.png",
"scale": "1x"
},
{
"size": "32x32",
"idiom": "mac",
"filename": "app_icon_64.png",
"scale": "2x"
},
{
"size": "128x128",
"idiom": "mac",
"filename": "app_icon_128.png",
"scale": "1x"
},
{
"size": "128x128",
"idiom": "mac",
"filename": "app_icon_256.png",
"scale": "2x"
},
{
"size": "256x256",
"idiom": "mac",
"filename": "app_icon_256.png",
"scale": "1x"
},
{
"size": "256x256",
"idiom": "mac",
"filename": "app_icon_512.png",
"scale": "2x"
},
{
"size": "512x512",
"idiom": "mac",
"filename": "app_icon_512.png",
"scale": "1x"
},
{
"size": "512x512",
"idiom": "mac",
"filename": "app_icon_1024.png",
"scale": "2x"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -23,8 +23,10 @@
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<key>CFBundleURLIconFile</key>
<string></string>
<key>CFBundleURLName</key>
<string>com.carriez.rustdesk</string>
<key>CFBundleURLSchemes</key>
<array>
<string>rustdesk</string>
@@ -35,13 +37,15 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSMicrophoneUsageDescription</key>
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -1,4 +1,5 @@
import Cocoa
import AVFoundation
import FlutterMacOS
import desktop_multi_window
// import bitsdojo_window_macos
@@ -78,10 +79,29 @@ class MainFlutterWindow: NSWindow {
self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light")
result(nil)
break;
case "terminate":
NSApplication.shared.terminate(self)
result(nil)
case "canRecordAudio":
switch AVCaptureDevice.authorizationStatus(for: .audio) {
case .authorized:
result(1)
break
case .notDetermined:
result(0)
break
default:
result(-1)
break
}
case "requestRecordAudio":
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
result(granted)
})
break
default:
result(FlutterMethodNotImplemented)
}
})
}
}

View File

@@ -475,10 +475,10 @@ packages:
dependency: "direct main"
description:
name: flutter_custom_cursor
sha256: "6c5204cf6a16650355b8aa47a8402e79922c07641390a32021a1069b561909ec"
sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
version: "0.0.4"
flutter_improved_scrolling:
dependency: "direct main"
description:
@@ -488,6 +488,14 @@ packages:
url: "https://github.com/Kingtous/flutter_improved_scrolling"
source: git
version: "0.0.3"
flutter_launcher_icons:
dependency: "direct main"
description:
name: flutter_launcher_icons
sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c
url: "https://pub.dev"
source: hosted
version: "0.11.0"
flutter_lints:
dependency: "direct dev"
description:

View File

@@ -90,6 +90,7 @@ dependencies:
bot_toast: ^4.0.3
win32: any
password_strength: ^0.2.0
flutter_launcher_icons: ^0.11.0
dev_dependencies:
@@ -101,21 +102,21 @@ dev_dependencies:
flutter_lints: ^2.0.0
ffigen: ^7.2.4
# rerun: flutter pub run flutter_launcher_icons:main
icons_launcher:
# rerun: flutter pub run flutter_launcher_icons
flutter_icons:
image_path: "../res/icon.png"
platforms:
android:
enable: true
ios:
enable: true
windows:
enable: true
macos:
enable: true
image_path: "../res/mac-icon.png"
linux:
enable: true
remove_alpha_ios: true
android: true
ios: true
windows:
generate: true
macos:
image_path: "../res/mac-icon.png"
generate: true
linux: true
web:
generate: true
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -32,4 +32,4 @@
"purpose": "maskable"
}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB