add client chat page & chat overlay window
This commit is contained in:
		
							parent
							
								
									b106ed5717
								
							
						
					
					
						commit
						e9f8fd1175
					
				| @ -1,5 +1,44 @@ | ||||
| import 'package:dash_chat/dash_chat.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hbb/pages/chat_page.dart'; | ||||
| 
 | ||||
| import 'model.dart'; | ||||
| import 'native_model.dart'; | ||||
| 
 | ||||
| class ChatModel with ChangeNotifier { | ||||
|   final List<ChatMessage> _messages = []; | ||||
| 
 | ||||
|   final ChatUser me = ChatUser( | ||||
|     name:"me", | ||||
|   ); | ||||
| 
 | ||||
|   get messages => _messages; | ||||
| 
 | ||||
|   receive(String text){ | ||||
|     if (text.isEmpty) return; | ||||
|     // first message show overlay icon | ||||
|     if (iconOverlayEntry == null){ | ||||
|       showChatIconOverlay(); | ||||
|     } | ||||
|     _messages.add(ChatMessage(text: text, user: ChatUser( | ||||
|       name:FFI.ffiModel.pi.username, | ||||
|       uid: FFI.getId(), | ||||
|     ))); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| 
 | ||||
|   send(ChatMessage message){ | ||||
|     _messages.add(message); | ||||
|     if(message.text != null && message.text!.isNotEmpty){ | ||||
|       PlatformFFI.setByName("chat",message.text!); | ||||
|     } | ||||
|     notifyListeners(); | ||||
|   } | ||||
| 
 | ||||
|   release(){ | ||||
|     hideChatIconOverlay(); | ||||
|     hideChatWindowOverlay(); | ||||
|     _messages.clear(); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,6 @@ | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_easyloading/flutter_easyloading.dart'; | ||||
| import 'package:flutter_hbb/models/chat_model.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'dart:math'; | ||||
| import 'dart:convert'; | ||||
| @ -128,6 +129,9 @@ class FfiModel with ChangeNotifier { | ||||
|         Clipboard.setData(ClipboardData(text: evt['content'])); | ||||
|       } else if (name == 'permission') { | ||||
|         FFI.ffiModel.updatePermission(evt); | ||||
|       } else if (name == 'chat'){ | ||||
|         // FFI.setByName("chat",msg); | ||||
|         FFI.chatModel.receive(evt['text']??""); | ||||
|       } | ||||
|     } | ||||
|     if (pos != null) FFI.cursorModel.updateCursorPosition(pos); | ||||
| @ -703,6 +707,7 @@ class FFI { | ||||
|   static final cursorModel = CursorModel(); | ||||
|   static final canvasModel = CanvasModel(); | ||||
|   static final serverModel = ServerModel(); | ||||
|   static final chatModel = ChatModel(); | ||||
| 
 | ||||
|   static String getId() { | ||||
|     return getByName('remote_id'); | ||||
| @ -797,6 +802,7 @@ class FFI { | ||||
|   } | ||||
| 
 | ||||
|   static void close() { | ||||
|     chatModel.release(); | ||||
|     if (FFI.imageModel.image != null && !isDesktop) { | ||||
|       savePreference( | ||||
|           id, | ||||
|  | ||||
| @ -1,13 +1,21 @@ | ||||
| import 'package:dash_chat/dash_chat.dart'; | ||||
| import 'package:draggable_float_widget/draggable_float_widget.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hbb/common.dart'; | ||||
| import 'package:flutter_hbb/models/chat_model.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import '../main.dart'; | ||||
| import '../models/model.dart'; | ||||
| import '../models/native_model.dart'; | ||||
| import 'home_page.dart'; | ||||
| 
 | ||||
| OverlayEntry? iconOverlayEntry; | ||||
| OverlayEntry? windowOverlayEntry; | ||||
| 
 | ||||
| ChatPage chatPage = ChatPage(); | ||||
| 
 | ||||
| class ChatPage extends StatelessWidget implements PageShape { | ||||
| 
 | ||||
|   final FocusNode _focusNode = FocusNode(); | ||||
| 
 | ||||
|   @override | ||||
|   final title = "Chat"; | ||||
| 
 | ||||
| @ -19,31 +27,208 @@ class ChatPage extends StatelessWidget implements PageShape { | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       color: MyTheme.darkGray, | ||||
|       child: DashChat( | ||||
|         focusNode: _focusNode, | ||||
|         onSend: (ChatMessage) {}, | ||||
|         user: ChatUser(uid: "111", name: "Bob"), | ||||
|         messages: [ | ||||
|           ChatMessage( | ||||
|               text: "hello", user: ChatUser(uid: "111", name: "Bob")), | ||||
|           ChatMessage( | ||||
|               text: "hi", user: ChatUser(uid: "222", name: "Alice")), | ||||
|           ChatMessage( | ||||
|               text: "hello", user: ChatUser(uid: "111", name: "Bob")), | ||||
|           ChatMessage( | ||||
|               text: "hi", user: ChatUser(uid: "222", name: "Alice")), | ||||
|           ChatMessage( | ||||
|               text: "hello", user: ChatUser(uid: "111", name: "Bob")), | ||||
|           ChatMessage( | ||||
|               text: "hi", user: ChatUser(uid: "222", name: "Alice")), | ||||
|           ChatMessage( | ||||
|               text: "hello", user: ChatUser(uid: "111", name: "Bob")), | ||||
|           ChatMessage( | ||||
|               text: "hi", user: ChatUser(uid: "222", name: "Alice")), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     return ChangeNotifierProvider.value( | ||||
|         value: FFI.chatModel, | ||||
|         child: Container( | ||||
|             color: MyTheme.grayBg, | ||||
|             child: Consumer<ChatModel>(builder: (context, chatModel, child) { | ||||
|               return DashChat( | ||||
|                 sendOnEnter: false, // if true,reload keyboard everytime,need fix | ||||
|                 onSend: (chatMsg) { | ||||
|                   chatModel.send(chatMsg); | ||||
|                 }, | ||||
|                 user: chatModel.me, | ||||
|                 messages: chatModel.messages, | ||||
|               ); | ||||
|             }))); | ||||
|   } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| showChatIconOverlay({Offset offset = const Offset(200, 50)}) { | ||||
|   if (iconOverlayEntry != null) { | ||||
|     iconOverlayEntry!.remove(); | ||||
|   } | ||||
|   if (globalKey.currentState == null || globalKey.currentState!.overlay == null) | ||||
|     return; | ||||
|   final globalOverlayState = globalKey.currentState!.overlay!; | ||||
| 
 | ||||
|   final overlay = OverlayEntry(builder: (context) { | ||||
|     return DraggableFloatWidget( | ||||
|         config: DraggableFloatWidgetBaseConfig( | ||||
|           initPositionYInTop: false, | ||||
|           initPositionYMarginBorder: 100, | ||||
|           borderTopContainTopBar: true, | ||||
|         ), | ||||
|         child: FloatingActionButton( | ||||
|             onPressed: () { | ||||
|               if (windowOverlayEntry == null) { | ||||
|                 showChatWindowOverlay(); | ||||
|               } else { | ||||
|                 hideChatWindowOverlay(); | ||||
|               } | ||||
|             }, | ||||
|             child: Icon(Icons.message))); | ||||
|   }); | ||||
|   globalOverlayState.insert(overlay); | ||||
|   iconOverlayEntry = overlay; | ||||
|   debugPrint("created"); | ||||
| } | ||||
| 
 | ||||
| hideChatIconOverlay() { | ||||
|   if (iconOverlayEntry != null) { | ||||
|     iconOverlayEntry!.remove(); | ||||
|     iconOverlayEntry = null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| final FocusNode _focusNode = FocusNode(); | ||||
| 
 | ||||
| showChatWindowOverlay() { | ||||
|   if (windowOverlayEntry != null) return; | ||||
|   if (globalKey.currentState == null || globalKey.currentState!.overlay == null) | ||||
|     return; | ||||
|   final globalOverlayState = globalKey.currentState!.overlay!; | ||||
| 
 | ||||
|   final overlay = OverlayEntry(builder: (context) { | ||||
|     return ChatWindowOverlay(); | ||||
|   }); | ||||
|   _focusNode.requestFocus(); | ||||
|   globalOverlayState.insert(overlay); | ||||
|   windowOverlayEntry = overlay; | ||||
|   debugPrint("chatEntry created"); | ||||
| } | ||||
| 
 | ||||
| hideChatWindowOverlay() { | ||||
|   if (windowOverlayEntry != null) { | ||||
|     windowOverlayEntry!.remove(); | ||||
|     windowOverlayEntry = null; | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| toggleChatOverlay() { | ||||
|   if (iconOverlayEntry == null || windowOverlayEntry == null) { | ||||
|     showChatIconOverlay(); | ||||
|     showChatWindowOverlay(); | ||||
|   } else { | ||||
|     hideChatIconOverlay(); | ||||
|     hideChatWindowOverlay(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ChatWindowOverlay extends StatefulWidget { | ||||
|   final double windowWidth = 250; | ||||
|   final double windowHeight = 300; | ||||
| 
 | ||||
|   @override | ||||
|   State<StatefulWidget> createState() => _ChatWindowOverlayState(); | ||||
| } | ||||
| 
 | ||||
| class _ChatWindowOverlayState extends State<ChatWindowOverlay> { | ||||
|   Offset _o = Offset(20, 20); | ||||
|   bool _keyboardVisible = false; | ||||
|   double _saveHeight = 0; | ||||
|   double _lastBottomHeight = 0; | ||||
| 
 | ||||
|   changeOffset(Offset offset) { | ||||
|     final size = MediaQuery.of(context).size; | ||||
|     debugPrint("parent size:$size"); | ||||
|     double x = 0; | ||||
|     double y = 0; | ||||
| 
 | ||||
|     if (_o.dx + offset.dx + widget.windowWidth > size.width) { | ||||
|       x = size.width - widget.windowWidth; | ||||
|     } else if (_o.dx + offset.dx < 0) { | ||||
|       x = 0; | ||||
|     } else { | ||||
|       x = _o.dx + offset.dx; | ||||
|     } | ||||
| 
 | ||||
|     if (_o.dy + offset.dy + widget.windowHeight > size.height) { | ||||
|       y = size.height - widget.windowHeight; | ||||
|     } else if (_o.dy + offset.dy < 0) { | ||||
|       y = 0; | ||||
|     } else { | ||||
|       y = _o.dy + offset.dy; | ||||
|     } | ||||
|     setState(() { | ||||
|       _o = Offset(x, y); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   checkScreenSize(){ | ||||
|     // TODO 横屏处理 | ||||
|   } | ||||
| 
 | ||||
|   checkKeyBoard(){ | ||||
|     final bottomHeight = MediaQuery.of(context).viewInsets.bottom; | ||||
|     final currentVisible = bottomHeight != 0; | ||||
| 
 | ||||
|     debugPrint(bottomHeight.toString() + currentVisible.toString()); | ||||
|     // save | ||||
|     if (!_keyboardVisible && currentVisible){ | ||||
|       _saveHeight = _o.dy; | ||||
|       debugPrint("on save $_saveHeight"); | ||||
|     } | ||||
| 
 | ||||
|     // reset | ||||
|     if (_lastBottomHeight>0 && bottomHeight == 0){ | ||||
|       debugPrint("on reset"); | ||||
|       _o = Offset(_o.dx,_saveHeight); | ||||
|     } | ||||
| 
 | ||||
|     // onKeyboardVisible | ||||
|     if (_keyboardVisible && currentVisible){ | ||||
| 
 | ||||
| 
 | ||||
|       final sumHeight = bottomHeight + widget.windowHeight; | ||||
|       final contextHeight = MediaQuery.of(context).size.height; | ||||
|       debugPrint("prepare update sumHeight:$sumHeight,contextHeight:$contextHeight"); | ||||
|       if(sumHeight + _o.dy > contextHeight){ | ||||
|         final y = contextHeight - sumHeight; | ||||
|         debugPrint("on update"); | ||||
|         _o = Offset(_o.dx,y); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     _keyboardVisible = currentVisible; | ||||
|     _lastBottomHeight = bottomHeight; | ||||
| 
 | ||||
|   } | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     checkKeyBoard(); | ||||
|     checkScreenSize(); | ||||
|     return Positioned( | ||||
|         top: _o.dy, | ||||
|         left: _o.dx, | ||||
|         width: widget.windowWidth, | ||||
|         height: widget.windowHeight, | ||||
|         child: Scaffold( | ||||
|           resizeToAvoidBottomInset: false, | ||||
|           appBar: CustomAppBar( | ||||
|             onPanUpdate: (d) => changeOffset(d.delta), | ||||
|             appBar: AppBar( | ||||
|                 title: Text("Chat")), | ||||
|           ), | ||||
|           body: chatPage, | ||||
|         )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { | ||||
|   final GestureDragUpdateCallback onPanUpdate; | ||||
|   final AppBar appBar; | ||||
| 
 | ||||
|   const CustomAppBar( | ||||
|       {Key? key, required this.onPanUpdate, required this.appBar}) | ||||
|       : super(key: key); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return GestureDetector(onPanUpdate: onPanUpdate, child: appBar); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Size get preferredSize => new Size.fromHeight(kToolbarHeight); | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hbb/pages/chat_page.dart'; | ||||
| import 'package:flutter_hbb/pages/file_manager_page.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
| import 'dart:async'; | ||||
| @ -53,6 +55,21 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|             getSearchBarUI(), | ||||
|             Container(height: 12), | ||||
|             getPeers(), | ||||
|             ElevatedButton( | ||||
|                 onPressed: () { | ||||
|                   Navigator.push( | ||||
|                     context, | ||||
|                     MaterialPageRoute( | ||||
|                       builder: (BuildContext context) => FileManagerPage(), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|                 child: Text("File")), | ||||
|             ElevatedButton( | ||||
|                 onPressed: () { | ||||
|                   toggleChatOverlay(); | ||||
|                 }, | ||||
|                 child: Text("Chat")) | ||||
|           ]), | ||||
|     ); | ||||
|   } | ||||
| @ -65,15 +82,12 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|   void connect(String id) { | ||||
|     if (id == '') return; | ||||
|     id = id.replaceAll(' ', ''); | ||||
|         () async { | ||||
|       await Navigator.push<dynamic>( | ||||
|         context, | ||||
|         MaterialPageRoute<dynamic>( | ||||
|           builder: (BuildContext context) => RemotePage(id: id), | ||||
|         ), | ||||
|       ); | ||||
|       setState(() {}); | ||||
|     }(); | ||||
|     Navigator.push( | ||||
|       context, | ||||
|       MaterialPageRoute( | ||||
|         builder: (BuildContext context) => RemotePage(id: id), | ||||
|       ), | ||||
|     ); | ||||
|     FocusScopeNode currentFocus = FocusScope.of(context); | ||||
|     if (!currentFocus.hasPrimaryFocus) { | ||||
|       currentFocus.unfocus(); | ||||
| @ -84,20 +98,20 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|     return _updateUrl.isEmpty | ||||
|         ? SizedBox(height: 0) | ||||
|         : InkWell( | ||||
|         onTap: () async { | ||||
|           final url = _updateUrl + '.apk'; | ||||
|           if (await canLaunch(url)) { | ||||
|             await launch(url); | ||||
|           } | ||||
|         }, | ||||
|         child: Container( | ||||
|             alignment: AlignmentDirectional.center, | ||||
|             width: double.infinity, | ||||
|             color: Colors.pinkAccent, | ||||
|             padding: EdgeInsets.symmetric(vertical: 12), | ||||
|             child: Text(translate('Download new version'), | ||||
|                 style: TextStyle( | ||||
|                     color: Colors.white, fontWeight: FontWeight.bold)))); | ||||
|             onTap: () async { | ||||
|               final url = _updateUrl + '.apk'; | ||||
|               if (await canLaunch(url)) { | ||||
|                 await launch(url); | ||||
|               } | ||||
|             }, | ||||
|             child: Container( | ||||
|                 alignment: AlignmentDirectional.center, | ||||
|                 width: double.infinity, | ||||
|                 color: Colors.pinkAccent, | ||||
|                 padding: EdgeInsets.symmetric(vertical: 12), | ||||
|                 child: Text(translate('Download new version'), | ||||
|                     style: TextStyle( | ||||
|                         color: Colors.white, fontWeight: FontWeight.bold)))); | ||||
|   } | ||||
| 
 | ||||
|   Widget getSearchBarUI() { | ||||
| @ -113,12 +127,7 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|           child: Ink( | ||||
|             decoration: BoxDecoration( | ||||
|               color: MyTheme.white, | ||||
|               borderRadius: const BorderRadius.only( | ||||
|                 bottomRight: Radius.circular(13.0), | ||||
|                 bottomLeft: Radius.circular(13.0), | ||||
|                 topLeft: Radius.circular(13.0), | ||||
|                 topRight: Radius.circular(13.0), | ||||
|               ), | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(13)), | ||||
|             ), | ||||
|             child: Row( | ||||
|               children: <Widget>[ | ||||
| @ -164,7 +173,7 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|                         color: MyTheme.darkGray, size: 45), | ||||
|                     onPressed: onConnect, | ||||
|                   ), | ||||
|                 ) | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
| @ -209,11 +218,11 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|           child: Card( | ||||
|               child: GestureDetector( | ||||
|                   onTap: () => { | ||||
|                     if (!isDesktop) {connect('${p.id}')} | ||||
|                   }, | ||||
|                         if (!isDesktop) {connect('${p.id}')} | ||||
|                       }, | ||||
|                   onDoubleTap: () => { | ||||
|                     if (isDesktop) {connect('${p.id}')} | ||||
|                   }, | ||||
|                         if (isDesktop) {connect('${p.id}')} | ||||
|                       }, | ||||
|                   onLongPressStart: (details) { | ||||
|                     var x = details.globalPosition.dx; | ||||
|                     var y = details.globalPosition.dy; | ||||
| @ -258,9 +267,9 @@ class _ConnectionPageState extends State<ConnectionPage> { | ||||
|     ); | ||||
|     if (value == 'remove') { | ||||
|       setState(() => FFI.setByName('remove', '$id')); | ||||
|           () async { | ||||
|       () async { | ||||
|         removePreference(id); | ||||
|       }(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | ||||
							
								
								
									
										38
									
								
								lib/pages/file_manager_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/pages/file_manager_page.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import 'dart:io'; | ||||
| import 'package:file_manager/file_manager.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| final FileManagerController controller = FileManagerController(); | ||||
| 
 | ||||
| class FileManagerPage extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|         body: FileManager( | ||||
|       controller: controller, | ||||
|       builder: (context, snapshot) { | ||||
|         final List<FileSystemEntity> entities = snapshot; | ||||
|         return ListView.builder( | ||||
|           itemCount: entities.length, | ||||
|           itemBuilder: (context, index) { | ||||
|             return Card( | ||||
|               child: ListTile( | ||||
|                 leading: FileManager.isFile(entities[index]) | ||||
|                     ? Icon(Icons.feed_outlined) | ||||
|                     : Icon(Icons.folder), | ||||
|                 title: Text(FileManager.basename(entities[index])), | ||||
|                 onTap: () { | ||||
|                   if (FileManager.isDirectory(entities[index])) { | ||||
|                     controller.openDirectory(entities[index]); // open directory | ||||
|                   } else { | ||||
|                     // Perform file-related tasks. | ||||
|                   } | ||||
|                 }, | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
|       }, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| @ -22,7 +22,7 @@ class _HomePageState extends State<HomePage> { | ||||
|   var _selectedIndex = 0; | ||||
|   final List<PageShape> _pages = [ | ||||
|     ConnectionPage(), | ||||
|     ChatPage(), | ||||
|     chatPage, | ||||
|     ServerPage(), | ||||
|     SettingsPage() | ||||
|   ]; | ||||
|  | ||||
| @ -11,6 +11,7 @@ import 'package:wakelock/wakelock.dart'; | ||||
| import '../common.dart'; | ||||
| import '../gestures.dart'; | ||||
| import '../models/model.dart'; | ||||
| import 'chat_page.dart'; | ||||
| 
 | ||||
| final initText = '\1' * 1024; | ||||
| 
 | ||||
| @ -307,6 +308,11 @@ class _RemotePageState extends State<RemotePage> { | ||||
|                           ) | ||||
|                         ]) + | ||||
|                   <Widget>[ | ||||
|                     IconButton( | ||||
|                       color: Colors.white, | ||||
|                       icon: Icon(Icons.message), | ||||
|                       onPressed:toggleChatOverlay, | ||||
|                     ), | ||||
|                     IconButton( | ||||
|                       color: Colors.white, | ||||
|                       icon: Icon(Icons.more_vert), | ||||
|  | ||||
| @ -46,7 +46,9 @@ dependencies: | ||||
|   shared_preferences: ^2.0.6 | ||||
|   toggle_switch: ^1.4.0 | ||||
|   dash_chat: ^1.1.16 | ||||
|   draggable_float_widget: ^0.0.2 | ||||
|   settings_ui: ^2.0.2 | ||||
|   file_manager: ^1.0.0 | ||||
| 
 | ||||
| dev_dependencies: | ||||
|   flutter_launcher_icons: ^0.9.1 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user