mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'master' into record
This commit is contained in:
@@ -5,14 +5,16 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/address_book.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/peer_widget.dart';
|
||||
import '../../common/widgets/peers_view.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/button.dart';
|
||||
|
||||
/// Connection page for connecting to a remote peer.
|
||||
class ConnectionPage extends StatefulWidget {
|
||||
@@ -74,10 +76,18 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
translate('Address Book')
|
||||
],
|
||||
children: [
|
||||
RecentPeerWidget(),
|
||||
FavoritePeerWidget(),
|
||||
DiscoveredPeerWidget(),
|
||||
const AddressBook(),
|
||||
RecentPeersView(
|
||||
menuPadding: EdgeInsets.only(left: 12.0, right: 3.0),
|
||||
),
|
||||
FavoritePeersView(
|
||||
menuPadding: EdgeInsets.only(left: 12.0, right: 3.0),
|
||||
),
|
||||
DiscoveredPeersView(
|
||||
menuPadding: EdgeInsets.only(left: 12.0, right: 3.0),
|
||||
),
|
||||
const AddressBook(
|
||||
menuPadding: EdgeInsets.only(left: 12.0, right: 3.0),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
@@ -100,10 +110,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
/// UI for the remote ID TextField.
|
||||
/// Search for a peer and connect to it if the id exists.
|
||||
Widget _buildRemoteIDTextField(BuildContext context) {
|
||||
RxBool ftHover = false.obs;
|
||||
RxBool ftPressed = false.obs;
|
||||
RxBool connHover = false.obs;
|
||||
RxBool connPressed = false.obs;
|
||||
RxBool inputFocused = false.obs;
|
||||
FocusNode focusNode = FocusNode();
|
||||
focusNode.addListener(() {
|
||||
@@ -113,7 +119,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
width: 320 + 20 * 2,
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
color: Theme.of(context).backgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
||||
),
|
||||
child: Ink(
|
||||
@@ -123,7 +129,10 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
children: [
|
||||
Text(
|
||||
translate('Control Remote Desktop'),
|
||||
style: const TextStyle(fontSize: 19, height: 1),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
@@ -142,13 +151,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor: MyTheme.color(context).text!,
|
||||
cursorColor:
|
||||
Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
hintText: inputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
hintStyle: TextStyle(
|
||||
color: MyTheme.color(context).placeholder),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
@@ -180,84 +188,17 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Obx(() => InkWell(
|
||||
onTapDown: (_) => ftPressed.value = true,
|
||||
onTapUp: (_) => ftPressed.value = false,
|
||||
onTapCancel: () => ftPressed.value = false,
|
||||
onHover: (value) => ftHover.value = value,
|
||||
onTap: () {
|
||||
onConnect(isFileTransfer: true);
|
||||
},
|
||||
child: Container(
|
||||
height: 27,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: ftPressed.value
|
||||
? MyTheme.accent
|
||||
: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: ftPressed.value
|
||||
? MyTheme.accent
|
||||
: ftHover.value
|
||||
? MyTheme.hoverBorder
|
||||
: MyTheme.border,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Text(
|
||||
translate(
|
||||
"Transfer File",
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ftPressed.value
|
||||
? MyTheme.color(context).bg
|
||||
: MyTheme.color(context).text),
|
||||
).marginSymmetric(horizontal: 12),
|
||||
),
|
||||
)),
|
||||
Button(
|
||||
isOutline: true,
|
||||
onTap: () {
|
||||
onConnect(isFileTransfer: true);
|
||||
},
|
||||
text: "Transfer File",
|
||||
),
|
||||
const SizedBox(
|
||||
width: 17,
|
||||
),
|
||||
Obx(
|
||||
() => InkWell(
|
||||
onTapDown: (_) => connPressed.value = true,
|
||||
onTapUp: (_) => connPressed.value = false,
|
||||
onTapCancel: () => connPressed.value = false,
|
||||
onHover: (value) => connHover.value = value,
|
||||
onTap: onConnect,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 80.0,
|
||||
),
|
||||
child: Container(
|
||||
height: 27,
|
||||
decoration: BoxDecoration(
|
||||
color: connPressed.value
|
||||
? MyTheme.accent
|
||||
: MyTheme.button,
|
||||
border: Border.all(
|
||||
color: connPressed.value
|
||||
? MyTheme.accent
|
||||
: connHover.value
|
||||
? MyTheme.hoverBorder
|
||||
: MyTheme.button,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
translate(
|
||||
"Connect",
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.color(context).bg),
|
||||
),
|
||||
).marginSymmetric(horizontal: 12),
|
||||
)),
|
||||
),
|
||||
),
|
||||
Button(onTap: onConnect, text: "Connect"),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -289,7 +230,11 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: svcStopped.value ? Colors.redAccent : Colors.green,
|
||||
color: svcStopped.value || svcStatusCode.value == 0
|
||||
? kColorWarn
|
||||
: (svcStatusCode.value == 1
|
||||
? Color.fromARGB(255, 50, 190, 166)
|
||||
: Color.fromARGB(255, 224, 79, 95)),
|
||||
),
|
||||
).paddingSymmetric(horizontal: 12.0);
|
||||
if (svcStopped.value) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:flutter/material.dart' hide MenuItem;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
@@ -12,6 +14,9 @@ import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../widgets/button.dart';
|
||||
|
||||
class DesktopHomePage extends StatefulWidget {
|
||||
const DesktopHomePage({Key? key}) : super(key: key);
|
||||
@@ -26,6 +31,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
with TrayListener, WindowListener, AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
var updateUrl = '';
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
@@ -68,12 +74,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
value: gFFI.serverModel,
|
||||
child: Container(
|
||||
width: 200,
|
||||
color: MyTheme.color(context).bg,
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: Column(
|
||||
children: [
|
||||
buildTip(context),
|
||||
buildIDBoard(context),
|
||||
buildPasswordBoard(context),
|
||||
buildHelpCards(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -82,7 +89,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
|
||||
buildRightPane(BuildContext context) {
|
||||
return Container(
|
||||
color: MyTheme.color(context).grayBg,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: ConnectionPage(),
|
||||
);
|
||||
}
|
||||
@@ -116,7 +123,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
translate("ID"),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: MyTheme.color(context).lightText),
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5)),
|
||||
).marginOnly(top: 5),
|
||||
buildPopupMenu(context)
|
||||
],
|
||||
@@ -152,21 +163,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
|
||||
Widget buildPopupMenu(BuildContext context) {
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
RxBool hover = false.obs;
|
||||
return InkWell(
|
||||
onTap: () async {},
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).backgroundColor,
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value
|
||||
? MyTheme.color(context).text
|
||||
: MyTheme.color(context).lightText,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -178,6 +188,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final model = gFFI.serverModel;
|
||||
RxBool refreshHover = false.obs;
|
||||
RxBool editHover = false.obs;
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
|
||||
child: Row(
|
||||
@@ -198,7 +209,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Text(
|
||||
translate("Password"),
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: MyTheme.color(context).lightText),
|
||||
fontSize: 14, color: textColor?.withOpacity(0.5)),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -228,8 +239,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
() => Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? MyTheme.color(context).text
|
||||
: Color(0xFFDDDDDD),
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD), // TODO
|
||||
size: 22,
|
||||
).marginOnly(right: 8, bottom: 2),
|
||||
),
|
||||
@@ -241,12 +252,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
() => Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? MyTheme.color(context).text
|
||||
: Color(0xFFDDDDDD),
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD), // TODO
|
||||
size: 22,
|
||||
).marginOnly(right: 8, bottom: 2),
|
||||
),
|
||||
onTap: () => {},
|
||||
onTap: () => DesktopSettingPage.switch2page(1),
|
||||
onHover: (value) => editHover.value = value,
|
||||
),
|
||||
],
|
||||
@@ -270,7 +281,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
children: [
|
||||
Text(
|
||||
translate("Your Desktop"),
|
||||
style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
// style: TextStyle(
|
||||
// // color: MyTheme.color(context).text,
|
||||
// fontWeight: FontWeight.normal,
|
||||
// fontSize: 19),
|
||||
),
|
||||
SizedBox(
|
||||
height: 10.0,
|
||||
@@ -278,16 +293,93 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Text(
|
||||
translate("desk_tip"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.color(context).lighterText,
|
||||
height: 1.25),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHelpCards() {
|
||||
if (Platform.isWindows) {
|
||||
if (!bind.mainIsInstalled()) {
|
||||
return buildInstallCard(
|
||||
"", "install_tip", "Install", bind.mainGotoInstall);
|
||||
} else if (bind.mainIsInstalledLowerVersion()) {
|
||||
return buildInstallCard("Status", "Your installation is lower version.",
|
||||
"Click to upgrade", bind.mainUpdateMe);
|
||||
}
|
||||
}
|
||||
if (updateUrl.isNotEmpty) {
|
||||
return buildInstallCard(
|
||||
"Status",
|
||||
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
|
||||
"Click to download", () async {
|
||||
final Uri url = Uri.parse('https://rustdesk.com');
|
||||
await launchUrl(url);
|
||||
});
|
||||
}
|
||||
if (Platform.isMacOS) {}
|
||||
if (bind.mainIsInstalledLowerVersion()) {}
|
||||
return Container();
|
||||
}
|
||||
|
||||
Widget buildInstallCard(String title, String content, String btnText,
|
||||
GestureTapCallback onPressed) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 226, 66, 188),
|
||||
Color.fromARGB(255, 244, 114, 124),
|
||||
],
|
||||
)),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: (title.isNotEmpty
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: Text(
|
||||
translate(title),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
).marginOnly(bottom: 6)),
|
||||
]
|
||||
: <Widget>[]) +
|
||||
<Widget>[
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Button(
|
||||
padding: 8,
|
||||
isOutline: true,
|
||||
text: translate(btnText),
|
||||
textColor: Colors.white,
|
||||
borderColor: Colors.white,
|
||||
textSize: 20,
|
||||
radius: 10,
|
||||
onTap: onPressed,
|
||||
)
|
||||
]),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
debugPrint('click ${menuItem.key}');
|
||||
@@ -305,6 +397,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(const Duration(seconds: 5), () async {
|
||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (updateUrl.isNotEmpty) setState(() {});
|
||||
});
|
||||
trayManager.addListener(this);
|
||||
windowManager.addListener(this);
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
@@ -331,7 +427,7 @@ Future<bool> loginDialog() async {
|
||||
var userNameMsg = "";
|
||||
String pass = "";
|
||||
var passMsg = "";
|
||||
var userContontroller = TextEditingController(text: userName);
|
||||
var userController = TextEditingController(text: userName);
|
||||
var pwdController = TextEditingController(text: pass);
|
||||
|
||||
var isInProgress = false;
|
||||
@@ -349,7 +445,7 @@ Future<bool> loginDialog() async {
|
||||
});
|
||||
}
|
||||
|
||||
userName = userContontroller.text;
|
||||
userName = userController.text;
|
||||
pass = pwdController.text;
|
||||
if (userName.isEmpty) {
|
||||
userNameMsg = translate("Username missed");
|
||||
@@ -385,6 +481,7 @@ Future<bool> loginDialog() async {
|
||||
close();
|
||||
}
|
||||
|
||||
// 登录dialog
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Login")),
|
||||
content: ConstrainedBox(
|
||||
@@ -411,7 +508,7 @@ Future<bool> loginDialog() async {
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||
controller: userContontroller,
|
||||
controller: userController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,9 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -18,7 +20,7 @@ import '../../common/widgets/dialog.dart';
|
||||
|
||||
const double _kTabWidth = 235;
|
||||
const double _kTabHeight = 42;
|
||||
const double _kCardFixedWidth = 560;
|
||||
const double _kCardFixedWidth = 540;
|
||||
const double _kCardLeftMargin = 15;
|
||||
const double _kContentHMargin = 15;
|
||||
const double _kContentHSubMargin = _kContentHMargin + 33;
|
||||
@@ -28,6 +30,8 @@ const double _kListViewBottomMargin = 15;
|
||||
const double _kTitleFontSize = 20;
|
||||
const double _kContentFontSize = 15;
|
||||
const Color _accentColor = MyTheme.accent;
|
||||
const String _kSettingPageControllerTag = "settingPageController";
|
||||
const String _kSettingPageIndexTag = "settingPageIndex";
|
||||
|
||||
class _TabInfo {
|
||||
late final String label;
|
||||
@@ -37,10 +41,30 @@ class _TabInfo {
|
||||
}
|
||||
|
||||
class DesktopSettingPage extends StatefulWidget {
|
||||
const DesktopSettingPage({Key? key}) : super(key: key);
|
||||
final int initialPage;
|
||||
|
||||
const DesktopSettingPage({Key? key, required this.initialPage})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
|
||||
|
||||
static void switch2page(int page) {
|
||||
if (page >= 5) return;
|
||||
try {
|
||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
||||
DesktopTabPage.onAddSetting(initialPage: page);
|
||||
PageController controller = Get.find(tag: _kSettingPageControllerTag);
|
||||
RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag);
|
||||
selectedIndex.value = page;
|
||||
controller.jumpToPage(page);
|
||||
} else {
|
||||
DesktopTabPage.onAddSetting(initialPage: page);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
@@ -50,12 +74,12 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
_TabInfo('Security', Icons.enhanced_encryption_outlined,
|
||||
Icons.enhanced_encryption),
|
||||
_TabInfo('Network', Icons.link_outlined, Icons.link),
|
||||
_TabInfo('Acount', Icons.person_outline, Icons.person),
|
||||
_TabInfo('Account', Icons.person_outline, Icons.person),
|
||||
_TabInfo('About', Icons.info_outline, Icons.info)
|
||||
];
|
||||
|
||||
late PageController controller;
|
||||
RxInt selectedIndex = 0.obs;
|
||||
late RxInt selectedIndex;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -63,14 +87,24 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = PageController();
|
||||
selectedIndex = (widget.initialPage < 5 ? widget.initialPage : 0).obs;
|
||||
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
|
||||
controller = PageController(initialPage: widget.initialPage);
|
||||
Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
Get.delete<PageController>(tag: _kSettingPageControllerTag);
|
||||
Get.delete<RxInt>(tag: _kSettingPageIndexTag);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
@@ -85,7 +119,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
const VerticalDivider(thickness: 1, width: 1),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: MyTheme.color(context).grayBg,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: DesktopScrollWrapper(
|
||||
scrollController: controller,
|
||||
child: PageView(
|
||||
@@ -94,7 +128,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
_General(),
|
||||
_Safety(),
|
||||
_Network(),
|
||||
_Acount(),
|
||||
_Account(),
|
||||
_About(),
|
||||
],
|
||||
)),
|
||||
@@ -387,7 +421,7 @@ class _Safety extends StatefulWidget {
|
||||
class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
bool locked = true;
|
||||
bool locked = bind.mainIsInstalled();
|
||||
final scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
@@ -533,7 +567,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
_OptionCheckBox(context, 'Deny remote access', 'stop-service',
|
||||
checkedIcon: const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Color.fromARGB(255, 255, 204, 0),
|
||||
color: kColorWarn,
|
||||
),
|
||||
enabled: enabled),
|
||||
Offstage(
|
||||
@@ -541,6 +575,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
|
||||
enabled: enabled),
|
||||
),
|
||||
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
|
||||
reverse: true, enabled: enabled),
|
||||
...directIp(context),
|
||||
whitelist(),
|
||||
]);
|
||||
@@ -700,14 +736,14 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
}
|
||||
|
||||
class _Acount extends StatefulWidget {
|
||||
const _Acount({Key? key}) : super(key: key);
|
||||
class _Account extends StatefulWidget {
|
||||
const _Account({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_Acount> createState() => _AcountState();
|
||||
State<_Account> createState() => _AccountState();
|
||||
}
|
||||
|
||||
class _AcountState extends State<_Acount> {
|
||||
class _AccountState extends State<_Account> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
@@ -717,12 +753,12 @@ class _AcountState extends State<_Acount> {
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: [
|
||||
_Card(title: 'Acount', children: [login()]),
|
||||
_Card(title: 'Account', children: [accountAction()]),
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin));
|
||||
}
|
||||
|
||||
Widget login() {
|
||||
Widget accountAction() {
|
||||
return _futureBuilder(future: () async {
|
||||
return await gFFI.userModel.getUserName();
|
||||
}(), hasData: (data) {
|
||||
@@ -730,12 +766,14 @@ class _AcountState extends State<_Acount> {
|
||||
return _Button(
|
||||
username.isEmpty ? 'Login' : 'Logout',
|
||||
() => {
|
||||
loginDialog().then((success) {
|
||||
if (success) {
|
||||
// refresh frame
|
||||
setState(() {});
|
||||
}
|
||||
})
|
||||
username.isEmpty
|
||||
? loginDialog().then((success) {
|
||||
if (success) {
|
||||
// refresh frame
|
||||
setState(() {});
|
||||
}
|
||||
})
|
||||
: gFFI.userModel.logOut()
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -859,7 +897,9 @@ Widget _Card({required String title, required List<Widget> children}) {
|
||||
}
|
||||
|
||||
Color? _disabledTextColor(BuildContext context, bool enabled) {
|
||||
return enabled ? null : MyTheme.color(context).lighterText;
|
||||
return enabled
|
||||
? null
|
||||
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
@@ -1339,91 +1379,6 @@ void changeServer() async {
|
||||
});
|
||||
}
|
||||
|
||||
void changeWhiteList({Function()? callback}) async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
||||
var newWhiteListField = newWhiteList.join('\n');
|
||||
var controller = TextEditingController(text: newWhiteListField);
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("IP Whitelisting")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus()),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await bind.mainSetOption(key: 'whitelist', value: '');
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Clear"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
newWhiteListField = controller.text.trim();
|
||||
var newWhiteList = "";
|
||||
if (newWhiteListField.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ips =
|
||||
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
// test ip
|
||||
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip)) {
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
newWhiteList = ips.join(',');
|
||||
}
|
||||
oldOptions['whitelist'] = newWhiteList;
|
||||
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void changeSocks5Proxy() async {
|
||||
var socks = await bind.mainGetSocks();
|
||||
|
||||
|
||||
@@ -14,6 +14,23 @@ class DesktopTabPage extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||
|
||||
static void onAddSetting({int initialPage = 0}) {
|
||||
try {
|
||||
DesktopTabController tabController = Get.find();
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelSettingPage,
|
||||
label: kTabLabelSettingPage,
|
||||
selectedIcon: Icons.build_sharp,
|
||||
unselectedIcon: Icons.build_outlined,
|
||||
page: DesktopSettingPage(
|
||||
key: const ValueKey(kTabLabelSettingPage),
|
||||
initialPage: initialPage,
|
||||
)));
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
@@ -22,6 +39,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelHomePage,
|
||||
label: kTabLabelHomePage,
|
||||
@@ -33,6 +51,12 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
)));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
Get.delete<DesktopTabController>();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
RxBool fullscreen = false.obs;
|
||||
@@ -42,13 +66,13 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
OverlayEntry(builder: (context) {
|
||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
onTap: onAddSetting,
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
isClose: false,
|
||||
),
|
||||
));
|
||||
@@ -62,13 +86,4 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
fullscreen.value ? kFullScreenEdgeSize : kWindowEdgeSize,
|
||||
child: tabWidget));
|
||||
}
|
||||
|
||||
void onAddSetting() {
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelSettingPage,
|
||||
label: kTabLabelSettingPage,
|
||||
selectedIcon: Icons.build_sharp,
|
||||
unselectedIcon: Icons.build_outlined,
|
||||
page: DesktopSettingPage(key: const ValueKey(kTabLabelSettingPage))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
Flexible(flex: 3, child: body(isLocal: true)),
|
||||
|
||||
@@ -72,7 +72,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
|
||||
@@ -70,7 +70,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).grayBg,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: FutureBuilder(future: () async {
|
||||
if (!widget.isRDP) {
|
||||
refreshTunnelConfig();
|
||||
@@ -80,7 +80,8 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||
width: 20,
|
||||
color: Theme.of(context).scaffoldBackgroundColor)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -88,7 +89,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
color: Theme.of(context).backgroundColor,
|
||||
border: Border.all(width: 1, color: MyTheme.border)),
|
||||
child:
|
||||
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
||||
@@ -131,7 +132,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||
.copyWith(backgroundColor: Theme.of(context).backgroundColor),
|
||||
child: Obx(() => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: pfs.length + 2,
|
||||
@@ -139,7 +140,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
if (index == 0) {
|
||||
return Container(
|
||||
height: 25,
|
||||
color: MyTheme.color(context).grayBg,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Row(children: [
|
||||
text('Local Port'),
|
||||
const SizedBox(width: _kColumn1Width),
|
||||
@@ -166,7 +167,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
|
||||
return Container(
|
||||
height: _kRowHeight,
|
||||
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||
decoration: BoxDecoration(color: Theme.of(context).backgroundColor),
|
||||
child: Row(children: [
|
||||
buildTunnelInputCell(context,
|
||||
controller: localPortController,
|
||||
@@ -216,11 +217,12 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
{required TextEditingController controller,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? hint}) {
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
return Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
inputFormatters: inputFormatters,
|
||||
cursorColor: MyTheme.color(context).text,
|
||||
cursorColor: textColor,
|
||||
cursorHeight: 20,
|
||||
cursorWidth: 1,
|
||||
decoration: InputDecoration(
|
||||
@@ -228,12 +230,12 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
borderSide: BorderSide(color: MyTheme.color(context).border!)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: MyTheme.color(context).border!)),
|
||||
fillColor: MyTheme.color(context).bg,
|
||||
fillColor: Theme.of(context).backgroundColor,
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(
|
||||
color: MyTheme.color(context).placeholder, fontSize: 16)),
|
||||
style: TextStyle(color: MyTheme.color(context).text, fontSize: 16),
|
||||
hintStyle:
|
||||
TextStyle(color: Theme.of(context).hintColor, fontSize: 16)),
|
||||
style: TextStyle(color: textColor, fontSize: 16),
|
||||
).marginAll(10),
|
||||
);
|
||||
}
|
||||
@@ -250,7 +252,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
? MyTheme.currentThemeMode() == ThemeMode.dark
|
||||
? const Color(0xFF202020)
|
||||
: const Color(0xFFF4F5F6)
|
||||
: MyTheme.color(context).bg),
|
||||
: Theme.of(context).backgroundColor),
|
||||
child: Row(children: [
|
||||
text(pf.localPort.toString()),
|
||||
const SizedBox(width: _kColumn1Width),
|
||||
@@ -292,7 +294,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
).marginOnly(left: _kTextLeftMargin));
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||
.copyWith(backgroundColor: Theme.of(context).backgroundColor),
|
||||
child: ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: 2,
|
||||
@@ -300,7 +302,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
if (index == 0) {
|
||||
return Container(
|
||||
height: 25,
|
||||
color: MyTheme.color(context).grayBg,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Row(children: [
|
||||
text1('Local Port'),
|
||||
const SizedBox(width: _kColumn1Width),
|
||||
@@ -311,7 +313,8 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
} else {
|
||||
return Container(
|
||||
height: _kRowHeight,
|
||||
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||
decoration:
|
||||
BoxDecoration(color: Theme.of(context).backgroundColor),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
|
||||
@@ -80,7 +80,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../widgets/remote_menubar.dart';
|
||||
import '../../common.dart';
|
||||
import '../../mobile/widgets/dialog.dart';
|
||||
@@ -45,7 +44,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
late RxBool _keyboardEnabled;
|
||||
|
||||
final FocusNode _rawKeyFocusNode = FocusNode();
|
||||
var _isPhysicalMouse = false;
|
||||
var _imageFocused = false;
|
||||
|
||||
Function(bool)? _onEnterOrLeaveImage4Menubar;
|
||||
@@ -139,7 +137,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
@@ -445,6 +443,7 @@ class ImagePainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class QualityMonitor extends StatelessWidget {
|
||||
static const textStyle = TextStyle(color: MyTheme.grayBg);
|
||||
final QualityMonitorModel qualityMonitorModel;
|
||||
QualityMonitor(this.qualityMonitorModel);
|
||||
|
||||
@@ -464,23 +463,23 @@ class QualityMonitor extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
"Speed: ${qualityMonitorModel.data.speed ?? ''}",
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
style: textStyle,
|
||||
),
|
||||
Text(
|
||||
"FPS: ${qualityMonitorModel.data.fps ?? ''}",
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
style: textStyle,
|
||||
),
|
||||
Text(
|
||||
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
style: textStyle,
|
||||
),
|
||||
Text(
|
||||
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
style: textStyle,
|
||||
),
|
||||
Text(
|
||||
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -91,7 +91,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
showTabBar: fullscreen.isFalse,
|
||||
|
||||
@@ -69,7 +69,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
OverlayEntry(builder: (context) {
|
||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
backgroundColor: Theme.of(context).backgroundColor,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -145,7 +145,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
windowManager.startDragging();
|
||||
},
|
||||
child: Container(
|
||||
color: MyTheme.color(context).bg,
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -310,14 +310,15 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
],
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: client.isFileTransfer,
|
||||
child: IconButton(
|
||||
onPressed: () => checkClickTime(
|
||||
client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)),
|
||||
icon: Icon(Icons.message_outlined),
|
||||
),
|
||||
)
|
||||
Consumer<ServerModel>(
|
||||
builder: (_, model, child) => Offstage(
|
||||
offstage: !client.authorized || client.isFileTransfer,
|
||||
child: IconButton(
|
||||
onPressed: () => checkClickTime(client.id,
|
||||
() => gFFI.chatModel.toggleCMChatPage(client.id)),
|
||||
icon: Icon(Icons.message_outlined),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
84
flutter/lib/desktop/widgets/button.dart
Normal file
84
flutter/lib/desktop/widgets/button.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
class Button extends StatefulWidget {
|
||||
GestureTapCallback onTap;
|
||||
String text;
|
||||
double? textSize;
|
||||
double? minWidth;
|
||||
bool isOutline;
|
||||
double? padding;
|
||||
Color? textColor;
|
||||
double? radius;
|
||||
Color? borderColor;
|
||||
|
||||
Button({
|
||||
Key? key,
|
||||
this.minWidth,
|
||||
this.isOutline = false,
|
||||
this.textSize,
|
||||
this.padding,
|
||||
this.textColor,
|
||||
this.radius,
|
||||
this.borderColor,
|
||||
required this.onTap,
|
||||
required this.text,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Button> createState() => _ButtonState();
|
||||
}
|
||||
|
||||
class _ButtonState extends State<Button> {
|
||||
RxBool hover = false.obs;
|
||||
RxBool pressed = false.obs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => InkWell(
|
||||
onTapDown: (_) => pressed.value = true,
|
||||
onTapUp: (_) => pressed.value = false,
|
||||
onTapCancel: () => pressed.value = false,
|
||||
onHover: (value) => hover.value = value,
|
||||
onTap: widget.onTap,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: widget.minWidth ?? 70.0,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(widget.padding ?? 4.5),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: pressed.value
|
||||
? MyTheme.accent
|
||||
: (widget.isOutline
|
||||
? Colors.transparent
|
||||
: MyTheme.button),
|
||||
border: Border.all(
|
||||
color: pressed.value
|
||||
? MyTheme.accent
|
||||
: hover.value
|
||||
? MyTheme.hoverBorder
|
||||
: (widget.isOutline
|
||||
? widget.borderColor ?? MyTheme.border
|
||||
: MyTheme.button),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(widget.radius ?? 5),
|
||||
),
|
||||
child: Text(
|
||||
translate(
|
||||
widget.text,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: widget.textSize ?? 12.0,
|
||||
color: pressed.value || !widget.isOutline
|
||||
? Theme.of(context).backgroundColor
|
||||
: widget.textColor ??
|
||||
Theme.of(context).textTheme.titleLarge?.color),
|
||||
).marginSymmetric(horizontal: 12),
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ import 'package:flutter/material.dart';
|
||||
// void setState(VoidCallback fn) { }
|
||||
// enum Menu { itemOne, itemTwo, itemThree, itemFour }
|
||||
|
||||
const Duration _kMenuDuration = Duration(milliseconds: 300);
|
||||
// const Duration _kMenuDuration = Duration(milliseconds: 300);
|
||||
const Duration _kMenuDuration = Duration(milliseconds: 0);
|
||||
const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
|
||||
const double _kMenuHorizontalPadding = 16.0;
|
||||
const double _kMenuDividerHeight = 16.0;
|
||||
@@ -22,7 +23,7 @@ const double _kMenuDividerHeight = 16.0;
|
||||
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
|
||||
const double _kMenuMaxWidth = double.infinity;
|
||||
// const double _kMenuVerticalPadding = 8.0;
|
||||
const double _kMenuVerticalPadding = 0.0;
|
||||
const double _kMenuVerticalPadding = 8.0;
|
||||
const double _kMenuWidthStep = 0.0;
|
||||
//const double _kMenuScreenPadding = 8.0;
|
||||
const double _kMenuScreenPadding = 0.0;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import './material_mod_popup_menu.dart' as mod_menu;
|
||||
@@ -78,7 +77,8 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
constraints: BoxConstraints(
|
||||
minHeight: widget.height, maxHeight: widget.height),
|
||||
padding:
|
||||
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
@@ -156,12 +156,14 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
final RadioCurOptionGetter curOptionGetter;
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
MenuEntryRadios({
|
||||
required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
this.padding,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) {
|
||||
@@ -189,30 +191,37 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
height: conf.height,
|
||||
child: TextButton(
|
||||
child: Container(
|
||||
padding: padding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
constraints:
|
||||
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: Obx(() => opt.value == curOption.value
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: conf.commonColor,
|
||||
)
|
||||
: const SizedBox.shrink())),
|
||||
)),
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: MenuConfig.iconScale,
|
||||
child: Obx(() => opt.value == curOption.value
|
||||
? IconButton(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
8.0, 0.0, 8.0, 0.0),
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.check,
|
||||
color: conf.commonColor,
|
||||
))
|
||||
: const SizedBox.shrink()),
|
||||
))),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -239,12 +248,14 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
final RadioCurOptionGetter curOptionGetter;
|
||||
final RadioOptionSetter optionSetter;
|
||||
final RxString _curOption = "".obs;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
MenuEntrySubRadios({
|
||||
required this.text,
|
||||
required this.optionsGetter,
|
||||
required this.curOptionGetter,
|
||||
required this.optionSetter,
|
||||
this.padding,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
@@ -275,28 +286,34 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
height: conf.height,
|
||||
child: TextButton(
|
||||
child: Container(
|
||||
padding: padding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
constraints:
|
||||
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: Transform.scale(
|
||||
scale: MenuConfig.iconScale,
|
||||
child: Obx(() => opt.value == curOption.value
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: conf.commonColor,
|
||||
)
|
||||
? IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
Icons.check,
|
||||
color: conf.commonColor,
|
||||
))
|
||||
: const SizedBox.shrink())),
|
||||
)),
|
||||
],
|
||||
@@ -318,7 +335,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
return [
|
||||
PopupMenuChildrenItem(
|
||||
enabled: super.enabled,
|
||||
padding: EdgeInsets.zero,
|
||||
padding: padding,
|
||||
height: conf.height,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(),
|
||||
@@ -327,7 +344,7 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@@ -345,28 +362,37 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
}
|
||||
}
|
||||
|
||||
enum SwitchType {
|
||||
sswitch,
|
||||
scheckbox,
|
||||
}
|
||||
|
||||
typedef SwitchGetter = Future<bool> Function();
|
||||
typedef SwitchSetter = Future<void> Function(bool);
|
||||
|
||||
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
final SwitchType switchType;
|
||||
final String text;
|
||||
final EdgeInsets? padding;
|
||||
Rx<TextStyle>? textStyle;
|
||||
|
||||
MenuEntrySwitchBase({
|
||||
required this.switchType,
|
||||
required this.text,
|
||||
required dismissOnClicked,
|
||||
this.textStyle,
|
||||
this.padding,
|
||||
RxBool? enabled,
|
||||
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
|
||||
|
||||
RxBool get curOption;
|
||||
Future<void> setOption(bool option);
|
||||
Future<void> setOption(bool? option);
|
||||
|
||||
@override
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
textStyle ??= const TextStyle(
|
||||
color: Colors.black,
|
||||
textStyle ??= TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal)
|
||||
.obs;
|
||||
@@ -376,6 +402,7 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
height: conf.height,
|
||||
child: TextButton(
|
||||
child: Container(
|
||||
padding: padding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
height: conf.height,
|
||||
child: Row(children: [
|
||||
@@ -386,16 +413,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Obx(() => Switch(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
)),
|
||||
child: Transform.scale(
|
||||
scale: MenuConfig.iconScale,
|
||||
child: Obx(() {
|
||||
if (switchType == SwitchType.sswitch) {
|
||||
return Switch(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Checkbox(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
))
|
||||
])),
|
||||
onPressed: () {
|
||||
@@ -416,15 +460,19 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||
final RxBool _curOption = false.obs;
|
||||
|
||||
MenuEntrySwitch({
|
||||
required SwitchType switchType,
|
||||
required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
EdgeInsets? padding,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
switchType: switchType,
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
padding: padding,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
) {
|
||||
@@ -436,11 +484,13 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||
@override
|
||||
RxBool get curOption => _curOption;
|
||||
@override
|
||||
setOption(bool option) async {
|
||||
await setter(option);
|
||||
final opt = await getter();
|
||||
if (_curOption.value != opt) {
|
||||
_curOption.value = opt;
|
||||
setOption(bool? option) async {
|
||||
if (option != null) {
|
||||
await setter(option);
|
||||
final opt = await getter();
|
||||
if (_curOption.value != opt) {
|
||||
_curOption.value = opt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,32 +503,40 @@ class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
|
||||
final SwitchSetter setter;
|
||||
|
||||
MenuEntrySwitch2({
|
||||
required SwitchType switchType,
|
||||
required String text,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
EdgeInsets? padding,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
switchType: switchType,
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
padding: padding,
|
||||
dismissOnClicked: dismissOnClicked);
|
||||
|
||||
@override
|
||||
RxBool get curOption => getter();
|
||||
@override
|
||||
setOption(bool option) async {
|
||||
await setter(option);
|
||||
setOption(bool? option) async {
|
||||
if (option != null) {
|
||||
await setter(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final List<MenuEntryBase<T>> entries;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
MenuEntrySubMenu({
|
||||
required this.text,
|
||||
required this.entries,
|
||||
this.padding,
|
||||
RxBool? enabled,
|
||||
}) : super(enabled: enabled);
|
||||
|
||||
@@ -490,7 +548,7 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
PopupMenuChildrenItem(
|
||||
enabled: super.enabled,
|
||||
height: conf.height,
|
||||
padding: EdgeInsets.zero,
|
||||
padding: padding,
|
||||
position: mod_menu.PopupMenuPosition.overSide,
|
||||
itemBuilder: (BuildContext context) => entries
|
||||
.map((entry) => entry.build(context, conf))
|
||||
@@ -501,7 +559,9 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
Obx(() => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: super.enabled!.value ? Colors.black : Colors.grey,
|
||||
color: super.enabled!.value
|
||||
? Theme.of(context).textTheme.titleLarge?.color
|
||||
: Colors.grey,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
)),
|
||||
@@ -522,10 +582,12 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
final Widget Function(TextStyle? style) childBuilder;
|
||||
Function() proc;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
MenuEntryButton({
|
||||
required this.childBuilder,
|
||||
required this.proc,
|
||||
this.padding,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
}) : super(
|
||||
@@ -534,8 +596,8 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
);
|
||||
|
||||
Widget _buildChild(BuildContext context, MenuConfig conf) {
|
||||
const enabledStyle = TextStyle(
|
||||
color: Colors.black,
|
||||
final enabledStyle = TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
const disabledStyle = TextStyle(
|
||||
@@ -553,8 +615,10 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
padding: padding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
constraints:
|
||||
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
||||
child: childBuilder(
|
||||
super.enabled!.value ? enabledStyle : disabledStyle),
|
||||
),
|
||||
|
||||
@@ -20,7 +20,7 @@ import './material_mod_popup_menu.dart' as mod_menu;
|
||||
class _MenubarTheme {
|
||||
static const Color commonColor = MyTheme.accent;
|
||||
// kMinInteractiveDimension
|
||||
static const double height = 25.0;
|
||||
static const double height = 20.0;
|
||||
static const double dividerHeight = 12.0;
|
||||
}
|
||||
|
||||
@@ -391,31 +391,41 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
List<MenuEntryBase<String>> _getControlMenu(BuildContext context) {
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
final perms = widget.ffi.ffiModel.permissions;
|
||||
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
|
||||
final List<MenuEntryBase<String>> displayMenu = [];
|
||||
displayMenu.addAll([
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('OS Password'),
|
||||
style: style,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => showSetOSPassword(
|
||||
widget.id, false, widget.ffi.dialogManager),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
height: _MenubarTheme.height,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('OS Password'),
|
||||
style: style,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
showSetOSPassword(
|
||||
widget.id, false, widget.ffi.dialogManager);
|
||||
})),
|
||||
))
|
||||
],
|
||||
)),
|
||||
proc: () {
|
||||
showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryButton<String>(
|
||||
@@ -426,6 +436,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
connect(context, widget.id, isFileTransfer: true);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryButton<String>(
|
||||
@@ -433,6 +444,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
translate('TCP Tunneling'),
|
||||
style: style,
|
||||
),
|
||||
padding: padding,
|
||||
proc: () {
|
||||
connect(context, widget.id, isTcpTunneling: true);
|
||||
},
|
||||
@@ -451,6 +463,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
showAuditDialog(widget.id, widget.ffi.dialogManager);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
);
|
||||
@@ -467,6 +480,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
bind.sessionCtrlAltDel(id: widget.id);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
@@ -483,6 +497,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
@@ -496,6 +511,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
bind.sessionLockScreen(id: widget.id);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
|
||||
@@ -513,6 +529,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
value: '${blockInput.value ? "un" : ""}block-input');
|
||||
blockInput.value = !blockInput.value;
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
@@ -527,6 +544,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
bind.sessionRefresh(id: widget.id);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
@@ -547,6 +565,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
// }
|
||||
// }();
|
||||
// },
|
||||
// padding: padding,
|
||||
// dismissOnClicked: true,
|
||||
// ));
|
||||
// }
|
||||
@@ -559,6 +578,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
proc: () {
|
||||
widget.ffi.cursorModel.reset();
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
@@ -567,125 +587,155 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
}
|
||||
|
||||
List<MenuEntryBase<String>> _getDisplayMenu(dynamic futureData) {
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0);
|
||||
final displayMenu = [
|
||||
MenuEntryRadios<String>(
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale original'), value: 'original'),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale adaptive'), value: 'adaptive'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'view-style') ??
|
||||
'adaptive';
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "view-style", value: newValue);
|
||||
widget.ffi.canvasModel.updateViewStyle();
|
||||
}),
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale original'),
|
||||
value: 'original',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scale adaptive'),
|
||||
value: 'adaptive',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'view-style') ??
|
||||
'adaptive';
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "view-style", value: newValue);
|
||||
widget.ffi.canvasModel.updateViewStyle();
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryDivider<String>(),
|
||||
MenuEntryRadios<String>(
|
||||
text: translate('Scroll Style'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('ScrollAuto'), value: 'scrollauto'),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scrollbar'), value: 'scrollbar'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'scroll-style') ??
|
||||
'';
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "scroll-style", value: newValue);
|
||||
widget.ffi.canvasModel.updateScrollStyle();
|
||||
}),
|
||||
text: translate('Scroll Style'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('ScrollAuto'),
|
||||
value: 'scrollauto',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Scrollbar'),
|
||||
value: 'scrollbar',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'scroll-style') ??
|
||||
'';
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "scroll-style", value: newValue);
|
||||
widget.ffi.canvasModel.updateScrollStyle();
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryDivider<String>(),
|
||||
MenuEntryRadios<String>(
|
||||
text: translate('Image Quality'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Good image quality'), value: 'best'),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Balanced'), value: 'balanced'),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Optimize reaction time'), value: 'low'),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Custom'),
|
||||
value: 'custom',
|
||||
dismissOnClicked: true),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
String quality =
|
||||
await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced';
|
||||
if (quality == '') quality = 'balanced';
|
||||
return quality;
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
if (oldValue != newValue) {
|
||||
await bind.sessionSetImageQuality(id: widget.id, value: newValue);
|
||||
}
|
||||
text: translate('Image Quality'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Good image quality'),
|
||||
value: 'best',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Balanced'),
|
||||
value: 'balanced',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Optimize reaction time'),
|
||||
value: 'low',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Custom'),
|
||||
value: 'custom',
|
||||
dismissOnClicked: true),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
String quality =
|
||||
await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced';
|
||||
if (quality == '') quality = 'balanced';
|
||||
return quality;
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
if (oldValue != newValue) {
|
||||
await bind.sessionSetImageQuality(id: widget.id, value: newValue);
|
||||
}
|
||||
|
||||
if (newValue == 'custom') {
|
||||
final btnCancel = msgBoxButton(translate('Close'), () {
|
||||
widget.ffi.dialogManager.dismissAll();
|
||||
});
|
||||
final quality =
|
||||
await bind.sessionGetCustomImageQuality(id: widget.id);
|
||||
double initValue = quality != null && quality.isNotEmpty
|
||||
? quality[0].toDouble()
|
||||
: 50.0;
|
||||
const minValue = 10.0;
|
||||
const maxValue = 100.0;
|
||||
if (initValue < minValue) {
|
||||
initValue = minValue;
|
||||
}
|
||||
if (initValue > maxValue) {
|
||||
initValue = maxValue;
|
||||
}
|
||||
final RxDouble sliderValue = RxDouble(initValue);
|
||||
final rxReplay = rxdart.ReplaySubject<double>();
|
||||
rxReplay
|
||||
.throttleTime(const Duration(milliseconds: 1000),
|
||||
trailing: true, leading: false)
|
||||
.listen((double v) {
|
||||
() async {
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
id: widget.id, value: v.toInt());
|
||||
}();
|
||||
});
|
||||
final slider = Obx(() {
|
||||
return Slider(
|
||||
value: sliderValue.value,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
divisions: 90,
|
||||
onChanged: (double value) {
|
||||
sliderValue.value = value;
|
||||
rxReplay.add(value);
|
||||
},
|
||||
);
|
||||
});
|
||||
final content = Row(
|
||||
children: [
|
||||
slider,
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Obx(() => Text(
|
||||
'${sliderValue.value.round()}% Bitrate',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)))
|
||||
],
|
||||
);
|
||||
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
|
||||
content, [btnCancel]);
|
||||
if (newValue == 'custom') {
|
||||
final btnCancel = msgBoxButton(translate('Close'), () {
|
||||
widget.ffi.dialogManager.dismissAll();
|
||||
});
|
||||
final quality =
|
||||
await bind.sessionGetCustomImageQuality(id: widget.id);
|
||||
double initValue = quality != null && quality.isNotEmpty
|
||||
? quality[0].toDouble()
|
||||
: 50.0;
|
||||
const minValue = 10.0;
|
||||
const maxValue = 100.0;
|
||||
if (initValue < minValue) {
|
||||
initValue = minValue;
|
||||
}
|
||||
}),
|
||||
if (initValue > maxValue) {
|
||||
initValue = maxValue;
|
||||
}
|
||||
final RxDouble sliderValue = RxDouble(initValue);
|
||||
final rxReplay = rxdart.ReplaySubject<double>();
|
||||
rxReplay
|
||||
.throttleTime(const Duration(milliseconds: 1000),
|
||||
trailing: true, leading: false)
|
||||
.listen((double v) {
|
||||
() async {
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
id: widget.id, value: v.toInt());
|
||||
}();
|
||||
});
|
||||
final slider = Obx(() {
|
||||
return Slider(
|
||||
value: sliderValue.value,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
divisions: 90,
|
||||
onChanged: (double value) {
|
||||
sliderValue.value = value;
|
||||
rxReplay.add(value);
|
||||
},
|
||||
);
|
||||
});
|
||||
final content = Row(
|
||||
children: [
|
||||
slider,
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Obx(() => Text(
|
||||
'${sliderValue.value.round()}% Bitrate',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)))
|
||||
],
|
||||
);
|
||||
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
|
||||
content, [btnCancel]);
|
||||
}
|
||||
},
|
||||
padding: padding,
|
||||
),
|
||||
MenuEntryDivider<String>(),
|
||||
];
|
||||
|
||||
@@ -701,30 +751,49 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
} finally {}
|
||||
if (codecs.length == 2 && (codecs[0] || codecs[1])) {
|
||||
displayMenu.add(MenuEntryRadios<String>(
|
||||
text: translate('Codec Preference'),
|
||||
optionsGetter: () {
|
||||
final list = [
|
||||
MenuEntryRadioOption(text: translate('Auto'), value: 'auto'),
|
||||
MenuEntryRadioOption(text: 'VP9', value: 'vp9'),
|
||||
];
|
||||
if (codecs[0]) {
|
||||
list.add(MenuEntryRadioOption(text: 'H264', value: 'h264'));
|
||||
}
|
||||
if (codecs[1]) {
|
||||
list.add(MenuEntryRadioOption(text: 'H265', value: 'h265'));
|
||||
}
|
||||
return list;
|
||||
},
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'codec-preference') ??
|
||||
'auto';
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "codec-preference", value: newValue);
|
||||
bind.sessionChangePreferCodec(id: widget.id);
|
||||
}));
|
||||
text: translate('Codec Preference'),
|
||||
optionsGetter: () {
|
||||
final list = [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Auto'),
|
||||
value: 'auto',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryRadioOption(
|
||||
text: 'VP9',
|
||||
value: 'vp9',
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
];
|
||||
if (codecs[0]) {
|
||||
list.add(MenuEntryRadioOption(
|
||||
text: 'H264',
|
||||
value: 'h264',
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
if (codecs[1]) {
|
||||
list.add(MenuEntryRadioOption(
|
||||
text: 'H265',
|
||||
value: 'h265',
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
return list;
|
||||
},
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetOption(
|
||||
id: widget.id, arg: 'codec-preference') ??
|
||||
'auto';
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionPeerOption(
|
||||
id: widget.id, name: "codec-preference", value: newValue);
|
||||
bind.sessionChangePreferCodec(id: widget.id);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,62 +801,74 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
displayMenu.add(() {
|
||||
final state = ShowRemoteCursorState.find(widget.id);
|
||||
return MenuEntrySwitch2<String>(
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () {
|
||||
return state;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
state.value = v;
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
});
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () {
|
||||
return state;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
state.value = v;
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
}());
|
||||
|
||||
/// Show quality monitor
|
||||
displayMenu.add(MenuEntrySwitch<String>(
|
||||
text: translate('Show quality monitor'),
|
||||
getter: () async {
|
||||
return 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);
|
||||
}));
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Show quality monitor'),
|
||||
getter: () async {
|
||||
return 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);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
|
||||
final perms = widget.ffi.ffiModel.permissions;
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
|
||||
if (perms['audio'] != false) {
|
||||
displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio'));
|
||||
displayMenu
|
||||
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
|
||||
}
|
||||
|
||||
if (Platform.isWindows &&
|
||||
pi.platform == 'Windows' &&
|
||||
perms['file'] != false) {
|
||||
displayMenu.add(_createSwitchMenuEntry(
|
||||
'Allow file copy and paste', 'enable-file-transfer'));
|
||||
'Allow file copy and paste', 'enable-file-transfer', padding, true));
|
||||
}
|
||||
|
||||
if (perms['keyboard'] != false) {
|
||||
if (perms['clipboard'] != false) {
|
||||
displayMenu.add(
|
||||
_createSwitchMenuEntry('Disable clipboard', 'disable-clipboard'));
|
||||
displayMenu.add(_createSwitchMenuEntry(
|
||||
'Disable clipboard', 'disable-clipboard', padding, true));
|
||||
}
|
||||
displayMenu.add(_createSwitchMenuEntry(
|
||||
'Lock after session end', 'lock-after-session-end'));
|
||||
'Lock after session end', 'lock-after-session-end', padding, true));
|
||||
if (pi.platform == 'Windows') {
|
||||
displayMenu.add(MenuEntrySwitch2<String>(
|
||||
dismissOnClicked: true,
|
||||
text: translate('Privacy mode'),
|
||||
getter: () {
|
||||
return PrivacyModeState.find(widget.id);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'privacy-mode');
|
||||
}));
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Privacy mode'),
|
||||
getter: () {
|
||||
return PrivacyModeState.find(widget.id);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'privacy-mode');
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
return displayMenu;
|
||||
@@ -796,34 +877,39 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
List<MenuEntryBase<String>> _getKeyboardMenu() {
|
||||
final keyboardMenu = [
|
||||
MenuEntryRadios<String>(
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(
|
||||
text: translate('Legacy mode'), value: 'legacy'),
|
||||
MenuEntryRadioOption(text: translate('Map mode'), value: 'map'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetKeyboardName(id: widget.id);
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionSetKeyboardMode(
|
||||
id: widget.id, keyboardMode: newValue);
|
||||
widget.ffi.canvasModel.updateViewStyle();
|
||||
})
|
||||
text: translate('Ratio'),
|
||||
optionsGetter: () => [
|
||||
MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'),
|
||||
MenuEntryRadioOption(text: translate('Map mode'), value: 'map'),
|
||||
],
|
||||
curOptionGetter: () async {
|
||||
return await bind.sessionGetKeyboardName(id: widget.id);
|
||||
},
|
||||
optionSetter: (String oldValue, String newValue) async {
|
||||
await bind.sessionSetKeyboardMode(
|
||||
id: widget.id, keyboardMode: newValue);
|
||||
widget.ffi.canvasModel.updateViewStyle();
|
||||
},
|
||||
)
|
||||
];
|
||||
|
||||
return keyboardMenu;
|
||||
}
|
||||
|
||||
MenuEntrySwitch<String> _createSwitchMenuEntry(String text, String option) {
|
||||
MenuEntrySwitch<String> _createSwitchMenuEntry(
|
||||
String text, String option, EdgeInsets? padding, bool dismissOnClicked) {
|
||||
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);
|
||||
});
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate(text),
|
||||
getter: () async {
|
||||
return bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(id: widget.id, value: option);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -490,6 +490,15 @@ class _ListView extends StatelessWidget {
|
||||
this.tabBuilder,
|
||||
this.labelGetter});
|
||||
|
||||
/// Check whether to show ListView
|
||||
///
|
||||
/// Conditions:
|
||||
/// - hide single item when only has one item (home) on [DesktopTabPage].
|
||||
bool isHideSingleItem() {
|
||||
return state.value.tabs.length == 1 &&
|
||||
controller.tabType == DesktopTabType.main;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => ListView(
|
||||
@@ -497,38 +506,41 @@ class _ListView extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: state.value.tabs.asMap().entries.map((e) {
|
||||
final index = e.key;
|
||||
final tab = e.value;
|
||||
return _Tab(
|
||||
index: index,
|
||||
label: labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label),
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
selected: state.value.selected,
|
||||
onClose: () {
|
||||
if (tab.onTabCloseButton != null) {
|
||||
tab.onTabCloseButton!();
|
||||
} else {
|
||||
controller.remove(index);
|
||||
}
|
||||
},
|
||||
onSelected: () => controller.jumpTo(index),
|
||||
tabBuilder: tabBuilder == null
|
||||
? null
|
||||
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
||||
return tabBuilder!(
|
||||
tab.label,
|
||||
icon,
|
||||
labelWidget,
|
||||
themeConf,
|
||||
);
|
||||
children: isHideSingleItem()
|
||||
? List.empty()
|
||||
: state.value.tabs.asMap().entries.map((e) {
|
||||
final index = e.key;
|
||||
final tab = e.value;
|
||||
return _Tab(
|
||||
index: index,
|
||||
label: labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label),
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
selected: state.value.selected,
|
||||
onClose: () {
|
||||
if (tab.onTabCloseButton != null) {
|
||||
tab.onTabCloseButton!();
|
||||
} else {
|
||||
controller.remove(index);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList()));
|
||||
onSelected: () => controller.jumpTo(index),
|
||||
tabBuilder: tabBuilder == null
|
||||
? null
|
||||
: (Widget icon, Widget labelWidget,
|
||||
TabThemeConf themeConf) {
|
||||
return tabBuilder!(
|
||||
tab.label,
|
||||
icon,
|
||||
labelWidget,
|
||||
themeConf,
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user