diff --git a/lib/main.dart b/lib/main.dart index 32b8cdf8a..c853505a6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,23 +21,13 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { final analytics = FirebaseAnalytics(); - final providers = [ - ChangeNotifierProvider.value(value: FFI.ffiModel), - ChangeNotifierProvider.value(value: FFI.imageModel), - ChangeNotifierProvider.value(value: FFI.cursorModel), - ChangeNotifierProvider.value(value: FFI.canvasModel), - ]; - if (!isWeb) { - providers.addAll([ - ChangeNotifierProvider.value(value: FFI.chatModel), - ChangeNotifierProvider.value(value: FFI.fileModel), - ]); - if (isAndroid) { - providers.add(ChangeNotifierProvider.value(value: FFI.serverModel)); - } - } return MultiProvider( - providers: providers, + providers: [ + ChangeNotifierProvider.value(value: FFI.ffiModel), + ChangeNotifierProvider.value(value: FFI.imageModel), + ChangeNotifierProvider.value(value: FFI.cursorModel), + ChangeNotifierProvider.value(value: FFI.canvasModel), + ], child: MaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, diff --git a/lib/models/chat_model.dart b/lib/models/chat_model.dart index 1b8be60f1..0c3aaa798 100644 --- a/lib/models/chat_model.dart +++ b/lib/models/chat_model.dart @@ -17,14 +17,24 @@ class ChatModel with ChangeNotifier { final ChatUser me = ChatUser( uid:"", name: "me", - customProperties: Map()..["id"] = clientModeID ); + final _scroller = ScrollController(); + var _currentID = clientModeID; - get messages => _messages; + ScrollController get scroller => _scroller; - get currentID => _currentID; + Map> get messages => _messages; + + int get currentID => _currentID; + + changeCurrentID(int id){ + if(_messages.containsKey(id)){ + _currentID = id; + notifyListeners(); + } + } receive(int id, String text) { if (text.isEmpty) return; @@ -32,18 +42,36 @@ class ChatModel with ChangeNotifier { if (iconOverlayEntry == null) { showChatIconOverlay(); } + late final chatUser; + if(id == clientModeID){ + chatUser = ChatUser( + name: FFI.ffiModel.pi.username, + uid: FFI.getId(), + ); + }else{ + chatUser = FFI.serverModel.clients[id]?.chatUser; + } + if(chatUser == null){ + return debugPrint("Failed to receive msg,user doesn't exist"); + } if(!_messages.containsKey(id)){ _messages[id] = []; } - // TODO peer info - _messages[id]?.add(ChatMessage( + _messages[id]!.add(ChatMessage( text: text, - user: ChatUser( - name: FFI.ffiModel.pi.username, - uid: FFI.getId(), - ))); + user: chatUser)); _currentID = id; notifyListeners(); + scrollToBottom(); + } + + scrollToBottom(){ + Future.delayed(Duration(milliseconds: 500), () { + _scroller.animateTo( + _scroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + }); } send(ChatMessage message) { @@ -59,13 +87,12 @@ class ChatModel with ChangeNotifier { } } notifyListeners(); + scrollToBottom(); } - release() { + close() { hideChatIconOverlay(); hideChatWindowOverlay(); - _messages.forEach((key, value) => value.clear()); - _messages.clear(); notifyListeners(); } } diff --git a/lib/models/model.dart b/lib/models/model.dart index 1df5b820a..1aa779689 100644 --- a/lib/models/model.dart +++ b/lib/models/model.dart @@ -722,7 +722,7 @@ class FFI { } static void close() { - chatModel.release(); + chatModel.close(); if (FFI.imageModel.image != null && !isDesktop) { savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); diff --git a/lib/models/server_model.dart b/lib/models/server_model.dart index b7abb291d..e406b1e4a 100644 --- a/lib/models/server_model.dart +++ b/lib/models/server_model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'package:dash_chat/dash_chat.dart'; import 'package:flutter/material.dart'; import '../common.dart'; import '../pages/server_page.dart'; @@ -17,7 +18,7 @@ class ServerModel with ChangeNotifier { final _serverId = TextEditingController(text: _emptyIdShow); final _serverPasswd = TextEditingController(text: ""); - List _clients = []; + Map _clients = {}; bool get isStart => _isStart; @@ -33,7 +34,7 @@ class ServerModel with ChangeNotifier { TextEditingController get serverPasswd => _serverPasswd; - List get clients => _clients; + Map get clients => _clients; ServerModel() { ()async{ @@ -191,9 +192,11 @@ class ServerModel with ChangeNotifier { var res = FFI.getByName("clients_state"); try { final List clientsJson = jsonDecode(res); - _clients = clientsJson - .map((clientJson) => Client.fromJson(jsonDecode(res))) - .toList(); + for (var clientJson in clientsJson){ + final client = Client.fromJson(jsonDecode(clientJson)); + _clients[client.id] = client; + } + notifyListeners(); } catch (e) {} } @@ -204,7 +207,7 @@ class ServerModel with ChangeNotifier { final Map response = Map(); response["id"] = client.id; DialogManager.show((setState, close) => CustomAlertDialog( - title: Text(client.isFileTransfer?"File":"Screen" + "Control Request"), + title: Text(translate(client.isFileTransfer?"File Connection":"Screen Connection")), content: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, @@ -234,7 +237,7 @@ class ServerModel with ChangeNotifier { "start_capture"); // to Android service debugPrint("_toAndroidStartCapture:$res"); } - _clients.add(client); + _clients[client.id] = client; notifyListeners(); close(); }), @@ -246,7 +249,8 @@ class ServerModel with ChangeNotifier { void onClientAuthorized(Map evt) { try{ - _clients.add(Client.fromJson(jsonDecode(evt['client']))); + final client = Client.fromJson(jsonDecode(evt['client'])); + _clients[client.id] = client; notifyListeners(); }catch(e){ @@ -256,8 +260,7 @@ class ServerModel with ChangeNotifier { void onClientRemove(Map evt) { try { final id = int.parse(evt['id'] as String); - Client client = _clients.singleWhere((c) => c.id == id); - _clients.remove(client); + _clients.remove(id); notifyListeners(); } catch (e) { // singleWhere fail ,reset the login dialog @@ -267,10 +270,10 @@ class ServerModel with ChangeNotifier { } closeAll() { - _clients.forEach((client) { - FFI.setByName("close_conn", client.id.toString()); + _clients.forEach((id,client) { + FFI.setByName("close_conn", id.toString()); }); - _clients = []; + _clients.clear(); } } @@ -283,6 +286,7 @@ class Client { bool keyboard = false; bool clipboard = false; bool audio = false; + late ChatUser chatUser; Client(this.authorized, this.isFileTransfer, this.name, this.peerId,this.keyboard,this.clipboard,this.audio); @@ -295,6 +299,10 @@ class Client { keyboard= json['keyboard']; clipboard= json['clipboard']; audio= json['audio']; + chatUser = ChatUser( + uid:peerId, + name: name, + ); } Map toJson() { diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 62c0717d1..ee72d4908 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -20,23 +20,47 @@ class ChatPage extends StatelessWidget implements PageShape { final icon = Icon(Icons.chat); @override - final appBarActions = []; + final appBarActions = [ + PopupMenuButton( + icon: Icon(Icons.list_alt), + itemBuilder: (context) { + final chatModel = FFI.chatModel; + final serverModel = FFI.serverModel; + return chatModel.messages.entries.map((entry) { + final id = entry.key; + final user = serverModel.clients[id]?.chatUser ?? chatModel.me; + return PopupMenuItem( + child: Text("${user.name} - ${user.uid}"), + value: id, + ); + }).toList(); + }, + onSelected: (id) { + FFI.chatModel.changeCurrentID(id); + }) + ]; @override Widget build(BuildContext context) { - return Container( - color: MyTheme.grayBg, - child: Consumer(builder: (context, chatModel, child) { - return DashChat( - inputContainerStyle: BoxDecoration(color: Colors.white70), - sendOnEnter: false, // if true,reload keyboard everytime,need fix - onSend: (chatMsg) { - chatModel.send(chatMsg); - }, - user: chatModel.me, - messages: chatModel.messages[chatModel.currentID] ?? [], - ); - })); + return ChangeNotifierProvider.value( + value: FFI.chatModel, + child: Container( + color: MyTheme.grayBg, + child: Consumer(builder: (context, chatModel, child) { + return DashChat( + inputContainerStyle: BoxDecoration(color: Colors.white70), + sendOnEnter: false, + // if true,reload keyboard everytime,need fix + onSend: (chatMsg) { + chatModel.send(chatMsg); + }, + user: chatModel.me, + messages: chatModel.messages[chatModel.currentID] ?? [], + // default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53 + scrollToBottom: false, + scrollController: chatModel.scroller, + ); + }))); } } @@ -209,24 +233,30 @@ class _ChatWindowOverlayState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Padding(padding: EdgeInsets.symmetric(horizontal: 15),child: Text( - translate("Chat"), - style: TextStyle( - color: Colors.white, - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 20), - )), + Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: Text( + translate("Chat"), + style: TextStyle( + color: Colors.white, + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 20), + )), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - IconButton(onPressed: () { - hideChatWindowOverlay(); - }, icon: Icon(Icons.keyboard_arrow_down)), - IconButton(onPressed: () { - hideChatWindowOverlay(); - hideChatIconOverlay(); - }, icon: Icon(Icons.close)) + IconButton( + onPressed: () { + hideChatWindowOverlay(); + }, + icon: Icon(Icons.keyboard_arrow_down)), + IconButton( + onPressed: () { + hideChatWindowOverlay(); + hideChatIconOverlay(); + }, + icon: Icon(Icons.close)) ], ) ], diff --git a/lib/pages/file_manager_page.dart b/lib/pages/file_manager_page.dart index 454b95b7c..71bf5088a 100644 --- a/lib/pages/file_manager_page.dart +++ b/lib/pages/file_manager_page.dart @@ -45,8 +45,9 @@ class _FileManagerPageState extends State { } @override - Widget build(BuildContext context) => - Consumer(builder: (_context, _model, _child) { + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: FFI.fileModel, + child: Consumer(builder: (_context, _model, _child) { return WillPopScope( onWillPop: () async { if (model.selectMode) { @@ -76,7 +77,7 @@ class _FileManagerPageState extends State { body: body(), bottomSheet: bottomSheet(), )); - }); + })); bool needShowCheckBox() { if (!model.selectMode) { @@ -260,8 +261,8 @@ class _FileManagerPageState extends State { itemBuilder: (context) { return SortBy.values .map((e) => PopupMenuItem( - child: Text( - translate(e.toString().split(".").last)), + child: + Text(translate(e.toString().split(".").last)), value: e, )) .toList(); @@ -380,7 +381,6 @@ class _FileManagerPageState extends State { children: [ Padding( padding: EdgeInsets.all(2), - // TODO child: Text( "${translate("Total")}: ${model.currentDir.entries.length}${translate("items")}", style: TextStyle(color: MyTheme.darkGray), @@ -394,7 +394,7 @@ class _FileManagerPageState extends State { Widget? bottomSheet() { final state = model.jobState; final isOtherPage = _selectedItems.isOtherPage(model.isLocal); - final selectedItemsLength = "${_selectedItems.length} ${translate("items")}"; // TODO t + final selectedItemsLen = "${_selectedItems.length} ${translate("items")}"; final local = _selectedItems.isLocal == null ? "" : " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; @@ -405,7 +405,7 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: Icon(Icons.check), title: translate("Selected"), - text: selectedItemsLength + local, + text: selectedItemsLen + local, onCanceled: () => model.toggleSelectMode(), actions: [ IconButton( @@ -422,7 +422,7 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: Icon(Icons.input), title: translate("Paste here?"), - text: selectedItemsLength + local, + text: selectedItemsLen + local, onCanceled: () => model.toggleSelectMode(), actions: [ IconButton( @@ -441,7 +441,8 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: CircularProgressIndicator(), title: translate("Waiting"), - text: "${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s", + text: + "${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s", onCanceled: null, ); case JobState.done: diff --git a/lib/pages/server_page.dart b/lib/pages/server_page.dart index 1e98e2db6..af28295ab 100644 --- a/lib/pages/server_page.dart +++ b/lib/pages/server_page.dart @@ -37,20 +37,22 @@ class ServerPage extends StatelessWidget implements PageShape { @override Widget build(BuildContext context) { checkService(); - return Consumer( - builder: (context, serverModel, child) => SingleChildScrollView( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ServerInfo(), - PermissionChecker(), - ConnectionManager(), - SizedBox.fromSize(size: Size(0, 15.0)), // Bottom padding - ], - ), - ), - )); + return ChangeNotifierProvider.value( + value: FFI.serverModel, + child: Consumer( + builder: (context, serverModel, child) => SingleChildScrollView( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ServerInfo(), + PermissionChecker(), + ConnectionManager(), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), + ))); } } @@ -155,7 +157,7 @@ class _PermissionCheckerState extends State { @override Widget build(BuildContext context) { final serverModel = Provider.of(context); - final hasAudioPermission = androidVersion>=30; + final hasAudioPermission = androidVersion >= 30; return PaddingCard( title: translate("Configuration Permissions"), child: Column( @@ -167,8 +169,13 @@ class _PermissionCheckerState extends State { serverModel.toggleInput), PermissionRow(translate("File Transfer"), serverModel.fileOk, serverModel.toggleFile), - hasAudioPermission?PermissionRow(translate("Audio Capture"), serverModel.audioOk, - serverModel.toggleAudio):Text("* ${translate("android_version_audio_tip")}",style: TextStyle(color: MyTheme.darkGray),), + hasAudioPermission + ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, + serverModel.toggleAudio) + : Text( + "* ${translate("android_version_audio_tip")}", + style: TextStyle(color: MyTheme.darkGray), + ), SizedBox(height: 8), serverModel.mediaOk ? ElevatedButton.icon( @@ -216,7 +223,7 @@ class PermissionRow extends StatelessWidget { TextButton( onPressed: onPressed, child: Text( - translate(isOk ?"CLOSE":"OPEN"), + translate(isOk ? "CLOSE" : "OPEN"), style: TextStyle(fontWeight: FontWeight.bold), )), const Divider(height: 0) @@ -230,16 +237,20 @@ class ConnectionManager extends StatelessWidget { Widget build(BuildContext context) { final serverModel = Provider.of(context); return Column( - children: serverModel.clients - .map((client) => PaddingCard( - title: translate(client.isFileTransfer?"File Connection":"Screen Connection"), - titleIcon: client.isFileTransfer?Icons.folder_outlined:Icons.mobile_screen_share, + 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: [ Padding( padding: EdgeInsets.symmetric(vertical: 5.0), - child: clientInfo(client), + child: clientInfo(entry.value), ), ElevatedButton.icon( style: ButtonStyle( @@ -247,7 +258,7 @@ class ConnectionManager extends StatelessWidget { MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - FFI.setByName("close_conn", client.id.toString()); + FFI.setByName("close_conn", entry.key.toString()); }, label: Text(translate("Close"))) ], @@ -257,7 +268,7 @@ class ConnectionManager extends StatelessWidget { } class PaddingCard extends StatelessWidget { - PaddingCard({required this.child, this.title,this.titleIcon}); + PaddingCard({required this.child, this.title, this.titleIcon}); final String? title; final IconData? titleIcon; @@ -273,7 +284,12 @@ class PaddingCard extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ - titleIcon !=null?Padding(padding: EdgeInsets.only(right: 10),child:Icon(titleIcon,color: MyTheme.accent80,size: 30)):SizedBox.shrink(), + titleIcon != null + ? Padding( + padding: EdgeInsets.only(right: 10), + child: Icon(titleIcon, + color: MyTheme.accent80, size: 30)) + : SizedBox.shrink(), Text( title!, style: TextStyle( @@ -284,7 +300,7 @@ class PaddingCard extends StatelessWidget { ), ) ], - ) )); + ))); } return Container( width: double.maxFinite, @@ -302,28 +318,27 @@ class PaddingCard extends StatelessWidget { } Widget clientInfo(Client client) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start - ,children: [ + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( child: Text(client.name[0]), backgroundColor: MyTheme.border), SizedBox(width: 12), Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center - ,children: [ - Text(client.name, style: TextStyle(color: MyTheme.idColor,fontSize: 20)), - SizedBox(width: 8), - Text(client.peerId, style: TextStyle(color: MyTheme.idColor,fontSize: 10)) - ]) + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(client.name, + style: TextStyle(color: MyTheme.idColor, fontSize: 20)), + SizedBox(width: 8), + Text(client.peerId, + style: TextStyle(color: MyTheme.idColor, fontSize: 10)) + ]) ], ), ]); } - void toAndroidChannelInit() { FFI.setMethodCallHandler((method, arguments) { debugPrint("flutter got android msg,$method,$arguments");