diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 01610527b..544dabaf7 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -68,19 +68,6 @@ typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); typedef StreamEventHandler = Future Function(Map); - -final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( - "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); -final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); -final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); -final iconFile = MemoryImage(Uint8List.fromList(base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); -final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); -final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'))); final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg=='))); @@ -196,6 +183,8 @@ class MyTheme { ), ); + static const SwitchThemeData switchTheme = SwitchThemeData(splashRadius: 0); + // Checkbox static const CheckboxThemeData checkboxTheme = CheckboxThemeData( splashRadius: 0, @@ -320,6 +309,7 @@ class MyTheme { ), ), ), + switchTheme: switchTheme, checkboxTheme: checkboxTheme, listTileTheme: listTileTheme, menuBarTheme: MenuBarThemeData( @@ -359,12 +349,16 @@ class MyTheme { ) : null, textTheme: const TextTheme( - titleLarge: TextStyle(fontSize: 19), - titleSmall: TextStyle(fontSize: 14), - bodySmall: TextStyle(fontSize: 12, height: 1.25), - bodyMedium: TextStyle(fontSize: 14, height: 1.25), - labelLarge: TextStyle( - fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), + titleLarge: TextStyle(fontSize: 19), + titleSmall: TextStyle(fontSize: 14), + bodySmall: TextStyle(fontSize: 12, height: 1.25), + bodyMedium: TextStyle(fontSize: 14, height: 1.25), + labelLarge: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: accent80, + ), + ), cardColor: Color(0xFF24252B), visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( @@ -410,6 +404,7 @@ class MyTheme { ), ), ), + switchTheme: switchTheme, checkboxTheme: checkboxTheme, listTileTheme: listTileTheme, menuBarTheme: MenuBarThemeData( diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 9460f4f41..751178ea6 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -1,7 +1,9 @@ import 'package:dash_chat_2/dash_chat_2.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../mobile/pages/home_page.dart'; @@ -43,12 +45,20 @@ class ChatPage extends StatelessWidget implements PageShape { @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( - value: chatModel, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Consumer(builder: (context, chatModel, child) { - final currentUser = chatModel.currentUser; - return Stack( + value: chatModel, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + padding: EdgeInsets.only(top: 14.0, bottom: 14.0, left: 14.0), + child: Consumer( + builder: (context, chatModel, child) { + final currentUser = chatModel.currentUser; + return Container( + padding: EdgeInsets.symmetric(vertical: 5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: Theme.of(context).colorScheme.background, + ), + child: Stack( children: [ LayoutBuilder(builder: (context, constraints) { final chat = DashChat( @@ -61,42 +71,43 @@ class ChatPage extends StatelessWidget implements PageShape { .messages[chatModel.currentID]?.chatMessages ?? [], inputOptions: InputOptions( - sendOnEnter: true, - focusNode: chatModel.inputNode, - inputTextStyle: TextStyle( - fontSize: 14, - color: Theme.of(context) - .textTheme - .titleLarge - ?.color), - inputDecoration: isDesktop - ? InputDecoration( - isDense: true, - hintText: - "${translate('Write a message')}", - filled: true, - fillColor: - Theme.of(context).colorScheme.background, - contentPadding: EdgeInsets.all(10), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: const BorderSide( - width: 0, - style: BorderStyle.none, - ), + sendOnEnter: true, + focusNode: chatModel.inputNode, + inputTextStyle: TextStyle( + fontSize: 14, + color: + Theme.of(context).textTheme.titleLarge?.color), + inputDecoration: isDesktop + ? InputDecoration( + isDense: true, + hintText: translate('Write a message'), + filled: true, + fillColor: + Theme.of(context).colorScheme.background, + contentPadding: EdgeInsets.all(10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: const BorderSide( + width: 1, + style: BorderStyle.solid, ), - ) - : defaultInputDecoration( - hintText: - "${translate('Write a message')}", - fillColor: - Theme.of(context).colorScheme.background), - sendButtonBuilder: defaultSendButton( - padding: EdgeInsets.symmetric( - horizontal: 6, vertical: 0), - color: Theme.of(context).colorScheme.primary)), + ), + ) + : defaultInputDecoration( + hintText: translate('Write a message'), + fillColor: + Theme.of(context).colorScheme.background, + ), + sendButtonBuilder: defaultSendButton( + padding: + EdgeInsets.symmetric(horizontal: 6, vertical: 0), + color: MyTheme.accent, + icon: FluentIcons.send_24_filled, + ), + ), messageOptions: MessageOptions( showOtherUsersAvatar: false, + showOtherUsersName: false, textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, messageTextBuilder: (message, _, __) { @@ -104,32 +115,34 @@ class ChatPage extends StatelessWidget implements PageShape { message.user.id == currentUser.id; return Column( crossAxisAlignment: isOwnMessage - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, children: [ Text(message.text, style: TextStyle(color: Colors.white)), - Padding( - padding: const EdgeInsets.only(top: 5), - child: Text( - "${message.createdAt.hour}:${message.createdAt.minute}", - style: TextStyle( - color: Colors.white, - fontSize: 10, - ), + Text( + "${message.createdAt.hour}:${message.createdAt.minute.toString().padLeft(2, '0')}", + style: TextStyle( + color: Colors.white, + fontSize: 8, ), - ), + ).marginOnly(top: 3), ], ); }, - messageDecorationBuilder: (_, __, ___) => - defaultMessageDecoration( - color: MyTheme.accent80, - borderTopLeft: 8, - borderTopRight: 8, - borderBottomRight: 8, - borderBottomLeft: 8, - )), + messageDecorationBuilder: (message, __, ___) { + final isOwnMessage = + message.user.id == currentUser.id; + return defaultMessageDecoration( + color: isOwnMessage + ? Colors.blueGrey + : MyTheme.accent, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: isOwnMessage ? 8 : 2, + borderBottomLeft: isOwnMessage ? 2 : 8, + ); + }), ); return SelectionArea(child: chat); }), @@ -145,12 +158,17 @@ class ChatPage extends StatelessWidget implements PageShape { SizedBox(width: 5), Text( "${currentUser.firstName} ${currentUser.id}", - style: TextStyle(color: MyTheme.accent50), + style: TextStyle(color: MyTheme.accent), ), ], - )), + ), + ), ], - ); - }))); + ), + ); + }, + ), + ), + ); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 827fd3b3f..3e664c484 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -76,7 +76,8 @@ extension StringExtension on String { String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp)); } -const Size kConnectionManagerWindowSize = Size(300, 400); +const Size kConnectionManagerWindowSizeClosedChat = Size(300, 490); +const Size kConnectionManagerWindowSizeOpenChat = Size(700, 490); // Tabbar transition duration, now we remove the duration const Duration kTabTransitionDuration = Duration.zero; const double kEmptyMarginTop = 50; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 45591b79b..e5860a4f3 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -12,6 +12,7 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import '../../common.dart'; import '../../common/widgets/chat_page.dart'; @@ -70,25 +71,21 @@ class _DesktopServerPageState extends State Widget build(BuildContext context) { super.build(context); return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: gFFI.serverModel), - ChangeNotifierProvider.value(value: gFFI.chatModel), - ], - child: Consumer( - builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - ], - ), - ), - )))); + providers: [ + ChangeNotifierProvider.value(value: gFFI.serverModel), + ChangeNotifierProvider.value(value: gFFI.chatModel), + ], + child: Consumer( + builder: (context, serverModel, child) => Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: ConnectionManager(), + ), + ), + ), + ); } @override @@ -136,41 +133,56 @@ class ConnectionManagerState extends State { onPointerDown: pointerHandler, onPointerMove: pointerHandler, child: DesktopTab( - showTitle: false, - showMaximize: false, - showMinimize: true, - showClose: true, - onWindowCloseButton: handleWindowCloseButton, - controller: serverModel.tabController, - maxLabelWidth: 100, - tail: buildScrollJumper(), - selectedTabBackgroundColor: - Theme.of(context).hintColor.withOpacity(0.2), - tabBuilder: (key, icon, label, themeConf) { - final client = serverModel.clients.firstWhereOrNull( - (client) => client.id.toString() == key); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Tooltip( - message: key, - waitDuration: Duration(seconds: 1), - child: label), - Obx(() => Offstage( - offstage: - !(client?.hasUnreadChatMessage.value ?? false), - child: - Icon(Icons.circle, color: Colors.red, size: 10))) - ], - ); - }, - pageViewBuilder: (pageView) => Row(children: [ - Expanded(child: pageView), - Consumer( - builder: (_, model, child) => model.isShowCMChatPage - ? Expanded(child: Scaffold(body: ChatPage())) - : Offstage()) - ]))); + showTitle: false, + showMaximize: false, + showMinimize: true, + showClose: true, + onWindowCloseButton: handleWindowCloseButton, + controller: serverModel.tabController, + maxLabelWidth: 100, + tail: buildScrollJumper(), + selectedTabBackgroundColor: + Theme.of(context).hintColor.withOpacity(0.2), + tabBuilder: (key, icon, label, themeConf) { + final client = serverModel.clients + .firstWhereOrNull((client) => client.id.toString() == key); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Tooltip( + message: key, + waitDuration: Duration(seconds: 1), + child: label), + Obx(() => Offstage( + offstage: + !(client?.hasUnreadChatMessage.value ?? false), + child: Icon(Icons.circle, color: Colors.red, size: 10))) + ], + ); + }, + pageViewBuilder: (pageView) => Row( + children: [ + Consumer( + builder: (_, model, child) => model.isShowCMChatPage + ? Expanded( + child: ChatPage(), + flex: (kConnectionManagerWindowSizeOpenChat.width - + kConnectionManagerWindowSizeClosedChat + .width) + .toInt(), + ) + : Offstage(), + ), + Expanded( + child: pageView, + flex: kConnectionManagerWindowSizeClosedChat.width + .toInt() - + 4 // prevent stretch of the page view when chat is open, + ), + ], + ), + ), + ); } Widget buildTitleBar() { @@ -238,22 +250,24 @@ class ConnectionManagerState extends State { Widget buildConnectionCard(Client client) { return Consumer( - builder: (context, value, child) => Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - key: ValueKey(client.id), - children: [ - _CmHeader(client: client), - client.type_() != ClientType.remote || client.disconnected - ? Offstage() - : _PrivilegeBoard(client: client), - Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: _CmControlPanel(client: client), - )) - ], - ).paddingSymmetric(vertical: 8.0, horizontal: 8.0)); + builder: (context, value, child) => Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey(client.id), + children: [ + _CmHeader(client: client), + client.type_() != ClientType.remote || client.disconnected + ? Offstage() + : _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + ), + ) + ], + ).paddingSymmetric(vertical: 4.0, horizontal: 8.0), + ); } class _AppIcon extends StatelessWidget { @@ -326,70 +340,108 @@ class _CmHeaderState extends State<_CmHeader> @override Widget build(BuildContext context) { super.build(context); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // icon - Container( - width: 90, - height: 90, - alignment: Alignment.center, - decoration: BoxDecoration(color: str2color(client.name)), - child: Text( - client.name[0], - style: TextStyle( - fontWeight: FontWeight.bold, color: Colors.white, fontSize: 65), - ), - ).marginOnly(left: 4.0, right: 8.0), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - child: Text( - client.name, - style: TextStyle( - color: MyTheme.cmIdColor, - fontWeight: FontWeight.bold, - fontSize: 20, - overflow: TextOverflow.ellipsis, - ), - maxLines: 1, - )), - FittedBox( - child: Text("(${client.peerId})", - style: - TextStyle(color: MyTheme.cmIdColor, fontSize: 14))), - SizedBox( - height: 16.0, + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Color(0xff00bfe1), + Color(0xff0071ff), + ], + ), + ), + margin: EdgeInsets.symmetric(horizontal: 5.0, vertical: 10.0), + padding: EdgeInsets.only( + top: 10.0, + bottom: 10.0, + left: 10.0, + right: 5.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 70, + height: 70, + alignment: Alignment.center, + decoration: BoxDecoration( + color: str2color(client.name), + borderRadius: BorderRadius.circular(15.0), + ), + child: Text( + client.name[0], + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 55, ), - FittedBox( - child: Row( - children: [ - Text(client.authorized + ), + ).marginOnly(right: 10.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + child: Text( + client.name, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + )), + FittedBox( + child: Text( + "(${client.peerId})", + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ).marginOnly(bottom: 10.0), + FittedBox( + child: Row( + children: [ + Text( + client.authorized ? client.disconnected ? translate("Disconnected") : translate("Connected") - : "${translate("Request access to your device")}...") - .marginOnly(right: 8.0), - if (client.authorized) - Obx(() => Text( - formatDurationToTime(Duration(seconds: _time.value)))) - ], - )) - ], + : "${translate("Request access to your device")}...", + style: TextStyle(color: Colors.white), + ).marginOnly(right: 8.0), + if (client.authorized) + Obx( + () => Text( + formatDurationToTime( + Duration(seconds: _time.value), + ), + style: TextStyle(color: Colors.white), + ), + ) + ], + )) + ], + ), ), - ), - Offstage( - offstage: !client.authorized || client.type_() != ClientType.remote, - child: IconButton( + Offstage( + offstage: !client.authorized || client.type_() != ClientType.remote, + child: IconButton( onPressed: () => checkClickTime( - client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), - icon: Icon(Icons.message_outlined), - splashRadius: kDesktopIconButtonSplashRadius), - ) - ], + client.id, + () => gFFI.chatModel.toggleCMChatPage(client.id), + ), + icon: Icon( + FluentIcons.chat_32_filled, + color: Colors.white, + ), + splashRadius: kDesktopIconButtonSplashRadius, + ), + ) + ], + ), ); } @@ -408,96 +460,159 @@ class _PrivilegeBoard extends StatefulWidget { class _PrivilegeBoardState extends State<_PrivilegeBoard> { late final client = widget.client; - Widget buildPermissionIcon( - bool enabled, ImageProvider icon, Function(bool)? onTap, String tooltip) { + Widget buildPermissionIcon(bool enabled, IconData iconData, + Function(bool)? onTap, String tooltipText) { return Tooltip( - message: tooltip, - child: Ink( - decoration: - BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), - padding: EdgeInsets.all(4.0), + message: tooltipText, + child: Container( + decoration: BoxDecoration( + color: enabled ? MyTheme.accent : Colors.grey[700], + borderRadius: BorderRadius.circular(10.0), + ), + padding: EdgeInsets.all(8.0), child: InkWell( onTap: () => checkClickTime(widget.client.id, () => onTap?.call(!enabled)), - child: Image( - image: icon, - width: 50, - height: 50, - fit: BoxFit.scaleDown, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Icon( + iconData, + size: 30.0, + color: Colors.white, + ), + Text( + enabled ? "ON" : "OFF", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w200, + color: Colors.white, + fontSize: 10.0, + ), + ) + ], ), ), - ).marginSymmetric(horizontal: 4.0), + ), ); } @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.only(top: 16.0, bottom: 8.0), + width: double.infinity, + height: 200.0, + margin: EdgeInsets.all(5.0), + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: Theme.of(context).colorScheme.background, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 1, + offset: Offset(0, 1.5), + ), + ], + ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( translate("Permissions"), - style: TextStyle(fontSize: 16), - ).marginOnly(left: 4.0), - SizedBox( - height: 8.0, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ).marginOnly(left: 4.0, bottom: 8.0), + Expanded( + child: GridView.count( + crossAxisCount: 3, + padding: EdgeInsets.symmetric(horizontal: 20.0), + mainAxisSpacing: 20.0, + crossAxisSpacing: 20.0, + children: [ + buildPermissionIcon( + client.keyboard, + FluentIcons.keyboard_24_filled, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "keyboard", enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, + translate('Allow using keyboard and mouse'), + ), + buildPermissionIcon( + client.clipboard, + FluentIcons.clipboard_24_filled, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "clipboard", enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, + translate('Allow using clipboard'), + ), + buildPermissionIcon( + client.audio, + FluentIcons.speaker_1_24_filled, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "audio", enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Allow hearing sound'), + ), + buildPermissionIcon( + client.file, + FluentIcons.arrow_sort_24_filled, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "file", enabled: enabled); + setState(() { + client.file = enabled; + }); + }, + translate('Allow file copy and paste'), + ), + buildPermissionIcon( + client.restart, + FluentIcons.arrow_sync_circle_20_filled, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "restart", enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, + translate('Allow remote restart'), + ), + buildPermissionIcon( + client.recording, + FluentIcons.record_stop_24_filled, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "recording", enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Allow recording session'), + ) + ], + ), ), - FittedBox( - child: Row( - children: [ - buildPermissionIcon(client.keyboard, iconKeyboard, (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "keyboard", enabled: enabled); - setState(() { - client.keyboard = enabled; - }); - }, translate('Allow using keyboard and mouse')), - buildPermissionIcon(client.clipboard, iconClipboard, (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "clipboard", enabled: enabled); - setState(() { - client.clipboard = enabled; - }); - }, translate('Allow using clipboard')), - buildPermissionIcon(client.audio, iconAudio, (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "audio", enabled: enabled); - setState(() { - client.audio = enabled; - }); - }, translate('Allow hearing sound')), - buildPermissionIcon(client.file, iconFile, (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "file", enabled: enabled); - setState(() { - client.file = enabled; - }); - }, translate('Allow file copy and paste')), - buildPermissionIcon(client.restart, iconRestart, (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "restart", enabled: enabled); - setState(() { - client.restart = enabled; - }); - }, translate('Allow remote restart')), - buildPermissionIcon(client.recording, iconRecording, (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "recording", enabled: enabled); - setState(() { - client.recording = enabled; - }); - }, translate('Allow recording session')) - ], - )), ], ), ); } } -const double bigMargin = 15; +const double buttonBottomMargin = 8; class _CmControlPanel extends StatelessWidget { final Client client; @@ -524,12 +639,18 @@ class _CmControlPanel extends StatelessWidget { children: [ Offstage( offstage: !client.inVoiceCall, - child: buildButton(context, - color: Colors.red, - onClick: () => closeVoiceCall(), - icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), - text: "Stop voice call", - textColor: Colors.white), + child: buildButton( + context, + color: Colors.red, + onClick: () => closeVoiceCall(), + icon: Icon( + FluentIcons.call_end_20_filled, + color: Colors.white, + size: 14, + ), + text: "Stop voice call", + textColor: Colors.white, + ), ), Offstage( offstage: !client.incomingVoiceCall, @@ -539,18 +660,27 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: MyTheme.accent, onClick: () => handleVoiceCall(true), - icon: Icon(Icons.phone_enabled, color: Colors.white), + icon: Icon( + FluentIcons.call_20_filled, + color: Colors.white, + size: 14, + ), text: "Accept", textColor: Colors.white), ), Expanded( - child: buildButton(context, - color: Colors.red, - onClick: () => handleVoiceCall(false), - icon: - Icon(Icons.phone_disabled_rounded, color: Colors.white), - text: "Dismiss", - textColor: Colors.white), + child: buildButton( + context, + color: Colors.red, + onClick: () => handleVoiceCall(false), + icon: Icon( + FluentIcons.call_dismiss_20_filled, + color: Colors.white, + size: 14, + ), + text: "Dismiss", + textColor: Colors.white, + ), ) ], ), @@ -566,31 +696,40 @@ class _CmControlPanel extends StatelessWidget { ), Offstage( offstage: !showElevation, - child: buildButton(context, color: Colors.green[700], onClick: () { - handleElevate(context); - windowManager.minimize(); - }, - icon: Icon( - Icons.security_sharp, - color: Colors.white, - ), - text: 'Elevate', - textColor: Colors.white), + child: buildButton( + context, + color: MyTheme.accent, + onClick: () { + handleElevate(context); + windowManager.minimize(); + }, + icon: Icon( + FluentIcons.shield_checkmark_20_filled, + color: Colors.white, + size: 14, + ), + text: 'Elevate', + textColor: Colors.white, + ), ), Row( children: [ Expanded( - child: buildButton(context, - color: Colors.redAccent, - onClick: handleDisconnect, - text: 'Disconnect', - textColor: Colors.white)), + child: buildButton(context, + color: Colors.redAccent, + onClick: handleDisconnect, + text: 'Disconnect', + icon: Icon( + FluentIcons.plug_disconnected_20_filled, + color: Colors.white, + size: 14, + ), + textColor: Colors.white), + ), ], ) ], - ) - .marginOnly(bottom: showElevation ? 0 : bigMargin) - .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); + ).marginOnly(bottom: buttonBottomMargin); } buildDisconnected(BuildContext context) { @@ -604,7 +743,7 @@ class _CmControlPanel extends StatelessWidget { text: 'Close', textColor: Colors.white)), ], - ).marginOnly(bottom: 15).marginSymmetric(horizontal: bigMargin); + ).marginOnly(bottom: buttonBottomMargin); } buildUnAuthorized(BuildContext context) { @@ -626,8 +765,9 @@ class _CmControlPanel extends StatelessWidget { }, text: 'Accept', icon: Icon( - Icons.security_sharp, + FluentIcons.shield_checkmark_20_filled, color: Colors.white, + size: 14, ), textColor: Colors.white), ), @@ -638,26 +778,33 @@ class _CmControlPanel extends StatelessWidget { Expanded( child: Column( children: [ - buildButton(context, color: MyTheme.accent, onClick: () { - handleAccept(context); - windowManager.minimize(); - }, text: 'Accept', textColor: Colors.white), + buildButton( + context, + color: MyTheme.accent, + onClick: () { + handleAccept(context); + windowManager.minimize(); + }, + text: 'Accept', + textColor: Colors.white, + ), ], ), ), Expanded( - child: buildButton(context, - color: Colors.transparent, - border: Border.all(color: Colors.grey), - onClick: handleDisconnect, - text: 'Cancel', - textColor: null)), + child: buildButton( + context, + color: Colors.transparent, + border: Border.all(color: Colors.grey), + onClick: handleDisconnect, + text: 'Cancel', + textColor: null, + ), + ), ], ), ], - ) - .marginOnly(bottom: showElevation ? 0 : bigMargin) - .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); + ).marginOnly(bottom: buttonBottomMargin); } Widget buildButton( @@ -686,18 +833,21 @@ class _CmControlPanel extends StatelessWidget { ); } return Container( - height: 30, + height: 28, decoration: BoxDecoration( - color: color, borderRadius: BorderRadius.circular(4), border: border), + color: color, + borderRadius: BorderRadius.circular(10.0), + border: border), child: InkWell( - onTap: () => checkClickTime(client.id, onClick), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Offstage(offstage: icon == null, child: icon), - textWidget, - ], - )), + onTap: () => checkClickTime(client.id, onClick), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Offstage(offstage: icon == null, child: icon).marginOnly(right: 5), + textWidget, + ], + ), + ), ).marginAll(4); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 318f9c2cc..3dca92a2e 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -233,21 +233,21 @@ void runConnectionManagerScreen(bool hide) async { } void showCmWindow() { - WindowOptions windowOptions = - getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); + WindowOptions windowOptions = getHiddenTitleBarWindowOptions( + size: kConnectionManagerWindowSizeClosedChat); windowManager.waitUntilReadyToShow(windowOptions, () async { bind.mainHideDocker(); await windowManager.show(); await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]); // ensure initial window size to be changed await windowManager.setSizeAlignment( - kConnectionManagerWindowSize, Alignment.topRight); + kConnectionManagerWindowSizeClosedChat, Alignment.topRight); }); } void hideCmWindow() { - WindowOptions windowOptions = - getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); + WindowOptions windowOptions = getHiddenTitleBarWindowOptions( + size: kConnectionManagerWindowSizeClosedChat); windowManager.setOpacity(0); windowManager.waitUntilReadyToShow(windowOptions, () async { bind.mainHideDocker(); diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 9db5a1571..6b3379310 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; @@ -104,21 +105,23 @@ class ChatModel with ChangeNotifier { final overlay = OverlayEntry(builder: (context) { return DraggableFloatWidget( - config: DraggableFloatWidgetBaseConfig( - initPositionYInTop: false, - initPositionYMarginBorder: 100, - borderTopContainTopBar: true, - ), - child: FloatingActionButton( - onPressed: () { - if (chatWindowOverlayEntry == null) { - showChatWindowOverlay(); - } else { - hideChatWindowOverlay(); - } - }, - backgroundColor: Theme.of(context).colorScheme.primary, - child: Icon(Icons.message))); + config: DraggableFloatWidgetBaseConfig( + initPositionYInTop: false, + initPositionYMarginBorder: 100, + borderTopContainTopBar: true, + ), + child: FloatingActionButton( + onPressed: () { + if (chatWindowOverlayEntry == null) { + showChatWindowOverlay(); + } else { + hideChatWindowOverlay(); + } + }, + backgroundColor: Theme.of(context).colorScheme.primary, + child: Icon(FluentIcons.chat_24_filled), + ), + ); }); overlayState.insert(overlay); chatIconOverlayEntry = overlay; @@ -203,11 +206,12 @@ class ChatModel with ChangeNotifier { notifyListeners(); await windowManager.show(); await windowManager.setSizeAlignment( - kConnectionManagerWindowSize, Alignment.topRight); + kConnectionManagerWindowSizeClosedChat, Alignment.topRight); } else { requestChatInputFocus(); await windowManager.show(); - await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); + await windowManager.setSizeAlignment( + kConnectionManagerWindowSizeOpenChat, Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; notifyListeners(); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 273c4fe58..e1db03f27 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -288,8 +288,8 @@ class ServerModel with ChangeNotifier { /// Toggle the screen sharing service. toggleService() async { if (_isStart) { - final res = - await parent.target?.dialogManager.show((setState, close, context) { + final res = await parent.target?.dialogManager + .show((setState, close, context) { submit() => close(true); return CustomAlertDialog( title: Row(children: [ @@ -311,8 +311,8 @@ class ServerModel with ChangeNotifier { stopService(); } } else { - final res = - await parent.target?.dialogManager.show((setState, close, context) { + final res = await parent.target?.dialogManager + .show((setState, close, context) { submit() => close(true); return CustomAlertDialog( title: Row(children: [ diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 30cfcd377..06920dc5c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -450,6 +450,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + fluentui_system_icons: + dependency: "direct main" + description: + name: fluentui_system_icons + sha256: "745d58831bba404532bebce20286fb1920c650a4cc74678990251a16fb4d2600" + url: "https://pub.dev" + source: hosted + version: "1.1.201" flutter: dependency: "direct main" description: flutter diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c3781a817..296aa0a89 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: package_info_plus: ^1.4.2 url_launcher: ^6.0.9 toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.14 + dash_chat_2: ^0.0.15 draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 @@ -99,6 +99,7 @@ dependencies: texture_rgba_renderer: ^0.0.16 percent_indicator: ^4.2.2 dropdown_button2: ^2.0.0 + fluentui_system_icons: ^1.1.201 dev_dependencies: icons_launcher: ^2.0.4 diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 2c037c7b0..fca9d8cee 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -45,12 +45,12 @@ void main(List args) async { ], supportedLocales: supportedLocales, home: const DesktopServerPage())); - WindowOptions windowOptions = - getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); + WindowOptions windowOptions = getHiddenTitleBarWindowOptions( + size: kConnectionManagerWindowSizeClosedChat); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); // ensure initial window size to be changed - await windowManager.setSize(kConnectionManagerWindowSize); + await windowManager.setSize(kConnectionManagerWindowSizeClosedChat); await Future.wait([ windowManager.setAlignment(Alignment.topRight), windowManager.focus(),