diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart new file mode 100644 index 000000000..584d74869 --- /dev/null +++ b/flutter/lib/cm_main.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'desktop/pages/server_page.dart'; + +/// -t lib/cm_main.dart to test cm +void main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + await initEnv(kAppTypeConnectionManager); + runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); + await windowManager.setSize(Size(400, 600)); + await windowManager.setAlignment(Alignment.topRight); +} diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7d3406aa1..20167aeb0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -8,7 +11,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -27,6 +29,15 @@ int androidVersion = 0; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); +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=='))); + class MyTheme { MyTheme._(); @@ -39,6 +50,7 @@ class MyTheme { static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); static const Color darkGray = Color(0xFFB9BABC); + static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; static ThemeData lightTheme = ThemeData( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e6c7d76bf..e399effc2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; // import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; @@ -111,13 +112,12 @@ class _DesktopServerPageState extends State { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer( - builder: (context, serverModel, child) => SingleChildScrollView( - controller: gFFI.serverModel.controller, + builder: (context, serverModel, child) => Material( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - ConnectionManager(), + Expanded(child: ConnectionManager()), SizedBox.fromSize(size: Size(0, 15.0)), ], ), @@ -130,81 +130,277 @@ class ConnectionManager extends StatelessWidget { @override Widget build(BuildContext context) { final serverModel = Provider.of(context); - return Column( - children: serverModel.clients.entries - .map((entry) => PaddingCard( - title: translate(entry.value.isFileTransfer - ? "File Connection" - : "Screen Connection"), - titleIcon: entry.value.isFileTransfer - ? Icons.folder_outlined - : Icons.mobile_screen_share, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: clientInfo(entry.value)), - Expanded( - flex: -1, - child: entry.value.isFileTransfer || - !entry.value.authorized - ? SizedBox.shrink() - : IconButton( - onPressed: () { - gFFI.chatModel - .changeCurrentID(entry.value.id); - final bar = - navigationBarKey.currentWidget; - if (bar != null) { - bar as BottomNavigationBar; - bar.onTap!(1); - } - }, - icon: Icon( - Icons.chat, - color: MyTheme.accent80, - ))) - ], - ), - entry.value.authorized - ? SizedBox.shrink() - : Text( - translate("android_new_connection_tip"), - style: TextStyle(color: Colors.black54), - ), - entry.value.authorized - ? ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red)), - icon: Icon(Icons.close), - onPressed: () { - bind.cmCloseConnection(connId: entry.key); - gFFI.invokeMethod( - "cancel_notification", entry.key); - }, - label: Text(translate("Close"))) - : Row(children: [ - TextButton( - child: Text(translate("Dismiss")), - onPressed: () { - serverModel.sendLoginResponse( - entry.value, false); - }), - SizedBox(width: 20), - ElevatedButton( - child: Text(translate("Accept")), - onPressed: () { - serverModel.sendLoginResponse( - entry.value, true); - }), - ]), - ], - ))) - .toList()); + // test case: + // serverModel.clients.clear(); + // serverModel.clients[0] = Client(false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); + return DefaultTabController( + length: serverModel.clients.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: kTextTabBarHeight, + child: TabBar( + isScrollable: true, + tabs: serverModel.clients.entries + .map((entry) => buildTab(entry)) + .toList(growable: false)), + ), + Expanded( + child: TabBarView( + children: serverModel.clients.entries + .map((entry) => buildConnectionCard(entry)) + .toList(growable: false)), + ) + ], + ), + ); } + + Widget buildConnectionCard(MapEntry entry) { + final client = entry.value; + return Column( + children: [ + _CmHeader(client: client), + _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + )) + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); + } + + Widget buildTab(MapEntry entry) { + return Tab( + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + "${entry.value.name}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + )), + ], + ), + ); + } +} + +class _CmHeader extends StatelessWidget { + final Client client; + + const _CmHeader({Key? key, required this.client}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // icon + Container( + width: 100, + height: 100, + alignment: Alignment.center, + decoration: BoxDecoration(color: str2color(client.name)), + child: Text( + "${client.name[0]}", + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.white, fontSize: 75), + ), + ).marginOnly(left: 4.0, right: 8.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${client.name}", + style: TextStyle( + color: MyTheme.cmIdColor, + fontWeight: FontWeight.bold, + fontSize: 20, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + Text("(${client.peerId})", + style: TextStyle(color: MyTheme.cmIdColor, fontSize: 14)), + SizedBox( + height: 16.0, + ), + Offstage( + offstage: !client.authorized, + child: Row( + children: [ + Text("${translate("Connected")}"), + ], + )) + ], + ), + ), + Offstage( + offstage: client.isFileTransfer, + child: IconButton( + onPressed: handleSendMsg, + icon: Icon(Icons.message_outlined), + ), + ) + ], + ); + } + + void handleSendMsg() {} +} + +class _PrivilegeBoard extends StatelessWidget { + final Client client; + + const _PrivilegeBoard({Key? key, required this.client}) : super(key: key); + + Widget buildPermissionIcon(bool enabled, ImageProvider icon, + Function(bool)? onTap, String? tooltip) { + return Tooltip( + message: tooltip ?? "", + child: Ink( + decoration: + BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), + padding: EdgeInsets.all(4.0), + child: InkWell( + onTap: () => onTap?.call(!enabled), + child: Image( + image: icon, + width: 50, + height: 50, + fit: BoxFit.scaleDown, + ), + ), + ).marginSymmetric(horizontal: 4.0), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Permissions"), + style: TextStyle(fontSize: 16), + ).marginOnly(left: 4.0), + SizedBox( + height: 8.0, + ), + Row( + children: [ + buildPermissionIcon( + client.keyboard, iconKeyboard, (enable) => null, null), + buildPermissionIcon( + client.clipboard, iconClipboard, (enable) => null, null), + buildPermissionIcon( + client.audio, iconAudio, (enable) => null, null), + // TODO: file transfer + buildPermissionIcon(false, iconFile, (enable) => null, null), + ], + ), + ], + ), + ); + } +} + +class _CmControlPanel extends StatelessWidget { + final Client client; + + const _CmControlPanel({Key? key, required this.client}) : super(key: key); + + @override + Widget build(BuildContext context) { + return client.authorized ? buildAuthorized() : buildUnAuthorized(); + } + + buildAuthorized() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Ink( + width: 200, + height: 40, + decoration: BoxDecoration( + color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: handleDisconnect, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Disconnect"), + style: TextStyle(color: Colors.white), + ), + ], + )), + ) + ], + ); + } + + buildUnAuthorized() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Ink( + width: 100, + height: 40, + decoration: BoxDecoration( + color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: handleAccept, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Accept"), + style: TextStyle(color: Colors.white), + ), + ], + )), + ), + SizedBox( + width: 30, + ), + Ink( + width: 100, + height: 40, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey)), + child: InkWell( + onTap: handleCancel, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Cancel"), + style: TextStyle(), + ), + ], + )), + ) + ], + ); + } + + void handleDisconnect() {} + + void handleCancel() {} + + void handleAccept() {} } class PaddingCard extends StatelessWidget { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index d8586baad..7f3acc79f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -49,6 +49,7 @@ Future main(List args) async { break; } } else if (args.isNotEmpty && args.first == '--cm') { + print("--cm started"); await windowManager.ensureInitialized(); runConnectionManagerScreen(); } else { @@ -117,7 +118,6 @@ void runFileTransferScreen(Map argument) async { void runConnectionManagerScreen() async { await initEnv(kAppTypeConnectionManager); - await windowManager.setAlwaysOnTop(true); await windowManager.setSize(Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index e03d0f9d6..e5465e1e3 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -97,8 +97,9 @@ class ServerModel with ChangeNotifier { } final res = await bind.mainCheckClientsLength(length: _clients.length); if (res != null) { - debugPrint("clients not match!"); - updateClientState(res); + // for test + // debugPrint("clients not match!"); + // updateClientState(res); } updatePasswordModel(); @@ -342,6 +343,7 @@ class ServerModel with ChangeNotifier { var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); + _clients.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); _clients[client.id] = client;