Merge pull request #1404 from fufesou/flutter_desktop_new_remote_menu

Flutter desktop new remote menu
This commit is contained in:
RustDesk 2022-08-30 23:31:20 +08:00 committed by GitHub
commit 694896abda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 672 additions and 301 deletions

View File

@ -743,39 +743,3 @@ Future<List<Peer>>? matchPeers(String searchText, List<Peer> peers) async {
}
return filteredList;
}
class PrivacyModeState {
static String tag(String id) => 'privacy_mode_' + id;
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class BlockInputState {
static String tag(String id) => 'block_input_' + id;
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class CurrentDisplayState {
static String tag(String id) => 'current_display_' + id;
static void init(String id) {
final RxInt state = RxInt(0);
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
}

View File

@ -0,0 +1,87 @@
import 'package:get/get.dart';
import '../consts.dart';
class PrivacyModeState {
static String tag(String id) => 'privacy_mode_$id';
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class BlockInputState {
static String tag(String id) => 'block_input_$id';
static void init(String id) {
final RxBool state = false.obs;
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class CurrentDisplayState {
static String tag(String id) => 'current_display_$id';
static void init(String id) {
final RxInt state = RxInt(0);
Get.put(state, tag: tag(id));
}
static void delete(String id) => Get.delete(tag: tag(id));
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
}
class ConnectionType {
final Rx<String> _secure = kInvalidValueStr.obs;
final Rx<String> _direct = kInvalidValueStr.obs;
Rx<String> get secure => _secure;
Rx<String> get direct => _direct;
static String get strSecure => 'secure';
static String get strInsecure => 'insecure';
static String get strDirect => '';
static String get strIndirect => '_relay';
void setSecure(bool v) {
_secure.value = v ? strSecure : strInsecure;
}
void setDirect(bool v) {
_direct.value = v ? strDirect : strIndirect;
}
bool isValid() {
return _secure.value != kInvalidValueStr &&
_direct.value != kInvalidValueStr;
}
}
class ConnectionTypeState {
static String tag(String id) => 'connection_type_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final ConnectionType collectionType = ConnectionType();
Get.put(collectionType, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static ConnectionType find(String id) =>
Get.find<ConnectionType>(tag: tag(id));
}

View File

@ -10,3 +10,5 @@ const String kTabLabelSettingPage = "Settings";
const int kDefaultDisplayWidth = 1280;
const int kDefaultDisplayHeight = 720;
const kInvalidValueStr = "InvalidValueStr";

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
@ -20,22 +21,24 @@ class ConnectionTabPage extends StatefulWidget {
class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabController = Get.put(DesktopTabController());
static final IconData selectedIcon = Icons.desktop_windows_sharp;
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
if (params['id'] != null) {
final peerId = params['id'];
if (peerId != null) {
ConnectionTypeState.init(peerId);
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
key: peerId,
label: peerId,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
page: Obx(() => RemotePage(
key: ValueKey(params['id']),
id: params['id'],
key: ValueKey(peerId),
id: peerId,
tabBarHeight:
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
))));
@ -103,6 +106,44 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
.setFullscreen(fullscreen.isTrue);
return pageView;
},
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!ConnectionTypeState.find(key).isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
final msgDirect = translate(
connectionType.direct.value ==
ConnectionType.strDirect
? 'Direct Connection'
: 'Relay Connection');
final msgSecure = translate(
connectionType.secure.value ==
ConnectionType.strSecure
? 'Secure Connection'
: 'Insecure Connection');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgDirect\n$msgSecure',
child: Image.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
],
);
}
}),
))),
),
));
@ -112,6 +153,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
if (tabController.state.value.tabs.isEmpty) {
WindowController.fromWindowId(windowId()).hide();
}
ConnectionTypeState.delete(id);
}
int windowId() {

View File

@ -5,11 +5,9 @@ import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
import 'package:tuple/tuple.dart';
// import 'package:window_manager/window_manager.dart';
@ -19,6 +17,8 @@ import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/chat_model.dart';
import '../../common/shared_state.dart';
final initText = '\1' * 1024;
@ -41,7 +41,7 @@ class _RemotePageState extends State<RemotePage>
Timer? _timer;
bool _showBar = !isWebDesktop;
String _value = '';
var _cursorOverImage = false.obs;
final _cursorOverImage = false.obs;
final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode();
@ -54,6 +54,18 @@ class _RemotePageState extends State<RemotePage>
_ffi.canvasModel.tabBarHeight = widget.tabBarHeight;
}
void _initStates(String id) {
PrivacyModeState.init(id);
BlockInputState.init(id);
CurrentDisplayState.init(id);
}
void _removeStates(String id) {
PrivacyModeState.delete(id);
BlockInputState.delete(id);
CurrentDisplayState.delete(id);
}
@override
void initState() {
super.initState();
@ -74,14 +86,12 @@ class _RemotePageState extends State<RemotePage>
_ffi.listenToMouse(true);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// WindowManager.instance.addListener(this);
PrivacyModeState.init(widget.id);
BlockInputState.init(widget.id);
CurrentDisplayState.init(widget.id);
_initStates(widget.id);
}
@override
void dispose() {
print("REMOTE PAGE dispose ${widget.id}");
debugPrint("REMOTE PAGE dispose ${widget.id}");
hideMobileActionsOverlay();
_ffi.listenToMouse(false);
_mobileFocusNode.dispose();
@ -97,9 +107,7 @@ class _RemotePageState extends State<RemotePage>
// WindowManager.instance.removeListener(this);
Get.delete<FFI>(tag: widget.id);
super.dispose();
PrivacyModeState.delete(widget.id);
BlockInputState.delete(widget.id);
CurrentDisplayState.delete(widget.id);
_removeStates(widget.id);
}
void resetTool() {

View File

@ -4,12 +4,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tuple/tuple.dart';
import './material_mod_popup_menu.dart' as modMenu;
const kInvalidValueStr = "InvalidValueStr";
import './material_mod_popup_menu.dart' as mod_menu;
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu
class PopupMenuChildrenItem<T> extends modMenu.PopupMenuEntry<T> {
class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
const PopupMenuChildrenItem({
key,
this.height = kMinInteractiveDimension,
@ -17,19 +15,19 @@ class PopupMenuChildrenItem<T> extends modMenu.PopupMenuEntry<T> {
this.enable = true,
this.textStyle,
this.onTap,
this.position = modMenu.PopupMenuPosition.overSide,
this.position = mod_menu.PopupMenuPosition.overSide,
this.offset = Offset.zero,
required this.itemBuilder,
required this.child,
}) : super(key: key);
final modMenu.PopupMenuPosition position;
final mod_menu.PopupMenuPosition position;
final Offset offset;
final TextStyle? textStyle;
final EdgeInsets? padding;
final bool enable;
final void Function()? onTap;
final List<modMenu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child;
@override
@ -59,7 +57,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!;
return modMenu.PopupMenuButton<T>(
return mod_menu.PopupMenuButton<T>(
enabled: widget.enable,
position: widget.position,
offset: widget.offset,
@ -88,22 +86,29 @@ class MenuConfig {
static const iconWidth = 12.0;
static const iconHeight = 12.0;
final double secondMenuHeight;
final double height;
final double dividerHeight;
final Color commonColor;
const MenuConfig(
{required this.commonColor,
this.secondMenuHeight = kMinInteractiveDimension});
this.height = kMinInteractiveDimension,
this.dividerHeight = 16.0});
}
abstract class MenuEntryBase<T> {
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf);
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
}
class MenuEntryDivider<T> extends MenuEntryBase<T> {
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return const modMenu.PopupMenuDivider();
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
mod_menu.PopupMenuDivider(
height: conf.dividerHeight,
)
];
}
}
@ -111,6 +116,85 @@ typedef RadioOptionsGetter = List<Tuple2<String, String>> Function();
typedef RadioCurOptionGetter = Future<String> Function();
typedef RadioOptionSetter = Future<void> Function(String);
class MenuEntryRadioUtils<T> {}
class MenuEntryRadios<T> extends MenuEntryBase<T> {
final String text;
final RadioOptionsGetter optionsGetter;
final RadioCurOptionGetter curOptionGetter;
final RadioOptionSetter optionSetter;
final RxString _curOption = "".obs;
MenuEntryRadios(
{required this.text,
required this.optionsGetter,
required this.curOptionGetter,
required this.optionSetter}) {
() async {
_curOption.value = await curOptionGetter();
}();
}
List<Tuple2<String, String>> get options => optionsGetter();
RxString get curOption => _curOption;
setOption(String option) async {
await optionSetter(option);
final opt = await curOptionGetter();
if (_curOption.value != opt) {
_curOption.value = opt;
}
}
mod_menu.PopupMenuEntry<T> _buildMenuItem(
BuildContext context, MenuConfig conf, Tuple2<String, String> opt) {
return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: Row(
children: [
Text(
opt.item1,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 20.0,
height: 20.0,
child: Obx(() => opt.item2 == curOption.value
? Icon(
Icons.check,
color: conf.commonColor,
)
: const SizedBox.shrink())),
)),
],
),
),
onPressed: () {
if (opt.item2 != curOption.value) {
setOption(opt.item2);
}
},
),
);
}
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return options.map((opt) => _buildMenuItem(context, conf, opt)).toList();
}
}
class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
final String text;
final RadioOptionsGetter optionsGetter;
@ -138,33 +222,37 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
}
}
modMenu.PopupMenuEntry<T> _buildSecondMenu(
mod_menu.PopupMenuEntry<T> _buildSecondMenu(
BuildContext context, MenuConfig conf, Tuple2<String, String> opt) {
return modMenu.PopupMenuItem(
return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
constraints: BoxConstraints(minHeight: conf.height),
child: Row(
children: [
SizedBox(
width: 20.0,
height: 20.0,
child: Obx(() => opt.item2 == curOption.value
? Icon(
Icons.check,
color: conf.commonColor,
)
: SizedBox.shrink())),
const SizedBox(width: MenuConfig.midPadding),
Text(
opt.item1,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 20.0,
height: 20.0,
child: Obx(() => opt.item2 == curOption.value
? Icon(
Icons.check,
color: conf.commonColor,
)
: const SizedBox.shrink())),
)),
],
),
),
@ -178,31 +266,34 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
}
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return PopupMenuChildrenItem(
height: conf.secondMenuHeight,
padding: EdgeInsets.zero,
itemBuilder: (BuildContext context) =>
options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
PopupMenuChildrenItem(
padding: EdgeInsets.zero,
height: conf.height,
itemBuilder: (BuildContext context) =>
options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
))
]),
);
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
),
))
]),
)
];
}
}
@ -218,34 +309,38 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
Future<void> setOption(bool option);
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return modMenu.PopupMenuItem(
padding: EdgeInsets.zero,
child: Obx(
() => SwitchListTile(
value: curOption.value,
onChanged: (v) {
setOption(v);
},
title: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
child: Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
dense: true,
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: Obx(
() => SwitchListTile(
value: curOption.value,
onChanged: (v) {
setOption(v);
},
title: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
dense: true,
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
contentPadding: const EdgeInsets.only(left: 8.0),
),
contentPadding: EdgeInsets.only(left: 8.0),
),
),
);
)
];
}
}
@ -303,32 +398,37 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
});
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return PopupMenuChildrenItem(
height: conf.secondMenuHeight,
padding: EdgeInsets.zero,
position: modMenu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) =>
entries.map((entry) => entry.build(context, conf)).toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
PopupMenuChildrenItem(
height: conf.height,
padding: EdgeInsets.zero,
position: mod_menu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) => entries
.map((entry) => entry.build(context, conf))
.expand((i) => i)
.toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Text(
text,
style: const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
))
]),
);
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.keyboard_arrow_right,
color: conf.commonColor,
),
))
]),
)
];
}
}
@ -342,34 +442,27 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
});
@override
modMenu.PopupMenuEntry<T> build(BuildContext context, MenuConfig conf) {
return modMenu.PopupMenuItem(
padding: EdgeInsets.zero,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
child: childBuilder(
const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
onPressed: () {
proc();
},
),
);
}
}
class CustomMenu<T> {
final List<MenuEntryBase<T>> entries;
final MenuConfig conf;
const CustomMenu({required this.entries, required this.conf});
List<modMenu.PopupMenuEntry<T>> build(BuildContext context) {
return entries.map((entry) => entry.build(context, conf)).toList();
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: childBuilder(
const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
)),
onPressed: () {
proc();
},
),
)
];
}
}

