From e9f8fd1175dd053f46d16dc4950d41d3fcdc5979 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 3 Mar 2022 14:58:57 +0800 Subject: [PATCH] add client chat page & chat overlay window --- lib/models/chat_model.dart | 39 +++++ lib/models/model.dart | 6 + lib/pages/chat_page.dart | 243 +++++++++++++++++++++++++++---- lib/pages/connection_page.dart | 81 ++++++----- lib/pages/file_manager_page.dart | 38 +++++ lib/pages/home_page.dart | 2 +- lib/pages/remote_page.dart | 6 + pubspec.yaml | 2 + 8 files changed, 351 insertions(+), 66 deletions(-) create mode 100644 lib/pages/file_manager_page.dart diff --git a/lib/models/chat_model.dart b/lib/models/chat_model.dart index 46091b5cd..d70956603 100644 --- a/lib/models/chat_model.dart +++ b/lib/models/chat_model.dart @@ -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 _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(); + } } \ No newline at end of file diff --git a/lib/models/model.dart b/lib/models/model.dart index 8e659facd..3f50be145 100644 --- a/lib/models/model.dart +++ b/lib/models/model.dart @@ -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, diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index f3262c6c5..8bbabec4f 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -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(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, + ); + }))); } -} \ No newline at end of file +} + +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 createState() => _ChatWindowOverlayState(); +} + +class _ChatWindowOverlayState extends State { + 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); +} diff --git a/lib/pages/connection_page.dart b/lib/pages/connection_page.dart index eb2d9f784..02c3a09ba 100644 --- a/lib/pages/connection_page.dart +++ b/lib/pages/connection_page.dart @@ -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 { 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 { void connect(String id) { if (id == '') return; id = id.replaceAll(' ', ''); - () async { - await Navigator.push( - context, - MaterialPageRoute( - 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 { 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 { 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: [ @@ -164,7 +173,7 @@ class _ConnectionPageState extends State { color: MyTheme.darkGray, size: 45), onPressed: onConnect, ), - ) + ), ], ), ), @@ -209,11 +218,11 @@ class _ConnectionPageState extends State { 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 { ); if (value == 'remove') { setState(() => FFI.setByName('remove', '$id')); - () async { + () async { removePreference(id); }(); } } -} \ No newline at end of file +} diff --git a/lib/pages/file_manager_page.dart b/lib/pages/file_manager_page.dart new file mode 100644 index 000000000..42117588e --- /dev/null +++ b/lib/pages/file_manager_page.dart @@ -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 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. + } + }, + ), + ); + }, + ); + }, + )); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 0f3ecf65e..8b88eac27 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -22,7 +22,7 @@ class _HomePageState extends State { var _selectedIndex = 0; final List _pages = [ ConnectionPage(), - ChatPage(), + chatPage, ServerPage(), SettingsPage() ]; diff --git a/lib/pages/remote_page.dart b/lib/pages/remote_page.dart index b07bff520..673d16cac 100644 --- a/lib/pages/remote_page.dart +++ b/lib/pages/remote_page.dart @@ -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 { ) ]) + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.message), + onPressed:toggleChatOverlay, + ), IconButton( color: Colors.white, icon: Icon(Icons.more_vert), diff --git a/pubspec.yaml b/pubspec.yaml index b37dd33f2..70138f856 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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