import 'dart:async'; import 'dart:convert'; import 'dart:io'; 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/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; class DesktopHomePage extends StatefulWidget { DesktopHomePage({Key? key}) : super(key: key); @override State createState() => _DesktopHomePageState(); } const borderColor = Color(0xFF2F65BA); class _DesktopHomePageState extends State with TrayListener, WindowListener { @override void onWindowClose() async { super.onWindowClose(); // close all sub windows if (await windowManager.isPreventClose()) { try { await rustDeskWinManager.closeAllSubWindows(); } catch (err) { debugPrint("$err"); } finally { await windowManager.setPreventClose(false); await windowManager.close(); } } } @override Widget build(BuildContext context) { return Row( children: [ Flexible( child: buildServerInfo(context), flex: 1, ), Flexible( child: buildServerBoard(context), flex: 4, ), ], ); } buildServerInfo(BuildContext context) { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Container( child: Column( children: [ buildTip(context), buildIDBoard(context), buildPasswordBoard(context), ], ), ), ); } buildServerBoard(BuildContext context) { return Column( children: [ Expanded(child: ConnectionPage()), ], ); } buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Container( width: 3, height: 70, decoration: BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( translate("ID"), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500), ), buildPopupMenu(context) ], ), GestureDetector( onDoubleTap: () { Clipboard.setData( ClipboardData(text: model.serverId.text)); showToast(translate("Copied")); }, child: TextFormField( controller: model.serverId, readOnly: true, )), ], ), ), ), ], ), ); } Widget buildPopupMenu(BuildContext context) { var position; return GestureDetector( onTapDown: (detail) { final x = detail.globalPosition.dx; final y = detail.globalPosition.dy; position = RelativeRect.fromLTRB(x, y, x, y); }, onTap: () async { final userName = await gFFI.userModel.getUserName(); final enabledInput = await bind.mainGetOption(key: 'enable-audio'); final defaultInput = await gFFI.getDefaultAudioInput(); var menu = [ await genEnablePopupMenuItem( translate("Enable Keyboard/Mouse"), 'enable-keyboard', ), await genEnablePopupMenuItem( translate("Enable Clipboard"), 'enable-clipboard', ), await genEnablePopupMenuItem( translate("Enable File Transfer"), 'enable-file-transfer', ), await genEnablePopupMenuItem( translate("Enable TCP Tunneling"), 'enable-tunnel', ), genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), PopupMenuDivider(), PopupMenuItem( child: Text(translate("ID/Relay Server")), value: 'custom-server', ), PopupMenuItem( child: Text(translate("IP Whitelisting")), value: 'whitelist', ), PopupMenuItem( child: Text(translate("Socks5 Proxy")), value: 'socks5-proxy', ), PopupMenuDivider(), await genEnablePopupMenuItem( translate("Enable Service"), 'stop-service', ), // TODO: direct server await genEnablePopupMenuItem( translate("Always connected via relay"), 'allow-always-relay', ), await genEnablePopupMenuItem( translate("Start ID/relay service"), 'stop-rendezvous-service', ), PopupMenuDivider(), userName.isEmpty ? PopupMenuItem( child: Text(translate("Login")), value: 'login', ) : PopupMenuItem( child: Text("${translate("Logout")} $userName"), value: 'logout', ), PopupMenuItem( child: Text(translate("Change ID")), value: 'change-id', ), PopupMenuDivider(), await genEnablePopupMenuItem( translate("Dark Theme"), 'allow-darktheme', ), PopupMenuItem( child: Text(translate("About")), value: 'about', ), ]; final v = await showMenu(context: context, position: position, items: menu); if (v != null) { onSelectMenu(v); } }, child: Icon(Icons.more_vert_outlined)); } buildPasswordBoard(BuildContext context) { final model = gFFI.serverModel; return Container( margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Container( width: 3, height: 70, decoration: BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( translate("Password"), style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), ), Row( children: [ Expanded( child: GestureDetector( onDoubleTap: () { if (model.verificationMethod != kUsePermanentPassword) { Clipboard.setData( ClipboardData(text: model.serverPasswd.text)); showToast(translate("Copied")); } }, child: TextFormField( controller: model.serverPasswd, readOnly: true, ), ), ), IconButton( icon: Icon(Icons.refresh), onPressed: () => bind.mainUpdateTemporaryPassword(), ), FutureBuilder( future: buildPasswordPopupMenu(context), builder: (context, snapshot) { if (snapshot.hasError) { print("${snapshot.error}"); } if (snapshot.hasData) { return snapshot.data!; } else { return Offstage(); } }) ], ), ], ), ), ), ], ), ); } Future buildPasswordPopupMenu(BuildContext context) async { var position; return GestureDetector( onTapDown: (detail) { final x = detail.globalPosition.dx; final y = detail.globalPosition.dy; position = RelativeRect.fromLTRB(x, y, x, y); }, onTap: () async { var method = (String text, String value) => PopupMenuItem( child: Row( children: [ Offstage( offstage: gFFI.serverModel.verificationMethod != value, child: Icon(Icons.check)), Text( text, ), ], ), onTap: () => gFFI.serverModel.verificationMethod = value, ); final temporary_enabled = gFFI.serverModel.verificationMethod != kUsePermanentPassword; var menu = [ method(translate("Use temporary password"), kUseTemporaryPassword), method(translate("Use permanent password"), kUsePermanentPassword), method(translate("Use both passwords"), kUseBothPasswords), PopupMenuDivider(), PopupMenuItem( child: Text(translate("Set permanent password")), value: 'set-permanent-password', enabled: gFFI.serverModel.verificationMethod != kUseTemporaryPassword), PopupMenuItem( child: PopupMenuButton( padding: EdgeInsets.zero, child: Text( translate("Set temporary password length"), ), itemBuilder: (context) => ["6", "8", "10"] .map((e) => PopupMenuItem( child: Row( children: [ Offstage( offstage: gFFI.serverModel .temporaryPasswordLength != e, child: Icon(Icons.check)), Text( e, ), ], ), onTap: () { if (gFFI.serverModel.temporaryPasswordLength != e) { gFFI.serverModel.temporaryPasswordLength = e; bind.mainUpdateTemporaryPassword(); } }, )) .toList(), enabled: temporary_enabled, ), enabled: temporary_enabled), ]; final v = await showMenu(context: context, position: position, items: menu); if (v == "set-permanent-password") { setPasswordDialog(); } }, child: Icon(Icons.edit)); } buildTip(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( translate("Your Desktop"), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), ), SizedBox( height: 8.0, ), Text( translate("desk_tip"), overflow: TextOverflow.clip, style: TextStyle(fontSize: 14), ) ], ), ); } buildControlPanel(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: MyTheme.white), padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("Control Remote Desktop")), Form( child: Column( children: [ TextFormField( controller: TextEditingController(), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) ], ) ], )) ], ), ); } buildRecentSession(BuildContext context) { return Center(child: Text("waiting implementation")); } @override void onTrayMenuItemClick(MenuItem menuItem) { print("click ${menuItem.key}"); switch (menuItem.key) { case "quit": exit(0); case "show": // windowManager.show(); break; default: break; } } @override void initState() { super.initState(); trayManager.addListener(this); windowManager.addListener(this); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); if (call.method == "main_window_on_top") { window_on_top(null); } }); } @override void dispose() { trayManager.removeListener(this); windowManager.removeListener(this); super.dispose(); } void changeTheme(String choice) async { if (choice == "Y") { Get.changeTheme(MyTheme.darkTheme); } else { Get.changeTheme(MyTheme.lightTheme); } Get.find().setString("darkTheme", choice); Get.forceAppUpdate(); } void onSelectMenu(String key) async { if (key.startsWith('enable-')) { final option = await bind.mainGetOption(key: key); bind.mainSetOption(key: key, value: option == "N" ? "" : "N"); } else if (key.startsWith('allow-')) { final option = await bind.mainGetOption(key: key); final choice = option == "Y" ? "" : "Y"; bind.mainSetOption(key: key, value: choice); if (key == "allow-darktheme") changeTheme(choice); } else if (key == "stop-service") { final option = await bind.mainGetOption(key: key); bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); } else if (key == "change-id") { changeId(); } else if (key == "custom-server") { changeServer(); } else if (key == "whitelist") { changeWhiteList(); } else if (key == "socks5-proxy") { changeSocks5Proxy(); } else if (key == "about") { about(); } else if (key == "logout") { logOut(); } else if (key == "login") { login(); } } Future> genEnablePopupMenuItem( String label, String key) async { final v = await bind.mainGetOption(key: key); bool enable; if (key == "stop-service") { enable = v != "Y"; } else if (key.startsWith("allow-")) { enable = v == "Y"; } else { enable = v != "N"; } return PopupMenuItem( child: Row( children: [ Icon(Icons.check, color: enable ? null : MyTheme.accent.withAlpha(00)), Text( label, style: genTextStyle(enable), ), ], ), value: key, ); } TextStyle genTextStyle(bool isPositive) { return isPositive ? TextStyle() : TextStyle( color: Colors.redAccent, decoration: TextDecoration.lineThrough); } PopupMenuItem genAudioInputPopupMenuItem( bool enableInput, String defaultAudioInput) { final defaultInput = defaultAudioInput.obs; final enabled = enableInput.obs; return PopupMenuItem( child: FutureBuilder>( future: gFFI.getAudioInputs(), builder: (context, snapshot) { if (snapshot.hasData) { final inputs = snapshot.data!.toList(); if (Platform.isWindows) { inputs.insert(0, translate("System Sound")); } var inputList = inputs .map((e) => PopupMenuItem( child: Row( children: [ Obx(() => Offstage( offstage: defaultInput.value != e, child: Icon(Icons.check))), Expanded( child: Tooltip( message: e, child: Text( "$e", maxLines: 1, overflow: TextOverflow.ellipsis, ))), ], ), value: e, )) .toList(); inputList.insert( 0, PopupMenuItem( child: Row( children: [ Obx(() => Offstage( offstage: enabled.value, child: Icon(Icons.check))), Expanded(child: Text(translate("Mute"))), ], ), value: "Mute", )); return PopupMenuButton( padding: EdgeInsets.zero, child: Container( alignment: Alignment.centerLeft, child: Text(translate("Audio Input"))), itemBuilder: (context) => inputList, onSelected: (dev) async { if (dev == "Mute") { await bind.mainSetOption( key: 'enable-audio', value: enabled.value ? '' : 'N'); enabled.value = await bind.mainGetOption(key: 'enable-audio') != 'N'; } else if (dev != await gFFI.getDefaultAudioInput()) { gFFI.setDefaultAudioInput(dev); defaultInput.value = dev; } }, ); } else { return Text("..."); } }, ), value: 'audio-input', ); } /// change local ID void changeId() { var newId = ""; var msg = ""; var isInProgress = false; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Change ID")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("id_change_tip")), SizedBox( height: 8.0, ), Row( children: [ Text("ID:").marginOnly(bottom: 16.0), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { newId = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) ], maxLength: 16, ), ), ], ), SizedBox( height: 4.0, ), Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) ], ), actions: [ TextButton( onPressed: () { close(); }, child: Text(translate("Cancel"))), TextButton( onPressed: () async { setState(() { msg = ""; isInProgress = true; bind.mainChangeId(newId: newId); }); var status = await bind.mainGetAsyncStatus(); while (status == " ") { await Future.delayed(Duration(milliseconds: 100)); status = await bind.mainGetAsyncStatus(); } if (status.isEmpty) { // ok close(); return; } setState(() { isInProgress = false; msg = translate(status); }); }, child: Text(translate("OK"))), ], ); }); } void changeServer() async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; String relayServer = oldOptions['relay-server'] ?? ""; var relayServerMsg = ""; String apiServer = oldOptions['api-server'] ?? ""; var apiServerMsg = ""; var key = oldOptions['key'] ?? ""; var isInProgress = false; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("ID/Relay Server")), content: ConstrainedBox( constraints: BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('ID Server')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { idServer = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: idServerMsg.isNotEmpty ? idServerMsg : null), controller: TextEditingController(text: idServer), ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Relay Server')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { relayServer = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: relayServerMsg.isNotEmpty ? relayServerMsg : null), controller: TextEditingController(text: relayServer), ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('API Server')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { apiServer = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: apiServerMsg.isNotEmpty ? apiServerMsg : null), controller: TextEditingController(text: apiServer), ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Key')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { key = s; }, decoration: InputDecoration( border: OutlineInputBorder(), ), controller: TextEditingController(text: key), ), ), ], ), SizedBox( height: 4.0, ), Offstage( offstage: !isInProgress, child: LinearProgressIndicator()) ], ), ), actions: [ TextButton( onPressed: () { close(); }, child: Text(translate("Cancel"))), TextButton( onPressed: () async { setState(() { [idServerMsg, relayServerMsg, apiServerMsg] .forEach((element) { element = ""; }); isInProgress = true; }); final cancel = () { setState(() { isInProgress = false; }); }; idServer = idServer.trim(); relayServer = relayServer.trim(); apiServer = apiServer.trim(); key = key.trim(); if (idServer.isNotEmpty) { idServerMsg = translate( await bind.mainTestIfValidServer(server: idServer)); if (idServerMsg.isEmpty) { oldOptions['custom-rendezvous-server'] = idServer; } else { cancel(); return; } } else { oldOptions['custom-rendezvous-server'] = ""; } if (relayServer.isNotEmpty) { relayServerMsg = translate( await bind.mainTestIfValidServer(server: relayServer)); if (relayServerMsg.isEmpty) { oldOptions['relay-server'] = relayServer; } else { cancel(); return; } } else { oldOptions['relay-server'] = ""; } if (apiServer.isNotEmpty) { if (apiServer.startsWith('http://') || apiServer.startsWith("https://")) { oldOptions['api-server'] = apiServer; return; } else { apiServerMsg = translate("invalid_http"); cancel(); return; } } else { oldOptions['api-server'] = ""; } // ok oldOptions['key'] = key; await bind.mainSetOptions(json: jsonEncode(oldOptions)); close(); }, child: Text(translate("OK"))), ], ); }); } void changeWhiteList() async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); var msg = ""; var isInProgress = false; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("IP Whitelisting")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( onChanged: (s) { newWhiteListField = s; }, maxLines: null, decoration: InputDecoration( border: OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), controller: TextEditingController(text: newWhiteListField), ), ), ], ), SizedBox( height: 4.0, ), Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) ], ), actions: [ TextButton( onPressed: () { close(); }, child: Text(translate("Cancel"))), TextButton( onPressed: () async { setState(() { msg = ""; isInProgress = true; }); newWhiteListField = newWhiteListField.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)); close(); }, child: Text(translate("OK"))), ], ); }); } void changeSocks5Proxy() async { var socks = await bind.mainGetSocks(); String proxy = ""; String proxyMsg = ""; String username = ""; String password = ""; if (socks.length == 3) { proxy = socks[0]; username = socks[1]; password = socks[2]; } var isInProgress = false; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Socks5 Proxy")), content: ConstrainedBox( constraints: BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Hostname')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { proxy = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: TextEditingController(text: proxy), ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Username')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { username = s; }, decoration: InputDecoration( border: OutlineInputBorder(), ), controller: TextEditingController(text: username), ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { password = s; }, decoration: InputDecoration( border: OutlineInputBorder(), ), controller: TextEditingController(text: password), ), ), ], ), SizedBox( height: 8.0, ), Offstage( offstage: !isInProgress, child: LinearProgressIndicator()) ], ), ), actions: [ TextButton( onPressed: () { close(); }, child: Text(translate("Cancel"))), TextButton( onPressed: () async { setState(() { proxyMsg = ""; isInProgress = true; }); final cancel = () { setState(() { isInProgress = false; }); }; proxy = proxy.trim(); username = username.trim(); password = password.trim(); if (proxy.isNotEmpty) { proxyMsg = translate( await bind.mainTestIfValidServer(server: proxy)); if (proxyMsg.isEmpty) { // ignore } else { cancel(); return; } } await bind.mainSetSocks( proxy: proxy, username: username, password: password); close(); }, child: Text(translate("OK"))), ], ); }); } void about() async { final appName = await bind.mainGetAppName(); final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final linkStyle = TextStyle(decoration: TextDecoration.underline); DialogManager.show((setState, close) { return CustomAlertDialog( title: Text("About $appName"), content: ConstrainedBox( constraints: BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 8.0, ), Text("Version: $version").marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString("https://rustdesk.com/privacy"); }, child: Text( "Privacy Statement", style: linkStyle, ).marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString("https://rustdesk.com"); }, child: Text( "Website", style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( decoration: BoxDecoration(color: Color(0xFF2c8cff)), padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Copyright © 2022 Purslane Ltd.\n$license", style: TextStyle(color: Colors.white), ), Text( "Made with heart in this chaotic world!", style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white), ) ], ), ), ], ), ).marginSymmetric(vertical: 4.0) ], ), ), actions: [ TextButton( onPressed: () async { close(); }, child: Text(translate("OK"))), ], ); }); } void login() { loginDialog().then((success) { if (success) { // refresh frame setState(() {}); } }); } void logOut() { gFFI.userModel.logOut().then((_) => {setState(() {})}); } } /// common login dialog for desktop /// call this directly Future loginDialog() async { String userName = ""; var userNameMsg = ""; String pass = ""; var passMsg = ""; var isInProgress = false; var completer = Completer(); DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( constraints: BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text( "${translate('Username')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( onChanged: (s) { userName = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), controller: TextEditingController(text: userName), ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, onChanged: (s) { pass = s; }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: passMsg.isNotEmpty ? passMsg : null), controller: TextEditingController(text: pass), ), ), ], ), SizedBox( height: 4.0, ), Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) ], ), ), actions: [ TextButton( onPressed: () { completer.complete(false); close(); }, child: Text(translate("Cancel"))), TextButton( onPressed: () async { setState(() { userNameMsg = ""; passMsg = ""; isInProgress = true; }); final cancel = () { setState(() { isInProgress = false; }); }; userName = userName; pass = pass; if (userName.isEmpty) { userNameMsg = translate("Username missed"); cancel(); return; } if (pass.isEmpty) { passMsg = translate("Password missed"); cancel(); return; } try { final resp = await gFFI.userModel.login(userName, pass); if (resp.containsKey('error')) { passMsg = resp['error']; cancel(); return; } // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} debugPrint("$resp"); completer.complete(true); } catch (err) { print(err.toString()); cancel(); return; } close(); }, child: Text(translate("OK"))), ], ); }); return completer.future; } void setPasswordDialog() async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Set Password")), content: ConstrainedBox( constraints: BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text( "${translate('Password')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( border: OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, ), ), ], ), SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( constraints: BoxConstraints(minWidth: 100), child: Text("${translate('Confirmation')}:") .marginOnly(bottom: 16.0)), SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( border: OutlineInputBorder(), errorText: errMsg1.isNotEmpty ? errMsg1 : null), controller: p1, ), ), ], ), ], ), ), actions: [ TextButton( onPressed: () { close(); }, child: Text(translate("Cancel"))), TextButton( onPressed: () { setState(() { errMsg0 = ""; errMsg1 = ""; }); final pass = p0.text.trim(); if (pass.length < 6) { setState(() { errMsg0 = translate("Too short, at least 6 characters."); }); return; } if (p1.text.trim() != pass) { setState(() { errMsg1 = translate("The confirmation is not identical."); }); return; } bind.mainSetPermanentPassword(password: pass); close(); }, child: Text(translate("OK"))), ], ); }); }