flutter_desktop: new remote menu, mid commit
Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
parent
7b4a655eaf
commit
ea77d9284b
@ -447,7 +447,10 @@ void msgBox(
|
||||
0,
|
||||
wrap(translate('OK'), () {
|
||||
dialogManager.dismissAll();
|
||||
closeConnection();
|
||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (type.indexOf("custom") < 0) {
|
||||
closeConnection();
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (hasCancel == null) {
|
||||
@ -740,3 +743,39 @@ 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));
|
||||
}
|
||||
|
@ -22,26 +22,25 @@ class ConnectionTabPage extends StatefulWidget {
|
||||
|
||||
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final tabController = Get.put(DesktopTabController());
|
||||
static final Rx<String> _fullscreenID = "".obs;
|
||||
static final IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||
static final 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) {
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
page: RemotePage(
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
tabBarHeight:
|
||||
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
|
||||
fullscreenID: _fullscreenID,
|
||||
)));
|
||||
page: Obx(() => RemotePage(
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
tabBarHeight:
|
||||
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +53,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == "new_remote_desktop") {
|
||||
final args = jsonDecode(call.arguments);
|
||||
@ -64,14 +65,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
tabBarHeight: _fullscreenID.value.isNotEmpty
|
||||
? 0
|
||||
: kDesktopRemoteTabBarHeight,
|
||||
fullscreenID: _fullscreenID,
|
||||
)));
|
||||
closable: false,
|
||||
page: Obx(() => RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
tabBarHeight:
|
||||
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
|
||||
))));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.state.value.tabs.forEach((tab) {
|
||||
print("executing onDestroy hook, closing ${tab.label}}");
|
||||
@ -88,29 +88,31 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Obx(() => DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
isMainWindow: false,
|
||||
showTabBar: _fullscreenID.value.isEmpty,
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setFullscreen(_fullscreenID.value.isNotEmpty);
|
||||
return pageView;
|
||||
},
|
||||
))),
|
||||
),
|
||||
);
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return Obx(() => SubWindowDragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Obx(() => DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
isMainWindow: false,
|
||||
showTabBar: fullscreen.isFalse,
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setFullscreen(fullscreen.isTrue);
|
||||
return pageView;
|
||||
},
|
||||
))),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class DesktopTabPage extends StatefulWidget {
|
||||
@ -33,26 +34,29 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = isDarkTheme();
|
||||
return DragToResizeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
isMainWindow: true,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
onTap: onAddSetting,
|
||||
is_close: false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
RxBool fullscreen = false.obs;
|
||||
Get.put(fullscreen, tag: 'fullscreen');
|
||||
return Obx(() => DragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
isMainWindow: true,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
onTap: onAddSetting,
|
||||
is_close: false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void onAddSetting() {
|
||||
|
@ -9,9 +9,11 @@ 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';
|
||||
|
||||
import '../widgets/remote_menubar.dart';
|
||||
import '../../common.dart';
|
||||
import '../../mobile/widgets/dialog.dart';
|
||||
import '../../mobile/widgets/overlay.dart';
|
||||
@ -21,16 +23,14 @@ import '../../models/platform_model.dart';
|
||||
final initText = '\1' * 1024;
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.tabBarHeight,
|
||||
required this.fullscreenID})
|
||||
: super(key: key);
|
||||
RemotePage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.tabBarHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
final String id;
|
||||
final double tabBarHeight;
|
||||
final Rx<String> fullscreenID;
|
||||
|
||||
@override
|
||||
_RemotePageState createState() => _RemotePageState();
|
||||
@ -50,11 +50,15 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
late FFI _ffi;
|
||||
|
||||
void _updateTabBarHeight() {
|
||||
_ffi.canvasModel.tabBarHeight = widget.tabBarHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ffi = FFI();
|
||||
_ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight;
|
||||
_updateTabBarHeight();
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
_ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@ -70,6 +74,9 @@ 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);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -90,6 +97,9 @@ 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);
|
||||
}
|
||||
|
||||
void resetTool() {
|
||||
@ -217,6 +227,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
_updateTabBarHeight();
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(_ffi.dialogManager);
|
||||
@ -289,6 +300,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
Widget? getBottomAppBar(FfiModel ffiModel) {
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: BottomAppBar(
|
||||
@ -323,15 +335,11 @@ class _RemotePageState extends State<RemotePage>
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(widget.fullscreenID.value.isEmpty
|
||||
icon: Icon(fullscreen.isTrue
|
||||
? Icons.fullscreen
|
||||
: Icons.close_fullscreen),
|
||||
onPressed: () {
|
||||
if (widget.fullscreenID.value.isEmpty) {
|
||||
widget.fullscreenID.value = widget.id;
|
||||
} else {
|
||||
widget.fullscreenID.value = "";
|
||||
}
|
||||
fullscreen.value = !fullscreen.value;
|
||||
},
|
||||
)
|
||||
]) +
|
||||
@ -404,7 +412,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,7 +426,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousedown'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,7 +434,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mouseup'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -434,7 +442,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (_isPhysicalMouse) {
|
||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||
tabBarHeight: super.widget.tabBarHeight);
|
||||
tabBarHeight: widget.tabBarHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -500,6 +508,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
));
|
||||
}
|
||||
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
||||
paints.add(RemoteMenubar(
|
||||
id: widget.id,
|
||||
ffi: _ffi,
|
||||
));
|
||||
return Stack(
|
||||
children: paints,
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// multi-tab desktop remote screen
|
||||
@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
RxBool fullscreen = false.obs;
|
||||
Get.put(fullscreen, tag: 'fullscreen');
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
|
1321
flutter/lib/desktop/widgets/material_mod_popup_menu.dart
Normal file
1321
flutter/lib/desktop/widgets/material_mod_popup_menu.dart
Normal file
File diff suppressed because it is too large
Load Diff
375
flutter/lib/desktop/widgets/popup_menu.dart
Normal file
375
flutter/lib/desktop/widgets/popup_menu.dart
Normal file
@ -0,0 +1,375 @@
|
||||
import 'dart:core';
|
||||
|
||||
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";
|
||||
|
||||
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu
|
||||
class PopupMenuChildrenItem<T> extends modMenu.PopupMenuEntry<T> {
|
||||
const PopupMenuChildrenItem({
|
||||
key,
|
||||
this.height = kMinInteractiveDimension,
|
||||
this.padding,
|
||||
this.enable = true,
|
||||
this.textStyle,
|
||||
this.onTap,
|
||||
this.position = modMenu.PopupMenuPosition.overSide,
|
||||
this.offset = Offset.zero,
|
||||
required this.itemBuilder,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final modMenu.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 Widget child;
|
||||
|
||||
@override
|
||||
final double height;
|
||||
|
||||
@override
|
||||
bool represents(T? value) => false;
|
||||
|
||||
@override
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
||||
}
|
||||
|
||||
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
extends State<W> {
|
||||
@protected
|
||||
void handleTap(T value) {
|
||||
widget.onTap?.call();
|
||||
Navigator.pop<T>(context, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||
TextStyle style = widget.textStyle ??
|
||||
popupMenuTheme.textStyle ??
|
||||
theme.textTheme.subtitle1!;
|
||||
|
||||
return modMenu.PopupMenuButton<T>(
|
||||
enabled: widget.enable,
|
||||
position: widget.position,
|
||||
offset: widget.offset,
|
||||
onSelected: handleTap,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuConfig {
|
||||
// adapt to the screen height
|
||||
static const fontSize = 14.0;
|
||||
static const midPadding = 10.0;
|
||||
static const iconScale = 0.8;
|
||||
static const iconWidth = 12.0;
|
||||
static const iconHeight = 12.0;
|
||||
|
||||
final double secondMenuHeight;
|
||||
final Color commonColor;
|
||||
|
||||
const MenuConfig(
|
||||
{required this.commonColor,
|
||||
this.secondMenuHeight = kMinInteractiveDimension});
|
||||
}
|
||||
|
||||
abstract class MenuEntryBase<T> {
|
||||
modMenu.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();
|
||||
}
|
||||
}
|
||||
|
||||
typedef RadioOptionsGetter = List<Tuple2<String, String>> Function();
|
||||
typedef RadioCurOptionGetter = Future<String> Function();
|
||||
typedef RadioOptionSetter = Future<void> Function(String);
|
||||
|
||||
class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final RadioOptionsGetter optionsGetter;
|
||||
final RadioCurOptionGetter curOptionGetter;
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
|
||||
MenuEntrySubRadios(
|
||||
{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;
|
||||
}
|
||||
}
|
||||
|
||||
modMenu.PopupMenuEntry<T> _buildSecondMenu(
|
||||
BuildContext context, MenuConfig conf, Tuple2<String, String> opt) {
|
||||
return modMenu.PopupMenuItem(
|
||||
padding: EdgeInsets.zero,
|
||||
child: TextButton(
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.secondMenuHeight),
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (opt.item2 != curOption.value) {
|
||||
setOption(opt.item2);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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,
|
||||
),
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef SwitchGetter = Future<bool> Function();
|
||||
typedef SwitchSetter = Future<void> Function(bool);
|
||||
|
||||
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
|
||||
MenuEntrySwitchBase({required this.text});
|
||||
|
||||
RxBool get curOption;
|
||||
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,
|
||||
),
|
||||
contentPadding: EdgeInsets.only(left: 8.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||
final SwitchGetter getter;
|
||||
final SwitchSetter setter;
|
||||
final RxBool _curOption = false.obs;
|
||||
|
||||
MenuEntrySwitch(
|
||||
{required String text, required this.getter, required this.setter})
|
||||
: super(text: text) {
|
||||
() async {
|
||||
_curOption.value = await getter();
|
||||
}();
|
||||
}
|
||||
|
||||
@override
|
||||
RxBool get curOption => _curOption;
|
||||
@override
|
||||
setOption(bool option) async {
|
||||
await setter(option);
|
||||
final opt = await getter();
|
||||
if (_curOption.value != opt) {
|
||||
_curOption.value = opt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef Switch2Getter = RxBool Function();
|
||||
typedef Switch2Setter = Future<void> Function(bool);
|
||||
|
||||
class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
|
||||
final Switch2Getter getter;
|
||||
final SwitchSetter setter;
|
||||
|
||||
MenuEntrySwitch2(
|
||||
{required String text, required this.getter, required this.setter})
|
||||
: super(text: text);
|
||||
|
||||
@override
|
||||
RxBool get curOption => getter();
|
||||
@override
|
||||
setOption(bool option) async {
|
||||
await setter(option);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final List<MenuEntryBase<T>> entries;
|
||||
|
||||
MenuEntrySubMenu({
|
||||
required this.text,
|
||||
required this.entries,
|
||||
});
|
||||
|
||||
@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,
|
||||
),
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
final Widget Function(TextStyle? style) childBuilder;
|
||||
Function() proc;
|
||||
|
||||
MenuEntryButton({
|
||||
required this.childBuilder,
|
||||
required this.proc,
|
||||
});
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
560
flutter/lib/desktop/widgets/remote_menubar.dart
Normal file
560
flutter/lib/desktop/widgets/remote_menubar.dart
Normal file
@ -0,0 +1,560 @@
|
||||
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:tuple/tuple.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../mobile/widgets/dialog.dart';
|
||||
import '../../mobile/widgets/overlay.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import './popup_menu.dart';
|
||||
import './material_mod_popup_menu.dart' as modMenu;
|
||||
|
||||
class _MenubarTheme {
|
||||
static const Color commonColor = MyTheme.accent;
|
||||
static const double height = kMinInteractiveDimension;
|
||||
}
|
||||
|
||||
class RemoteMenubar extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
const RemoteMenubar({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RemoteMenubar> createState() => _RemoteMenubarState();
|
||||
}
|
||||
|
||||
class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
final RxBool _show = false.obs;
|
||||
final Rx<Color> _hideColor = Colors.white12.obs;
|
||||
|
||||
bool get isFullscreen => Get.find<RxBool>(tag: 'fullscreen').isTrue;
|
||||
void setFullscreen(bool v) {
|
||||
Get.find<RxBool>(tag: 'fullscreen').value = v;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Obx(
|
||||
() => _show.value ? _buildMenubar(context) : _buildShowHide(context)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShowHide(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 100,
|
||||
height: 5,
|
||||
child: TextButton(
|
||||
onHover: (bool v) {
|
||||
_hideColor.value = v ? Colors.white60 : Colors.white24;
|
||||
},
|
||||
onPressed: () {
|
||||
_show.value = !_show.value;
|
||||
},
|
||||
child: Obx(() => Container(
|
||||
color: _hideColor.value,
|
||||
))));
|
||||
}
|
||||
|
||||
Widget _buildMenubar(BuildContext context) {
|
||||
final List<Widget> menubarItems = [];
|
||||
if (!isWebDesktop) {
|
||||
menubarItems.add(_buildFullscreen(context));
|
||||
if (widget.ffi.ffiModel.isPeerAndroid) {
|
||||
menubarItems.add(IconButton(
|
||||
tooltip: translate('Mobile Actions'),
|
||||
color: _MenubarTheme.commonColor,
|
||||
icon: Icon(Icons.build),
|
||||
onPressed: () {
|
||||
if (mobileActionsOverlayEntry == null) {
|
||||
showMobileActionsOverlay();
|
||||
} else {
|
||||
hideMobileActionsOverlay();
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
menubarItems.add(_buildMonitor(context));
|
||||
menubarItems.add(_buildControl(context));
|
||||
menubarItems.add(_buildDisplay(context));
|
||||
if (!isWeb) {
|
||||
menubarItems.add(_buildChat(context));
|
||||
}
|
||||
menubarItems.add(_buildClose(context));
|
||||
return PopupMenuTheme(
|
||||
data: PopupMenuThemeData(
|
||||
textStyle: TextStyle(color: _MenubarTheme.commonColor)),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: menubarItems,
|
||||
)),
|
||||
_buildShowHide(context),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _buildFullscreen(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||
onPressed: () {
|
||||
setFullscreen(!isFullscreen);
|
||||
},
|
||||
icon: Obx(() => isFullscreen
|
||||
? Icon(
|
||||
Icons.fullscreen_exit,
|
||||
color: _MenubarTheme.commonColor,
|
||||
)
|
||||
: Icon(
|
||||
Icons.fullscreen,
|
||||
color: _MenubarTheme.commonColor,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChat(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: translate('Chat'),
|
||||
onPressed: () {
|
||||
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
|
||||
widget.ffi.chatModel.toggleChatOverlay();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.message,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonitor(BuildContext context) {
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
return modMenu.PopupMenuButton(
|
||||
tooltip: translate('Select Monitor'),
|
||||
padding: EdgeInsets.zero,
|
||||
position: modMenu.PopupMenuPosition.under,
|
||||
icon: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.personal_video,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 3.9),
|
||||
child: Obx(() {
|
||||
RxInt display = CurrentDisplayState.find(widget.id);
|
||||
return Text(
|
||||
"${display.value + 1}/${pi.displays.length}",
|
||||
style: TextStyle(color: _MenubarTheme.commonColor, fontSize: 8),
|
||||
);
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
itemBuilder: (BuildContext context) {
|
||||
final List<Widget> rowChildren = [];
|
||||
final double selectorScale = 1.3;
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
rowChildren.add(Transform.scale(
|
||||
scale: selectorScale,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.personal_video,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
TextButton(
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
constraints:
|
||||
BoxConstraints(minHeight: _MenubarTheme.height),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 2.5),
|
||||
child: Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(color: _MenubarTheme.commonColor),
|
||||
),
|
||||
)),
|
||||
onPressed: () {
|
||||
RxInt display = CurrentDisplayState.find(widget.id);
|
||||
if (display.value != i) {
|
||||
bind.sessionSwitchDisplay(id: widget.id, value: i);
|
||||
pi.currentDisplay = i;
|
||||
display.value = i;
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
return <modMenu.PopupMenuEntry<String>>[
|
||||
modMenu.PopupMenuItem<String>(
|
||||
height: _MenubarTheme.height,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: rowChildren),
|
||||
)
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControl(BuildContext context) {
|
||||
return modMenu.PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
Icons.bolt,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
tooltip: translate('Control Actions'),
|
||||
position: modMenu.PopupMenuPosition.under,
|
||||
itemBuilder: (BuildContext context) => _getControlMenu()
|
||||
.map((entry) => entry.build(
|
||||
context,
|
||||
MenuConfig(
|
||||
commonColor: _MenubarTheme.commonColor,
|
||||
secondMenuHeight: _MenubarTheme.height,
|
||||
)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisplay(BuildContext context) {
|
||||
return modMenu.PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
Icons.tv,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
tooltip: translate('Display Settings'),
|
||||
position: modMenu.PopupMenuPosition.under,
|
||||
onSelected: (String item) {},
|
||||
itemBuilder: (BuildContext context) => _getDisplayMenu()
|
||||
.map((entry) => entry.build(
|
||||
context,
|
||||
MenuConfig(
|
||||
commonColor: _MenubarTheme.commonColor,
|
||||
secondMenuHeight: _MenubarTheme.height,
|
||||
)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClose(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: translate('Close'),
|
||||
onPressed: () {
|
||||
clientClose(widget.ffi.dialogManager);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<MenuEntryBase<String>> _getControlMenu() {
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
final perms = widget.ffi.ffiModel.permissions;
|
||||
|
||||
final List<MenuEntryBase<String>> displayMenu = [];
|
||||
|
||||
if (pi.version.isNotEmpty) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Refresh'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
bind.sessionRefresh(id: widget.id);
|
||||
},
|
||||
));
|
||||
}
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('OS Password'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
|
||||
},
|
||||
));
|
||||
|
||||
if (!isWebDesktop) {
|
||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Paste'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
() async {
|
||||
ClipboardData? data =
|
||||
await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
||||
}
|
||||
}();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Reset canvas'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
widget.ffi.cursorModel.reset();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if (perms['keyboard'] != false) {
|
||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Insert') + ' Ctrl + Alt + Del',
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
bind.sessionCtrlAltDel(id: widget.id);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Insert Lock'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
bind.sessionLockScreen(id: widget.id);
|
||||
},
|
||||
));
|
||||
|
||||
if (pi.platform == 'Windows') {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||
translate(
|
||||
(BlockInputState.find(widget.id).value ? 'Unb' : 'B') +
|
||||
'lock user input'),
|
||||
style: style,
|
||||
)),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
RxBool blockInput = BlockInputState.find(widget.id);
|
||||
bind.sessionToggleOption(
|
||||
id: widget.id,
|
||||
value: (blockInput.value ? 'un' : '') + 'block-input');
|
||||
blockInput.value = !blockInput.value;
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (gFFI.ffiModel.permissions["restart"] != false &&
|
||||
(pi.platform == "Linux" ||
|
||||
pi.platform == "Windows" ||
|
||||
pi.platform == "Mac OS")) {
|
||||
displayMenu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Restart Remote Device'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
Navigator.pop(context);
|
||||
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
return displayMenu;
|
||||
}
|
||||
|
||||
List<MenuEntryBase<String>> _getDisplayMenu() {
|
||||
final displayMenu = [
|
||||
MenuEntrySubRadios<String>(
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () => [
|
||||
Tuple2<String, String>(translate('Original'), 'original'),
|
||||
Tuple2<String, String>(translate('Shrink'), 'shrink'),
|
||||
Tuple2<String, String>(translate('Stretch'), 'stretch'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'view-style') ??
|
||||
'';
|
||||
},
|
||||
optionSetter: (String v) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "view-style", value: v);
|
||||
widget.ffi.canvasModel.updateViewStyle();
|
||||
}),
|
||||
MenuEntrySubRadios<String>(
|
||||
text: translate('Scroll Style'),
|
||||
optionsGetter: () => [
|
||||
Tuple2<String, String>(translate('ScrollAuto'), 'scrollauto'),
|
||||
Tuple2<String, String>(translate('Scrollbar'), 'scrollbar'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'scroll-style') ??
|
||||
'';
|
||||
},
|
||||
optionSetter: (String v) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "scroll-style", value: v);
|
||||
widget.ffi.canvasModel.updateScrollStyle();
|
||||
}),
|
||||
MenuEntrySubRadios<String>(
|
||||
text: translate('Image Quality'),
|
||||
optionsGetter: () => [
|
||||
Tuple2<String, String>(translate('Good image quality'), 'best'),
|
||||
Tuple2<String, String>(translate('Balanced'), 'balanced'),
|
||||
Tuple2<String, String>(
|
||||
translate('Optimize reaction time'), 'low'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
String quality =
|
||||
await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced';
|
||||
if (quality == '') quality = 'balanced';
|
||||
return quality;
|
||||
},
|
||||
optionSetter: (String v) async {
|
||||
await bind.sessionSetImageQuality(id: widget.id, value: v);
|
||||
}),
|
||||
MenuEntrySwitch<String>(
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () async {
|
||||
return await bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
}),
|
||||
MenuEntrySwitch<String>(
|
||||
text: translate('Show quality monitor'),
|
||||
getter: () async {
|
||||
return await bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-quality-monitor');
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-quality-monitor');
|
||||
widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
}),
|
||||
];
|
||||
|
||||
final perms = widget.ffi.ffiModel.permissions;
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
|
||||
if (perms['audio'] != false) {
|
||||
displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio'));
|
||||
}
|
||||
if (perms['keyboard'] != false) {
|
||||
if (perms['clipboard'] != false) {
|
||||
displayMenu.add(
|
||||
_createSwitchMenuEntry('Disable clipboard', 'disable-clipboard'));
|
||||
}
|
||||
displayMenu.add(_createSwitchMenuEntry(
|
||||
'Lock after session end', 'lock-after-session-end'));
|
||||
if (pi.platform == 'Windows') {
|
||||
displayMenu.add(MenuEntrySwitch2<String>(
|
||||
text: translate('Privacy mode'),
|
||||
getter: () {
|
||||
return PrivacyModeState.find(widget.id);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
Navigator.pop(context);
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'privacy-mode');
|
||||
}));
|
||||
}
|
||||
}
|
||||
return displayMenu;
|
||||
}
|
||||
|
||||
MenuEntrySwitch<String> _createSwitchMenuEntry(String text, String option) {
|
||||
return MenuEntrySwitch<String>(
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void showSetOSPassword(
|
||||
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||
if (text != "" && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
@ -172,9 +172,9 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'update_quality_status') {
|
||||
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
||||
} else if (name == 'update_block_input_state') {
|
||||
updateBlockInputState(evt);
|
||||
updateBlockInputState(evt, peerId);
|
||||
} else if (name == 'update_privacy_mode') {
|
||||
updatePrivacyMode(evt);
|
||||
updatePrivacyMode(evt, peerId);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -231,9 +231,9 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'update_quality_status') {
|
||||
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
||||
} else if (name == 'update_block_input_state') {
|
||||
updateBlockInputState(evt);
|
||||
updateBlockInputState(evt, peerId);
|
||||
} else if (name == 'update_privacy_mode') {
|
||||
updatePrivacyMode(evt);
|
||||
updatePrivacyMode(evt, peerId);
|
||||
}
|
||||
};
|
||||
platformFFI.setEventCallback(cb);
|
||||
@ -305,6 +305,12 @@ class FfiModel with ChangeNotifier {
|
||||
_pi.sasEnabled = evt['sas_enabled'] == "true";
|
||||
_pi.currentDisplay = int.parse(evt['current_display']);
|
||||
|
||||
try {
|
||||
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (isPeerAndroid) {
|
||||
_touchMode = true;
|
||||
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
||||
@ -343,13 +349,24 @@ class FfiModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
updateBlockInputState(Map<String, dynamic> evt) {
|
||||
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
|
||||
_inputBlocked = evt['input_state'] == 'on';
|
||||
notifyListeners();
|
||||
try {
|
||||
BlockInputState.find(peerId).value = evt['input_state'] == 'on';
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
updatePrivacyMode(Map<String, dynamic> evt) {
|
||||
updatePrivacyMode(Map<String, dynamic> evt, String peerId) {
|
||||
notifyListeners();
|
||||
try {
|
||||
PrivacyModeState.find(peerId).value =
|
||||
bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode');
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "正在重启远程设备"),
|
||||
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
|
||||
("Copied", "已复制"),
|
||||
("Exit Fullscreen", "退出全屏"),
|
||||
("Fullscreen", "全屏"),
|
||||
("Mobile Actions", "移动端操作"),
|
||||
("Select Monitor", "选择监视器"),
|
||||
("Control Actions", "控制操作"),
|
||||
("Display Settings", "显示设置"),
|
||||
("Ratio", "比例"),
|
||||
("Image Quality", "画质"),
|
||||
("Scroll Style", "滚屏方式"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Ukončete celou obrazovku"),
|
||||
("Fullscreen", "Celá obrazovka"),
|
||||
("Mobile Actions", "Mobilní akce"),
|
||||
("Select Monitor", "Vyberte možnost Monitor"),
|
||||
("Control Actions", "Ovládací akce"),
|
||||
("Display Settings", "Nastavení obrazovky"),
|
||||
("Ratio", "Poměr"),
|
||||
("Image Quality", "Kvalita obrazu"),
|
||||
("Scroll Style", "Štýl posúvania"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Afslut fuldskærm"),
|
||||
("Fullscreen", "Fuld skærm"),
|
||||
("Mobile Actions", "Mobile handlinger"),
|
||||
("Select Monitor", "Vælg Monitor"),
|
||||
("Control Actions", "Kontrolhandlinger"),
|
||||
("Display Settings", "Skærmindstillinger"),
|
||||
("Ratio", "Forhold"),
|
||||
("Image Quality", "Billede kvalitet"),
|
||||
("Scroll Style", "Rulstil"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
|
||||
("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Vollbild beenden"),
|
||||
("Fullscreen", "Ganzer Bildschirm"),
|
||||
("Mobile Actions", "Mobile Aktionen"),
|
||||
("Select Monitor", "Wählen Sie Überwachen aus"),
|
||||
("Control Actions", "Kontrollaktionen"),
|
||||
("Display Settings", "Bildschirmeinstellungen"),
|
||||
("Ratio", "Verhältnis"),
|
||||
("Image Quality", "Bildqualität"),
|
||||
("Scroll Style", "Scroll-Stil"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Eliru Plenekranon"),
|
||||
("Fullscreen", "Plenekrane"),
|
||||
("Mobile Actions", "Poŝtelefonaj Agoj"),
|
||||
("Select Monitor", "Elektu Monitoron"),
|
||||
("Control Actions", "Kontrolaj Agoj"),
|
||||
("Display Settings", "Montraj Agordoj"),
|
||||
("Ratio", "Proporcio"),
|
||||
("Image Quality", "Bilda Kvalito"),
|
||||
("Scroll Style", "Ruluma Stilo"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Reiniciando dispositivo remoto"),
|
||||
("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Salir de pantalla completa"),
|
||||
("Fullscreen", "Pantalla completa"),
|
||||
("Mobile Actions", "Acciones móviles"),
|
||||
("Select Monitor", "Seleccionar monitor"),
|
||||
("Control Actions", "Acciones de control"),
|
||||
("Display Settings", "Configuración de pantalla"),
|
||||
("Ratio", "Relación"),
|
||||
("Image Quality", "La calidad de imagen"),
|
||||
("Scroll Style", "Estilo de desplazamiento"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Quitter le mode plein écran"),
|
||||
("Fullscreen", "Plein écran"),
|
||||
("Mobile Actions", "Actions mobiles"),
|
||||
("Select Monitor", "Sélectionnez Moniteur"),
|
||||
("Control Actions", "Actions de contrôle"),
|
||||
("Display Settings", "Paramètres d'affichage"),
|
||||
("Ratio", "Rapport"),
|
||||
("Image Quality", "Qualité d'image"),
|
||||
("Scroll Style", "Style de défilement"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Lépjen ki a teljes képernyőről"),
|
||||
("Fullscreen", "Teljes képernyő"),
|
||||
("Mobile Actions", "mobil műveletek"),
|
||||
("Select Monitor", "Válassza a Monitor lehetőséget"),
|
||||
("Control Actions", "Irányítási műveletek"),
|
||||
("Display Settings", "Megjelenítési beállítások"),
|
||||
("Ratio", "Hányados"),
|
||||
("Image Quality", "Képminőség"),
|
||||
("Scroll Style", "Görgetési stílus"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Keluar dari Layar Penuh"),
|
||||
("Fullscreen", "Layar penuh"),
|
||||
("Mobile Actions", "Tindakan Seluler"),
|
||||
("Select Monitor", "Pilih Monitor"),
|
||||
("Control Actions", "Tindakan Kontrol"),
|
||||
("Display Settings", "Pengaturan tampilan"),
|
||||
("Ratio", "Perbandingan"),
|
||||
("Image Quality", "Kualitas gambar"),
|
||||
("Scroll Style", "Gaya Gulir"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -301,5 +301,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
|
||||
("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"),
|
||||
("remote_restarting_tip", "Riavviare il dispositivo remoto"),
|
||||
("Exit Fullscreen", "Esci dalla modalità schermo intero"),
|
||||
("Fullscreen", "A schermo intero"),
|
||||
("Mobile Actions", "Azioni mobili"),
|
||||
("Select Monitor", "Seleziona Monitora"),
|
||||
("Control Actions", "Azioni di controllo"),
|
||||
("Display Settings", "Impostazioni di visualizzazione"),
|
||||
("Ratio", "Rapporto"),
|
||||
("Image Quality", "Qualità dell'immagine"),
|
||||
("Scroll Style", "Stile di scorrimento"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -299,5 +299,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "本当に再起動しますか"),
|
||||
("Restarting Remote Device", "リモート端末を再起動中"),
|
||||
("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"),
|
||||
("Exit Fullscreen", "全画面表示を終了"),
|
||||
("Fullscreen", "全画面表示"),
|
||||
("Mobile Actions", "モバイル アクション"),
|
||||
("Select Monitor", "モニターを選択"),
|
||||
("Control Actions", "コントロール アクション"),
|
||||
("Display Settings", "ディスプレイの設定"),
|
||||
("Ratio", "比率"),
|
||||
("Image Quality", "画質"),
|
||||
("Scroll Style", "スクロール スタイル"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -299,5 +299,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "정말로 재시작 하시겠습니까"),
|
||||
("Restarting Remote Device", "원격 기기를 다시 시작하는중"),
|
||||
("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."),
|
||||
("Exit Fullscreen", "전체 화면 종료"),
|
||||
("Fullscreen", "전체화면"),
|
||||
("Mobile Actions", "모바일 액션"),
|
||||
("Select Monitor", "모니터 선택"),
|
||||
("Control Actions", "제어 작업"),
|
||||
("Display Settings", "화면 설정"),
|
||||
("Ratio", "비율"),
|
||||
("Image Quality", "이미지 품질"),
|
||||
("Scroll Style", "스크롤 스타일"),
|
||||
].iter().cloned().collect();
|
||||
}
|
@ -303,5 +303,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Set security password", "Ustaw hasło zabezpieczające"),
|
||||
("Connection not allowed", "Połączenie niedozwolone"),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"),
|
||||
("Fullscreen", "Pełny ekran"),
|
||||
("Mobile Actions", "Działania mobilne"),
|
||||
("Select Monitor", "Wybierz Monitor"),
|
||||
("Control Actions", "Działania kontrolne"),
|
||||
("Display Settings", "Ustawienia wyświetlania"),
|
||||
("Ratio", "Stosunek"),
|
||||
("Image Quality", "Jakość obrazu"),
|
||||
("Scroll Style", "Styl przewijania"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -299,5 +299,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"),
|
||||
("Restarting Remote Device", "A reiniciar sistema remoto"),
|
||||
("remote_restarting_tip", ""),
|
||||
("Exit Fullscreen", "Sair da tela cheia"),
|
||||
("Fullscreen", "Tela cheia"),
|
||||
("Mobile Actions", "Ações para celular"),
|
||||
("Select Monitor", "Selecionar monitor"),
|
||||
("Control Actions", "Ações de controle"),
|
||||
("Display Settings", "Configurações do visor"),
|
||||
("Ratio", "Razão"),
|
||||
("Image Quality", "Qualidade da imagem"),
|
||||
("Scroll Style", "Estilo de rolagem"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", ""),
|
||||
("Fullscreen", ""),
|
||||
("Mobile Actions", ""),
|
||||
("Select Monitor", ""),
|
||||
("Control Actions", ""),
|
||||
("Display Settings", ""),
|
||||
("Ratio", ""),
|
||||
("Image Quality", ""),
|
||||
("Scroll Style", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Перезагрузка удаленного устройства"),
|
||||
("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Выйти из полноэкранного режима"),
|
||||
("Fullscreen", "Полноэкранный"),
|
||||
("Mobile Actions", "Мобильные действия"),
|
||||
("Select Monitor", "Выберите монитор"),
|
||||
("Control Actions", "Действия по управлению"),
|
||||
("Display Settings", "Настройки отображения"),
|
||||
("Ratio", "Соотношение"),
|
||||
("Image Quality", "Качество изображения"),
|
||||
("Scroll Style", "Стиль прокрутки"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Ukončiť celú obrazovku"),
|
||||
("Fullscreen", "Celá obrazovka"),
|
||||
("Mobile Actions", "Mobilné akcie"),
|
||||
("Select Monitor", "Vyberte možnosť Monitor"),
|
||||
("Control Actions", "Kontrolné akcie"),
|
||||
("Display Settings", "Nastavenia displeja"),
|
||||
("Ratio", "Pomer"),
|
||||
("Image Quality", "Kvalita obrazu"),
|
||||
("Scroll Style", "Štýl posúvania"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", ""),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", ""),
|
||||
("Fullscreen", ""),
|
||||
("Mobile Actions", ""),
|
||||
("Select Monitor", ""),
|
||||
("Control Actions", ""),
|
||||
("Display Settings", ""),
|
||||
("Ratio", ""),
|
||||
("Image Quality", ""),
|
||||
("Scroll Style", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
|
||||
("remote_restarting_tip", ""),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Tam ekrandan çık"),
|
||||
("Fullscreen", "Tam ekran"),
|
||||
("Mobile Actions", "Mobil İşlemler"),
|
||||
("Select Monitor", "Monitörü Seç"),
|
||||
("Control Actions", "Kontrol Eylemleri"),
|
||||
("Display Settings", "Görüntü ayarları"),
|
||||
("Ratio", "Oran"),
|
||||
("Image Quality", "Görüntü kalitesi"),
|
||||
("Scroll Style", "Kaydırma Stili"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "正在重啓遠程設備"),
|
||||
("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
|
||||
("Copied", "已複製"),
|
||||
("Exit Fullscreen", "退出全屏"),
|
||||
("Fullscreen", "全屏"),
|
||||
("Mobile Actions", "移動端操作"),
|
||||
("Select Monitor", "選擇監視器"),
|
||||
("Control Actions", "控制操作"),
|
||||
("Display Settings", "顯示設置"),
|
||||
("Ratio", "比例"),
|
||||
("Image Quality", "畫質"),
|
||||
("Scroll Style", "滾動樣式"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"),
|
||||
("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"),
|
||||
("Copied", ""),
|
||||
("Exit Fullscreen", "Thoát toàn màn hình"),
|
||||
("Fullscreen", "Toàn màn hình"),
|
||||
("Mobile Actions", "Hành động trên thiết bị di động"),
|
||||
("Select Monitor", "Chọn màn hình"),
|
||||
("Control Actions", "Kiểm soát hành động"),
|
||||
("Display Settings", "Thiết lập hiển thị"),
|
||||
("Ratio", "Tỉ lệ"),
|
||||
("Image Quality", "Chất lượng hình ảnh"),
|
||||
("Scroll Style", "Kiểu cuộn"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user