View File

@ -9,12 +9,15 @@ import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './material_mod_popup_menu.dart' as mod_menu;
class _MenubarTheme {
static const Color commonColor = MyTheme.accent;
static const double height = kMinInteractiveDimension;
// kMinInteractiveDimension
static const double height = 24.0;
static const double dividerHeight = 12.0;
}
class RemoteMenubar extends StatefulWidget {
@ -168,11 +171,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
),
itemBuilder: (BuildContext context) {
final List<Widget> rowChildren = [];
const double selectorScale = 1.3;
for (int i = 0; i < pi.displays.length; i++) {
rowChildren.add(Transform.scale(
scale: selectorScale,
child: Stack(
rowChildren.add(
Stack(
alignment: Alignment.center,
children: [
const Icon(
@ -203,7 +204,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
)
],
),
));
);
}
return <mod_menu.PopupMenuEntry<String>>[
mod_menu.PopupMenuItem<String>(
@ -232,8 +233,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
secondMenuHeight: _MenubarTheme.height,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
@ -253,8 +256,10 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
context,
const MenuConfig(
commonColor: _MenubarTheme.commonColor,
secondMenuHeight: _MenubarTheme.height,
height: _MenubarTheme.height,
dividerHeight: _MenubarTheme.dividerHeight,
)))
.expand((i) => i)
.toList(),
);
}
@ -398,7 +403,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
List<MenuEntryBase<String>> _getDisplayMenu() {
final displayMenu = [
MenuEntrySubRadios<String>(
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () => [
Tuple2<String, String>(translate('Original'), 'original'),
@ -415,7 +420,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
id: widget.id, name: "view-style", value: v);
widget.ffi.canvasModel.updateViewStyle();
}),
MenuEntrySubRadios<String>(
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
text: translate('Scroll Style'),
optionsGetter: () => [
Tuple2<String, String>(translate('ScrollAuto'), 'scrollauto'),
@ -431,7 +437,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
id: widget.id, name: "scroll-style", value: v);
widget.ffi.canvasModel.updateScrollStyle();
}),
MenuEntrySubRadios<String>(
MenuEntryDivider<String>(),
MenuEntryRadios<String>(
text: translate('Image Quality'),
optionsGetter: () => [
Tuple2<String, String>(translate('Good image quality'), 'best'),
@ -448,6 +455,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
optionSetter: (String v) async {
await bind.sessionSetImageQuality(id: widget.id, value: v);
}),
MenuEntryDivider<String>(),
MenuEntrySwitch<String>(
text: translate('Show remote cursor'),
getter: () async {

View File

@ -121,6 +121,16 @@ class DesktopTabController {
}
}
class TabThemeConf {
double iconSize;
TarBarTheme theme;
TabThemeConf({required this.iconSize, required this.theme});
}
typedef TabBuilder = Widget Function(
String key, Widget icon, Widget label, TabThemeConf themeConf);
typedef LabelGetter = Rx<String> Function(String key);
class DesktopTab extends StatelessWidget {
final Function(String)? onTabClose;
final TarBarTheme theme;
@ -134,24 +144,29 @@ class DesktopTab extends StatelessWidget {
final Widget Function(Widget pageView)? pageViewBuilder;
final Widget? tail;
final VoidCallback? onClose;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
final DesktopTabController controller;
Rx<DesktopTabState> get state => controller.state;
const DesktopTab(
{required this.controller,
required this.isMainWindow,
this.theme = const TarBarTheme.light(),
this.onTabClose,
this.showTabBar = true,
this.showLogo = true,
this.showTitle = true,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
this.pageViewBuilder,
this.tail,
this.onClose});
const DesktopTab({
required this.controller,
required this.isMainWindow,
this.theme = const TarBarTheme.light(),
this.onTabClose,
this.showTabBar = true,
this.showLogo = true,
this.showTitle = true,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
this.pageViewBuilder,
this.tail,
this.onClose,
this.tabBuilder,
this.labelGetter,
});
@override
Widget build(BuildContext context) {
@ -194,8 +209,10 @@ class DesktopTab extends StatelessWidget {
child: Row(
children: [
Offstage(
offstage: !Platform.isMacOS,
child: const SizedBox(width: 78,)),
offstage: !Platform.isMacOS,
child: const SizedBox(
width: 78,
)),
Row(children: [
Offstage(
offstage: !showLogo,
@ -228,6 +245,8 @@ class DesktopTab extends StatelessWidget {
controller: controller,
onTabClose: onTabClose,
theme: theme,
tabBuilder: tabBuilder,
labelGetter: labelGetter,
)),
),
],
@ -356,10 +375,18 @@ class _ListView extends StatelessWidget {
final DesktopTabController controller;
final Function(String key)? onTabClose;
final TarBarTheme theme;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
Rx<DesktopTabState> get state => controller.state;
const _ListView(
{required this.controller, required this.onTabClose, required this.theme});
_ListView(
{required this.controller,
required this.onTabClose,
required this.theme,
this.tabBuilder,
this.labelGetter});
@override
Widget build(BuildContext context) {
@ -373,7 +400,9 @@ class _ListView extends StatelessWidget {
final tab = e.value;
return _Tab(
index: index,
label: tab.label,
label: labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label),
selectedIcon: tab.selectedIcon,
unselectedIcon: tab.unselectedIcon,
closable: tab.closable,
@ -381,6 +410,16 @@ class _ListView extends StatelessWidget {
onClose: () => controller.remove(index),
onSelected: () => controller.jumpTo(index),
theme: theme,
tabBuilder: tabBuilder == null
? null
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
return tabBuilder!(
tab.label,
icon,
labelWidget,
themeConf,
);
},
);
}).toList()));
}
@ -388,7 +427,7 @@ class _ListView extends StatelessWidget {
class _Tab extends StatelessWidget {
late final int index;
late final String label;
late final Rx<String> label;
late final IconData? selectedIcon;
late final IconData? unselectedIcon;
late final bool closable;
@ -397,6 +436,8 @@ class _Tab extends StatelessWidget {
late final Function() onSelected;
final RxBool _hover = false.obs;
late final TarBarTheme theme;
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
tabBuilder;
_Tab(
{Key? key,
@ -404,6 +445,7 @@ class _Tab extends StatelessWidget {
required this.label,
this.selectedIcon,
this.unselectedIcon,
this.tabBuilder,
required this.closable,
required this.selected,
required this.onClose,
@ -411,11 +453,49 @@ class _Tab extends StatelessWidget {
required this.theme})
: super(key: key);
Widget _buildTabContent() {
bool showIcon = selectedIcon != null && unselectedIcon != null;
bool isSelected = index == selected;
final icon = Offstage(
offstage: !showIcon,
child: Icon(
isSelected ? selectedIcon : unselectedIcon,
size: _kIconSize,
color: isSelected
? theme.selectedtabIconColor
: theme.unSelectedtabIconColor,
).paddingOnly(right: 5));
final labelWidget = Obx(() {
return Text(
translate(label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? theme.selectedTextColor
: theme.unSelectedTextColor),
);
});
if (tabBuilder == null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
labelWidget,
],
);
} else {
return tabBuilder!(
icon, labelWidget, TabThemeConf(iconSize: _kIconSize, theme: theme));
}
}
@override
Widget build(BuildContext context) {
bool show_icon = selectedIcon != null && unselectedIcon != null;
bool is_selected = index == selected;
bool show_divider = index != selected - 1 && index != selected;
bool showIcon = selectedIcon != null && unselectedIcon != null;
bool isSelected = index == selected;
bool showDivider = index != selected - 1 && index != selected;
return Ink(
child: InkWell(
onHover: (hover) => _hover.value = hover,
@ -427,40 +507,19 @@ class _Tab extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Offstage(
offstage: !show_icon,
child: Icon(
is_selected ? selectedIcon : unselectedIcon,
size: _kIconSize,
color: is_selected
? theme.selectedtabIconColor
: theme.unSelectedtabIconColor,
).paddingOnly(right: 5)),
Text(
translate(label),
textAlign: TextAlign.center,
style: TextStyle(
color: is_selected
? theme.selectedTextColor
: theme.unSelectedTextColor),
),
],
),
_buildTabContent(),
Offstage(
offstage: !closable,
child: Obx((() => _CloseButton(
visiable: _hover.value,
tabSelected: is_selected,
tabSelected: isSelected,
onClose: () => onClose(),
theme: theme,
))),
)
])).paddingSymmetric(horizontal: 10),
Offstage(
offstage: !show_divider,
offstage: !showDivider,
child: VerticalDivider(
width: 1,
indent: _kDividerIndent,

View File

@ -202,7 +202,7 @@ class App extends StatelessWidget {
title: 'RustDesk',
theme: getCurrentTheme(),
home: isDesktop
? DesktopTabPage()
? const DesktopTabPage()
: !isAndroid
? WebHomePage()
: HomePage(),

View File

@ -17,6 +17,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
import '../common.dart';
import '../common/shared_state.dart';
import '../mobile/widgets/dialog.dart';
import '../mobile/widgets/overlay.dart';
import 'peer_model.dart';
@ -96,25 +97,26 @@ class FfiModel with ChangeNotifier {
clearPermissions();
}
void setConnectionType(bool secure, bool direct) {
void setConnectionType(String peerId, bool secure, bool direct) {
_secure = secure;
_direct = direct;
try {
var connectionType = ConnectionTypeState.find(peerId);
connectionType.setSecure(secure);
connectionType.setDirect(direct);
} catch (e) {
//
}
}
Image? getConnectionImage() {
String? icon;
if (secure == true && direct == true) {
icon = 'secure';
} else if (secure == false && direct == true) {
icon = 'insecure';
} else if (secure == false && direct == false) {
icon = 'insecure_relay';
} else if (secure == true && direct == false) {
icon = 'secure_relay';
if (secure == null || direct == null) {
return null;
} else {
final icon =
'${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}';
return Image.asset('assets/$icon.png', width: 48, height: 48);
}
return icon == null
? null
: Image.asset('assets/$icon.png', width: 48, height: 48);
}
void clearPermissions() {
@ -130,7 +132,8 @@ class FfiModel with ChangeNotifier {
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true');
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
@ -189,7 +192,7 @@ class FfiModel with ChangeNotifier {
handlePeerInfo(evt, peerId);
} else if (name == 'connection_ready') {
parent.target?.ffiModel.setConnectionType(
evt['secure'] == 'true', evt['direct'] == 'true');
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
} else if (name == 'cursor_data') {
@ -1067,8 +1070,10 @@ class FFI {
imageModel._id = id;
cursorModel.id = id;
}
final stream = bind.sessionConnect(
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward);
final stream = bind.sessionStart(id: id);
final cb = ffiModel.startEventListener(id);
() async {
await for (final message in stream) {

View File

@ -40,10 +40,7 @@ dependencies:
url_launcher: ^6.0.9
shared_preferences: ^2.0.6
toggle_switch: ^1.4.0
dash_chat_2:
git:
url: https://github.com/fufesou/Dash-Chat-2
ref: feat_maxWidth
dash_chat_2: ^0.0.14
draggable_float_widget: ^0.0.2
settings_ui: ^2.0.2
flutter_breadcrumb: ^1.0.1

View File

@ -847,7 +847,7 @@ impl VideoHandler {
pub struct LoginConfigHandler {
id: String,
pub is_file_transfer: bool,
is_port_forward: bool,
pub is_port_forward: bool,
hash: Hash,
password: Vec<u8>, // remember password for reconnect
pub remember: bool,

View File

@ -8,16 +8,13 @@ use std::{
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
use hbb_common::config::{PeerConfig, TransferSerde};
use hbb_common::fs::get_job;
use hbb_common::{
allow_err,
allow_err, bail,
compress::decompress,
config::{Config, LocalConfig},
fs,
config::{Config, LocalConfig, PeerConfig, TransferSerde},
fs::{
can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path,
DigestCheckResult,
self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm,
transform_windows_path, DigestCheckResult,
},
log,
message_proto::*,
@ -28,7 +25,7 @@ use hbb_common::{
sync::mpsc,
time::{self, Duration, Instant, Interval},
},
Stream,
ResultType, Stream,
};
use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL};
@ -60,7 +57,7 @@ pub struct Session {
id: String,
sender: Arc<RwLock<Option<mpsc::UnboundedSender<Data>>>>, // UI to rust
lc: Arc<RwLock<LoginConfigHandler>>,
events2ui: Arc<RwLock<StreamSink<EventToUI>>>,
events2ui: Arc<RwLock<Option<StreamSink<EventToUI>>>>,
}
impl Session {
@ -71,23 +68,17 @@ impl Session {
/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+
/// * `is_file_transfer` - If the session is used for file transfer.
/// * `is_port_forward` - If the session is used for port forward.
pub fn start(
identifier: &str,
is_file_transfer: bool,
is_port_forward: bool,
events2ui: StreamSink<EventToUI>,
) {
pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> {
// TODO check same id
let session_id = get_session_id(identifier.to_owned());
let session_id = get_session_id(id.to_owned());
LocalConfig::set_remote_id(&session_id);
// TODO close
// Self::close();
let events2ui = Arc::new(RwLock::new(events2ui));
let session = Session {
id: session_id.clone(),
sender: Default::default(),
lc: Default::default(),
events2ui,
events2ui: Arc::new(RwLock::new(None)),
};
session.lc.write().unwrap().initialize(
session_id.clone(),
@ -97,10 +88,29 @@ impl Session {
SESSIONS
.write()
.unwrap()
.insert(identifier.to_owned(), session.clone());
std::thread::spawn(move || {
Connection::start(session, is_file_transfer, is_port_forward);
});
.insert(id.to_owned(), session.clone());
Ok(())
}
/// Create a new remote session with the given id.
///
/// # Arguments
///
/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+
/// * `events2ui` - The events channel to ui.
pub fn start(id: &str, events2ui: StreamSink<EventToUI>) -> ResultType<()> {
if let Some(session) = SESSIONS.write().unwrap().get_mut(id) {
*session.events2ui.write().unwrap() = Some(events2ui);
let session = session.clone();
std::thread::spawn(move || {
let is_file_transfer = session.lc.read().unwrap().is_file_transfer;
let is_port_forward = session.lc.read().unwrap().is_port_forward;
Connection::start(session, is_file_transfer, is_port_forward);
});
Ok(())
} else {
bail!("No session with peer id {}", id)
}
}
/// Get the current session instance.
@ -305,7 +315,9 @@ impl Session {
assert!(h.get("name").is_none());
h.insert("name", name);
let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned());
self.events2ui.read().unwrap().add(EventToUI::Event(out));
if let Some(stream) = &*self.events2ui.read().unwrap() {
stream.add(EventToUI::Event(out));
}
}
/// Get platform of peer.
@ -998,11 +1010,12 @@ impl Connection {
})
};
if let Ok(true) = self.video_handler.handle_frame(vf) {
let stream = self.session.events2ui.read().unwrap();
self.frame_count.fetch_add(1, Ordering::Relaxed);
stream.add(EventToUI::Rgba(ZeroCopyBuffer(
self.video_handler.rgb.clone(),
)));
if let Some(stream) = &*self.session.events2ui.read().unwrap() {
self.frame_count.fetch_add(1, Ordering::Relaxed);
stream.add(EventToUI::Rgba(ZeroCopyBuffer(
self.video_handler.rgb.clone(),
)));
}
}
}
Some(message::Union::Hash(hash)) => {

View File

@ -107,14 +107,18 @@ pub fn host_stop_system_key_propagate(stopped: bool) {
crate::platform::windows::stop_system_key_propagate(stopped);
}
pub fn session_connect(
events2ui: StreamSink<EventToUI>,
id: String,
is_file_transfer: bool,
is_port_forward: bool,
) -> ResultType<()> {
Session::start(&id, is_file_transfer, is_port_forward, events2ui);
Ok(())
// FIXME: -> ResultType<()> cannot be parsed by frb_codegen
// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25
pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn<String> {
if let Err(e) = Session::add(&id, is_file_transfer, is_port_forward) {
SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
} else {
SyncReturn("".to_owned())
}
}
pub fn session_start(events2ui: StreamSink<EventToUI>, id: String) -> ResultType<()> {
Session::start(&id, events2ui)
}
pub fn session_get_remember(id: String) -> Option<bool> {
@ -602,7 +606,12 @@ pub fn main_load_lan_peers() {
};
}
pub fn session_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) {
pub fn session_add_port_forward(
id: String,
local_port: i32,
remote_host: String,
remote_port: i32,
) {
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
session.add_port_forward(local_port, remote_host, remote_port);
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "滚屏方式"),
("Show Menubar", "显示菜单栏"),
("Hide Menubar", "隐藏菜单栏"),
("Direct Connection", "直接连接"),
("Relay Connection", "中继连接"),
("Secure Connection", "安全连接"),
("Insecure Connection", "非安全连接"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Štýl posúvania"),
("Show Menubar", "Zobrazit panel nabídek"),
("Hide Menubar", "skrýt panel nabídek"),
("Direct Connection", "Přímé spojení"),
("Relay Connection", "Připojení relé"),
("Secure Connection", "Zabezpečené připojení"),
("Insecure Connection", "Nezabezpečené připojení"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Rulstil"),
("Show Menubar", "Vis menulinje"),
("Hide Menubar", "skjul menulinjen"),
("Direct Connection", "Direkte forbindelse"),
("Relay Connection", "Relæforbindelse"),
("Secure Connection", "Sikker forbindelse"),
("Insecure Connection", "Usikker forbindelse"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Scroll-Stil"),
("Show Menubar", "Menüleiste anzeigen"),
("Hide Menubar", "Menüleiste ausblenden"),
("Direct Connection", "Direkte Verbindung"),
("Relay Connection", "Relaisverbindung"),
("Secure Connection", "Sichere Verbindung"),
("Insecure Connection", "Unsichere Verbindung"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Ruluma Stilo"),
("Show Menubar", "Montru menubreton"),
("Hide Menubar", "kaŝi menubreton"),
("Direct Connection", "Rekta Konekto"),
("Relay Connection", "Relajsa Konekto"),
("Secure Connection", "Sekura Konekto"),
("Insecure Connection", "Nesekura Konekto"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Estilo de desplazamiento"),
("Show Menubar", "ajustes de pantalla"),
("Hide Menubar", "ocultar barra de menú"),
("Direct Connection", "Conexión directa"),
("Relay Connection", "Conexión de relé"),
("Secure Connection", "Conexión segura"),
("Insecure Connection", "Conexión insegura"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Style de défilement"),
("Show Menubar", "Afficher la barre de menus"),
("Hide Menubar", "masquer la barre de menus"),
("Direct Connection", "Connexion directe"),
("Relay Connection", "Connexion relais"),
("Secure Connection", "Connexion sécurisée"),
("Insecure Connection", "Connexion non sécurisée"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Görgetési stílus"),
("Show Menubar", "Menüsor megjelenítése"),
("Hide Menubar", "menüsor elrejtése"),
("Direct Connection", "Közvetlen kapcsolat"),
("Relay Connection", "Relé csatlakozás"),
("Secure Connection", "Biztonságos kapcsolat"),
("Insecure Connection", "Nem biztonságos kapcsolat"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Gaya Gulir"),
("Show Menubar", "Tampilkan bilah menu"),
("Hide Menubar", "sembunyikan bilah menu"),
("Direct Connection", "Koneksi langsung"),
("Relay Connection", "Koneksi Relay"),
("Secure Connection", "Koneksi aman"),
("Insecure Connection", "Koneksi Tidak Aman"),
].iter().cloned().collect();
}

View File

@ -312,5 +312,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Stile di scorrimento"),
("Show Menubar", "Mostra la barra dei menu"),
("Hide Menubar", "nascondi la barra dei menu"),
("Direct Connection", "Connessione diretta"),
("Relay Connection", "Collegamento a relè"),
("Secure Connection", "Connessione sicura"),
("Insecure Connection", "Connessione insicura"),
].iter().cloned().collect();
}

View File

@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "スクロール スタイル"),
("Show Menubar", "メニューバーを表示"),
("Hide Menubar", "メニューバーを隠す"),
("Direct Connection", "直接接続"),
("Relay Connection", "リレー接続"),
("Secure Connection", "安全な接続"),
("Insecure Connection", "安全でない接続"),
].iter().cloned().collect();
}

View File

@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "스크롤 스타일"),
("Show Menubar", "메뉴 표시줄 표시"),
("Hide Menubar", "메뉴 표시줄 숨기기"),
("Direct Connection", "직접 연결"),
("Relay Connection", "릴레이 연결"),
("Secure Connection", "보안 연결"),
("Insecure Connection", "안전하지 않은 연결"),
].iter().cloned().collect();
}

View File

@ -314,5 +314,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Styl przewijania"),
("Show Menubar", "Pokaż pasek menu"),
("Hide Menubar", "ukryj pasek menu"),
("Direct Connection", "Bezpośrednie połączenie"),
("Relay Connection", "Połączenie przekaźnika"),
("Secure Connection", "Bezpieczne połączenie"),
("Insecure Connection", "Niepewne połączenie"),
].iter().cloned().collect();
}

View File

@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Estilo de rolagem"),
("Show Menubar", "Mostrar barra de menus"),
("Hide Menubar", "ocultar barra de menu"),
("Direct Connection", "Conexão direta"),
("Relay Connection", "Conexão de relé"),
("Secure Connection", "Conexão segura"),
("Insecure Connection", "Conexão insegura"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", ""),
("Show Menubar", ""),
("Hide Menubar", ""),
("Direct Connection", ""),
("Relay Connection", ""),
("Secure Connection", ""),
("Insecure Connection", ""),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Стиль прокрутки"),
("Show Menubar", "Показать строку меню"),
("Hide Menubar", "скрыть строку меню"),
("Direct Connection", "Прямая связь"),
("Relay Connection", "Релейное соединение"),
("Secure Connection", "Безопасное соединение"),
("Insecure Connection", "Небезопасное соединение"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Štýl posúvania"),
("Show Menubar", "Zobraziť panel s ponukami"),
("Hide Menubar", "skryť panel s ponukami"),
("Direct Connection", "Priame pripojenie"),
("Relay Connection", "Reléové pripojenie"),
("Secure Connection", "Zabezpečené pripojenie"),
("Insecure Connection", "Nezabezpečené pripojenie"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", ""),
("Show Menubar", ""),
("Hide Menubar", ""),
("Direct Connection", ""),
("Relay Connection", ""),
("Secure Connection", ""),
("Insecure Connection", ""),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Kaydırma Stili"),
("Show Menubar", "Menü çubuğunu göster"),
("Hide Menubar", "menü çubuğunu gizle"),
("Direct Connection", "Doğrudan Bağlantı"),
("Relay Connection", "Röle Bağlantısı"),
("Secure Connection", "Güvenli bağlantı"),
("Insecure Connection", "Güvenli Bağlantı"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "滾動樣式"),
("Show Menubar", "顯示菜單欄"),
("Hide Menubar", "隱藏菜單欄"),
("Direct Connection", "直接連接"),
("Relay Connection", "中繼連接"),
("Secure Connection", "安全連接"),
("Insecure Connection", "非安全連接"),
].iter().cloned().collect();
}

View File

@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Scroll Style", "Kiểu cuộn"),
("Show Menubar", "Hiển thị thanh menu"),
("Hide Menubar", "ẩn thanh menu"),
("Direct Connection", "Kết nối trực tiếp"),
("Relay Connection", "Kết nối chuyển tiếp"),
("Secure Connection", "Kết nối an toàn"),
("Insecure Connection", "Kết nối không an toàn"),
].iter().cloned().collect();
}