Merge pull request #3123 from Heap-Hop/opt_chat_overlay_and_fix_pageview_2
Fix chat text selectable, refactor overlay and fix pageview
This commit is contained in:
commit
5aee66e89c
@ -367,20 +367,25 @@ class Dialog<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OverlayKeyState {
|
||||||
|
final _overlayKey = GlobalKey<OverlayState>();
|
||||||
|
|
||||||
|
/// use global overlay by default
|
||||||
|
OverlayState? get state =>
|
||||||
|
_overlayKey.currentState ?? globalKey.currentState?.overlay;
|
||||||
|
|
||||||
|
GlobalKey<OverlayState>? get key => _overlayKey;
|
||||||
|
}
|
||||||
|
|
||||||
class OverlayDialogManager {
|
class OverlayDialogManager {
|
||||||
OverlayState? _overlayState;
|
|
||||||
final Map<String, Dialog> _dialogs = {};
|
final Map<String, Dialog> _dialogs = {};
|
||||||
|
var _overlayKeyState = OverlayKeyState();
|
||||||
int _tagCount = 0;
|
int _tagCount = 0;
|
||||||
|
|
||||||
OverlayEntry? _mobileActionsOverlayEntry;
|
OverlayEntry? _mobileActionsOverlayEntry;
|
||||||
|
|
||||||
/// By default OverlayDialogManager use global overlay
|
void setOverlayState(OverlayKeyState overlayKeyState) {
|
||||||
OverlayDialogManager() {
|
_overlayKeyState = overlayKeyState;
|
||||||
_overlayState = globalKey.currentState?.overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOverlayState(OverlayState? overlayState) {
|
|
||||||
_overlayState = overlayState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dismissAll() {
|
void dismissAll() {
|
||||||
@ -404,7 +409,7 @@ class OverlayDialogManager {
|
|||||||
bool useAnimation = true,
|
bool useAnimation = true,
|
||||||
bool forceGlobal = false}) {
|
bool forceGlobal = false}) {
|
||||||
final overlayState =
|
final overlayState =
|
||||||
forceGlobal ? globalKey.currentState?.overlay : _overlayState;
|
forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state;
|
||||||
|
|
||||||
if (overlayState == null) {
|
if (overlayState == null) {
|
||||||
return Future.error(
|
return Future.error(
|
||||||
@ -508,7 +513,8 @@ class OverlayDialogManager {
|
|||||||
|
|
||||||
void showMobileActionsOverlay({FFI? ffi}) {
|
void showMobileActionsOverlay({FFI? ffi}) {
|
||||||
if (_mobileActionsOverlayEntry != null) return;
|
if (_mobileActionsOverlayEntry != null) return;
|
||||||
if (_overlayState == null) return;
|
final overlayState = _overlayKeyState.state;
|
||||||
|
if (overlayState == null) return;
|
||||||
|
|
||||||
// compute overlay position
|
// compute overlay position
|
||||||
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
||||||
@ -534,7 +540,7 @@ class OverlayDialogManager {
|
|||||||
onHidePressed: () => hideMobileActionsOverlay(),
|
onHidePressed: () => hideMobileActionsOverlay(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
_overlayState!.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
_mobileActionsOverlayEntry = overlay;
|
_mobileActionsOverlayEntry = overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,10 +95,31 @@ class ChatPage extends StatelessWidget implements PageShape {
|
|||||||
color: Theme.of(context).colorScheme.primary)),
|
color: Theme.of(context).colorScheme.primary)),
|
||||||
messageOptions: MessageOptions(
|
messageOptions: MessageOptions(
|
||||||
showOtherUsersAvatar: false,
|
showOtherUsersAvatar: false,
|
||||||
showTime: true,
|
|
||||||
currentUserTextColor: Colors.white,
|
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
maxWidth: constraints.maxWidth * 0.7,
|
maxWidth: constraints.maxWidth * 0.7,
|
||||||
|
messageTextBuilder: (message, _, __) {
|
||||||
|
final isOwnMessage =
|
||||||
|
message.user.id == currentUser.id;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: isOwnMessage
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
messageDecorationBuilder: (_, __, ___) =>
|
messageDecorationBuilder: (_, __, ___) =>
|
||||||
defaultMessageDecoration(
|
defaultMessageDecoration(
|
||||||
color: MyTheme.accent80,
|
color: MyTheme.accent80,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
@ -96,12 +97,14 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||||
child: Row(children: [
|
child: Obx(() => Opacity(
|
||||||
Icon(Icons.chat_bubble_outline,
|
opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4,
|
||||||
size: 20, color: Theme.of(context).colorScheme.primary),
|
child: Row(children: [
|
||||||
SizedBox(width: 6),
|
Icon(Icons.chat_bubble_outline,
|
||||||
Text(translate("Chat"))
|
size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
])),
|
SizedBox(width: 6),
|
||||||
|
Text(translate("Chat"))
|
||||||
|
])))),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(2),
|
padding: EdgeInsets.all(2),
|
||||||
child: ActionIcon(
|
child: ActionIcon(
|
||||||
@ -304,15 +307,17 @@ class _DraggableState extends State<Draggable> {
|
|||||||
if (widget.checkKeyboard) {
|
if (widget.checkKeyboard) {
|
||||||
checkKeyboard();
|
checkKeyboard();
|
||||||
}
|
}
|
||||||
if (widget.checkKeyboard) {
|
if (widget.checkScreenSize) {
|
||||||
checkScreenSize();
|
checkScreenSize();
|
||||||
}
|
}
|
||||||
return Positioned(
|
return Stack(children: [
|
||||||
top: _position.dy,
|
Positioned(
|
||||||
left: _position.dx,
|
top: _position.dy,
|
||||||
width: widget.width,
|
left: _position.dx,
|
||||||
height: widget.height,
|
width: widget.width,
|
||||||
child: widget.builder(context, onPanUpdate));
|
height: widget.height,
|
||||||
|
child: widget.builder(context, onPanUpdate))
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,3 +371,55 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: const SizedBox.shrink()));
|
: const SizedBox.shrink()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BlockableOverlayState extends OverlayKeyState {
|
||||||
|
final _middleBlocked = false.obs;
|
||||||
|
|
||||||
|
VoidCallback? onMiddleBlockedClick; // to-do use listener
|
||||||
|
|
||||||
|
RxBool get middleBlocked => _middleBlocked;
|
||||||
|
|
||||||
|
void addMiddleBlockedListener(void Function(bool) cb) {
|
||||||
|
_middleBlocked.listen(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMiddleBlocked(bool blocked) {
|
||||||
|
if (blocked != _middleBlocked.value) {
|
||||||
|
_middleBlocked.value = blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockableOverlay extends StatelessWidget {
|
||||||
|
final Widget underlying;
|
||||||
|
final List<OverlayEntry>? upperLayer;
|
||||||
|
|
||||||
|
final BlockableOverlayState state;
|
||||||
|
|
||||||
|
BlockableOverlay(
|
||||||
|
{required this.underlying, required this.state, this.upperLayer});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final initialEntries = [
|
||||||
|
OverlayEntry(builder: (_) => underlying),
|
||||||
|
|
||||||
|
/// middle layer
|
||||||
|
OverlayEntry(
|
||||||
|
builder: (context) => Obx(() => Listener(
|
||||||
|
onPointerDown: (_) {
|
||||||
|
state.onMiddleBlockedClick?.call();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
state.middleBlocked.value ? Colors.transparent : null)))),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (upperLayer != null) {
|
||||||
|
initialEntries.addAll(upperLayer!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set key
|
||||||
|
return Overlay(key: state.key, initialEntries: initialEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -64,23 +64,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tabWidget = Container(
|
final tabWidget = Container(
|
||||||
child: Overlay(initialEntries: [
|
child: Scaffold(
|
||||||
OverlayEntry(builder: (context) {
|
backgroundColor: Theme.of(context).backgroundColor,
|
||||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
body: DesktopTab(
|
||||||
return Scaffold(
|
controller: tabController,
|
||||||
backgroundColor: Theme.of(context).backgroundColor,
|
tail: ActionIcon(
|
||||||
body: DesktopTab(
|
message: 'Settings',
|
||||||
controller: tabController,
|
icon: IconFont.menu,
|
||||||
tail: ActionIcon(
|
onTap: DesktopTabPage.onAddSetting,
|
||||||
message: 'Settings',
|
isClose: false,
|
||||||
icon: IconFont.menu,
|
),
|
||||||
onTap: DesktopTabPage.onAddSetting,
|
)));
|
||||||
isClose: false,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
return Platform.isMacOS
|
return Platform.isMacOS
|
||||||
? tabWidget
|
? tabWidget
|
||||||
: Obx(
|
: Obx(
|
||||||
|
@ -80,6 +80,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Entry? _lastClickEntry;
|
Entry? _lastClickEntry;
|
||||||
|
|
||||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||||
|
final _overlayKeyState = OverlayKeyState();
|
||||||
|
|
||||||
ScrollController getBreadCrumbScrollController(bool isLocal) {
|
ScrollController getBreadCrumbScrollController(bool isLocal) {
|
||||||
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
|
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
|
||||||
@ -115,6 +116,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
// register location listener
|
// register location listener
|
||||||
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
|
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
|
||||||
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
|
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
|
||||||
|
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -137,9 +139,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return Overlay(initialEntries: [
|
return Overlay(key: _overlayKeyState.key, initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (_) {
|
||||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
|
||||||
return ChangeNotifierProvider.value(
|
return ChangeNotifierProvider.value(
|
||||||
value: _ffi.fileModel,
|
value: _ffi.fileModel,
|
||||||
child: Consumer<FileModel>(builder: (context, model, child) {
|
child: Consumer<FileModel>(builder: (context, model, child) {
|
||||||
|
@ -61,6 +61,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
late RxBool _remoteCursorMoved;
|
late RxBool _remoteCursorMoved;
|
||||||
late RxBool _keyboardEnabled;
|
late RxBool _keyboardEnabled;
|
||||||
|
|
||||||
|
final _blockableOverlayState = BlockableOverlayState();
|
||||||
|
|
||||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||||
|
|
||||||
Function(bool)? _onEnterOrLeaveImage4Menubar;
|
Function(bool)? _onEnterOrLeaveImage4Menubar;
|
||||||
@ -132,6 +134,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// });
|
// });
|
||||||
// _isCustomCursorInited = true;
|
// _isCustomCursorInited = true;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
_ffi.dialogManager.setOverlayState(_blockableOverlayState);
|
||||||
|
_ffi.chatModel.setOverlayState(_blockableOverlayState);
|
||||||
|
// make remote page penetrable automatically, effective for chat over remote
|
||||||
|
_blockableOverlayState.onMiddleBlockedClick = () {
|
||||||
|
_blockableOverlayState.setMiddleBlocked(false);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -191,39 +200,50 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
Widget buildBody(BuildContext context) {
|
Widget buildBody(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).backgroundColor,
|
backgroundColor: Theme.of(context).backgroundColor,
|
||||||
body: Overlay(
|
|
||||||
initialEntries: [
|
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
|
||||||
OverlayEntry(builder: (context) {
|
/// see override build() in [BlockableOverlay]
|
||||||
_ffi.chatModel.setOverlayState(Overlay.of(context));
|
body: BlockableOverlay(
|
||||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
state: _blockableOverlayState,
|
||||||
return Container(
|
underlying: Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: RawKeyFocusScope(
|
child: RawKeyFocusScope(
|
||||||
focusNode: _rawKeyFocusNode,
|
focusNode: _rawKeyFocusNode,
|
||||||
onFocusChange: (bool imageFocused) {
|
onFocusChange: (bool imageFocused) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
|
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
|
||||||
// See [onWindowBlur].
|
// See [onWindowBlur].
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
if (_isWindowBlur) {
|
if (_isWindowBlur) {
|
||||||
imageFocused = false;
|
imageFocused = false;
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
_rawKeyFocusNode.unfocus();
|
_rawKeyFocusNode.unfocus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (imageFocused) {
|
if (imageFocused) {
|
||||||
_ffi.inputModel.enterOrLeave(true);
|
_ffi.inputModel.enterOrLeave(true);
|
||||||
} else {
|
} else {
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inputModel: _ffi.inputModel,
|
inputModel: _ffi.inputModel,
|
||||||
child: getBodyForDesktop(context)));
|
child: getBodyForDesktop(context))),
|
||||||
})
|
upperLayer: [
|
||||||
],
|
OverlayEntry(
|
||||||
));
|
builder: (context) => RemoteMenubar(
|
||||||
|
id: widget.id,
|
||||||
|
ffi: _ffi,
|
||||||
|
state: widget.menubarState,
|
||||||
|
onEnterOrLeaveImageSetter: (func) =>
|
||||||
|
_onEnterOrLeaveImage4Menubar = func,
|
||||||
|
onEnterOrLeaveImageCleaner: () =>
|
||||||
|
_onEnterOrLeaveImage4Menubar = null,
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -344,13 +364,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
QualityMonitor(_ffi.qualityMonitorModel), null, null),
|
QualityMonitor(_ffi.qualityMonitorModel), null, null),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
paints.add(RemoteMenubar(
|
|
||||||
id: widget.id,
|
|
||||||
ffi: _ffi,
|
|
||||||
state: widget.menubarState,
|
|
||||||
onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func,
|
|
||||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null,
|
|
||||||
));
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: paints,
|
children: paints,
|
||||||
);
|
);
|
||||||
|
@ -68,26 +68,19 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
],
|
],
|
||||||
child: Consumer<ServerModel>(
|
child: Consumer<ServerModel>(
|
||||||
builder: (context, serverModel, child) => Container(
|
builder: (context, serverModel, child) => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border:
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
Border.all(color: MyTheme.color(context).border!)),
|
child: Scaffold(
|
||||||
child: Overlay(initialEntries: [
|
backgroundColor: Theme.of(context).backgroundColor,
|
||||||
OverlayEntry(builder: (context) {
|
body: Center(
|
||||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
child: Column(
|
||||||
return Scaffold(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
backgroundColor: Theme.of(context).backgroundColor,
|
children: [
|
||||||
body: Center(
|
Expanded(child: ConnectionManager()),
|
||||||
child: Column(
|
],
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
),
|
||||||
children: [
|
),
|
||||||
Expanded(child: ConnectionManager()),
|
))));
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -688,9 +688,11 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _chatButtonKey = GlobalKey();
|
||||||
Widget _buildChat(BuildContext context) {
|
Widget _buildChat(BuildContext context) {
|
||||||
FfiModel ffiModel = Provider.of<FfiModel>(context);
|
FfiModel ffiModel = Provider.of<FfiModel>(context);
|
||||||
return mod_menu.PopupMenuButton(
|
return mod_menu.PopupMenuButton(
|
||||||
|
key: _chatButtonKey,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: SvgPicture.asset(
|
icon: SvgPicture.asset(
|
||||||
"assets/chat.svg",
|
"assets/chat.svg",
|
||||||
@ -779,8 +781,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
proc: () {
|
proc: () {
|
||||||
|
RenderBox? renderBox =
|
||||||
|
_chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
|
||||||
|
Offset? initPos;
|
||||||
|
if (renderBox != null) {
|
||||||
|
final pos = renderBox.localToGlobal(Offset.zero);
|
||||||
|
initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
|
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
|
||||||
widget.ffi.chatModel.toggleChatOverlay();
|
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||||
},
|
},
|
||||||
padding: padding,
|
padding: padding,
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
@ -327,14 +327,32 @@ class DesktopTab extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _tabWidgets = [];
|
||||||
Widget _buildPageView() {
|
Widget _buildPageView() {
|
||||||
return _buildBlock(
|
return _buildBlock(
|
||||||
child: Obx(() => PageView(
|
child: Obx(() => PageView(
|
||||||
controller: state.value.pageController,
|
controller: state.value.pageController,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
children: state.value.tabs
|
children: () {
|
||||||
.map((tab) => tab.page)
|
/// to-do refactor, separate connection state and UI state for remote session.
|
||||||
.toList(growable: false))));
|
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
||||||
|
final tabLen = state.value.tabs.length;
|
||||||
|
if (tabLen == _tabWidgets.length) {
|
||||||
|
return _tabWidgets;
|
||||||
|
} else if (_tabWidgets.isNotEmpty &&
|
||||||
|
tabLen == _tabWidgets.length + 1) {
|
||||||
|
/// On add. Use the previous list(pointer) to prevent item's state init twice.
|
||||||
|
/// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built.
|
||||||
|
_tabWidgets.add(state.value.tabs.last.page);
|
||||||
|
return _tabWidgets;
|
||||||
|
} else {
|
||||||
|
/// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal.
|
||||||
|
/// the Widgets in list must enable [AutomaticKeepAliveClientMixin]
|
||||||
|
final newList = state.value.tabs.map((v) => v.page).toList();
|
||||||
|
_tabWidgets = newList;
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
|
}())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether to show ListView
|
/// Check whether to show ListView
|
||||||
@ -767,7 +785,8 @@ class _ListView extends StatelessWidget {
|
|||||||
tabBuilder: tabBuilder,
|
tabBuilder: tabBuilder,
|
||||||
tabMenuBuilder: tabMenuBuilder,
|
tabMenuBuilder: tabMenuBuilder,
|
||||||
maxLabelWidth: maxLabelWidth,
|
maxLabelWidth: maxLabelWidth,
|
||||||
selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor,
|
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
||||||
|
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
||||||
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
||||||
);
|
);
|
||||||
}).toList()));
|
}).toList()));
|
||||||
@ -1121,7 +1140,8 @@ class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
|||||||
dividerColor: dividerColor ?? this.dividerColor,
|
dividerColor: dividerColor ?? this.dividerColor,
|
||||||
hoverColor: hoverColor ?? this.hoverColor,
|
hoverColor: hoverColor ?? this.hoverColor,
|
||||||
closeHoverColor: closeHoverColor ?? this.closeHoverColor,
|
closeHoverColor: closeHoverColor ?? this.closeHoverColor,
|
||||||
selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor,
|
selectedTabBackgroundColor:
|
||||||
|
selectedTabBackgroundColor ?? this.selectedTabBackgroundColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1147,7 +1167,8 @@ class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
|||||||
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
|
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
|
||||||
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
|
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
|
||||||
closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t),
|
closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t),
|
||||||
selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t),
|
selectedTabBackgroundColor: Color.lerp(
|
||||||
|
selectedTabBackgroundColor, other.selectedTabBackgroundColor, t),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dash_chat_2/dash_chat_2.dart';
|
import 'package:dash_chat_2/dash_chat_2.dart';
|
||||||
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@ -27,16 +30,13 @@ class MessageBody {
|
|||||||
class ChatModel with ChangeNotifier {
|
class ChatModel with ChangeNotifier {
|
||||||
static final clientModeID = -1;
|
static final clientModeID = -1;
|
||||||
|
|
||||||
/// _overlayState:
|
|
||||||
/// Desktop: store session overlay by using [setOverlayState].
|
|
||||||
/// Mobile: always null, use global overlay.
|
|
||||||
/// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay]
|
|
||||||
OverlayState? _overlayState;
|
|
||||||
OverlayEntry? chatIconOverlayEntry;
|
OverlayEntry? chatIconOverlayEntry;
|
||||||
OverlayEntry? chatWindowOverlayEntry;
|
OverlayEntry? chatWindowOverlayEntry;
|
||||||
|
|
||||||
bool isConnManager = false;
|
bool isConnManager = false;
|
||||||
|
|
||||||
|
RxBool isWindowFocus = true.obs;
|
||||||
|
BlockableOverlayState? _blockableOverlayState;
|
||||||
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
|
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
|
||||||
|
|
||||||
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
|
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
|
||||||
@ -58,6 +58,19 @@ class ChatModel with ChangeNotifier {
|
|||||||
|
|
||||||
bool get isShowCMChatPage => _isShowCMChatPage;
|
bool get isShowCMChatPage => _isShowCMChatPage;
|
||||||
|
|
||||||
|
void setOverlayState(BlockableOverlayState blockableOverlayState) {
|
||||||
|
_blockableOverlayState = blockableOverlayState;
|
||||||
|
|
||||||
|
_blockableOverlayState!.addMiddleBlockedListener((v) {
|
||||||
|
if (!v) {
|
||||||
|
isWindowFocus.value = false;
|
||||||
|
if (isWindowFocus.value) {
|
||||||
|
isWindowFocus.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
|
|
||||||
ChatModel(this.parent);
|
ChatModel(this.parent);
|
||||||
@ -74,20 +87,6 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOverlayState(OverlayState? os) {
|
|
||||||
_overlayState = os;
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayState? _getOverlayState() {
|
|
||||||
if (_overlayState == null) {
|
|
||||||
if (globalKey.currentState == null ||
|
|
||||||
globalKey.currentState!.overlay == null) return null;
|
|
||||||
return globalKey.currentState!.overlay;
|
|
||||||
} else {
|
|
||||||
return _overlayState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
|
showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
|
||||||
if (chatIconOverlayEntry != null) {
|
if (chatIconOverlayEntry != null) {
|
||||||
chatIconOverlayEntry!.remove();
|
chatIconOverlayEntry!.remove();
|
||||||
@ -100,7 +99,7 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final overlayState = _getOverlayState();
|
final overlayState = _blockableOverlayState?.state;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
|
|
||||||
final overlay = OverlayEntry(builder: (context) {
|
final overlay = OverlayEntry(builder: (context) {
|
||||||
@ -132,23 +131,35 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showChatWindowOverlay() {
|
showChatWindowOverlay({Offset? chatInitPos}) {
|
||||||
if (chatWindowOverlayEntry != null) return;
|
if (chatWindowOverlayEntry != null) return;
|
||||||
final overlayState = _getOverlayState();
|
isWindowFocus.value = true;
|
||||||
|
_blockableOverlayState?.setMiddleBlocked(true);
|
||||||
|
|
||||||
|
final overlayState = _blockableOverlayState?.state;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
final overlay = OverlayEntry(builder: (context) {
|
final overlay = OverlayEntry(builder: (context) {
|
||||||
return DraggableChatWindow(
|
return Listener(
|
||||||
position: const Offset(20, 80),
|
onPointerDown: (_) {
|
||||||
width: 250,
|
if (!isWindowFocus.value) {
|
||||||
height: 350,
|
isWindowFocus.value = true;
|
||||||
chatModel: this);
|
_blockableOverlayState?.setMiddleBlocked(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: DraggableChatWindow(
|
||||||
|
position: chatInitPos ?? Offset(20, 80),
|
||||||
|
width: 250,
|
||||||
|
height: 350,
|
||||||
|
chatModel: this));
|
||||||
});
|
});
|
||||||
overlayState.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
chatWindowOverlayEntry = overlay;
|
chatWindowOverlayEntry = overlay;
|
||||||
|
requestChatInputFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
hideChatWindowOverlay() {
|
hideChatWindowOverlay() {
|
||||||
if (chatWindowOverlayEntry != null) {
|
if (chatWindowOverlayEntry != null) {
|
||||||
|
_blockableOverlayState?.setMiddleBlocked(false);
|
||||||
chatWindowOverlayEntry!.remove();
|
chatWindowOverlayEntry!.remove();
|
||||||
chatWindowOverlayEntry = null;
|
chatWindowOverlayEntry = null;
|
||||||
return;
|
return;
|
||||||
@ -158,13 +169,13 @@ class ChatModel with ChangeNotifier {
|
|||||||
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
|
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
|
||||||
chatWindowOverlayEntry == null);
|
chatWindowOverlayEntry == null);
|
||||||
|
|
||||||
toggleChatOverlay() {
|
toggleChatOverlay({Offset? chatInitPos}) {
|
||||||
if (_isChatOverlayHide()) {
|
if (_isChatOverlayHide()) {
|
||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
if (!isDesktop) {
|
if (!isDesktop) {
|
||||||
showChatIconOverlay();
|
showChatIconOverlay();
|
||||||
}
|
}
|
||||||
showChatWindowOverlay();
|
showChatWindowOverlay(chatInitPos: chatInitPos);
|
||||||
} else {
|
} else {
|
||||||
hideChatIconOverlay();
|
hideChatIconOverlay();
|
||||||
hideChatWindowOverlay();
|
hideChatWindowOverlay();
|
||||||
@ -194,6 +205,7 @@ class ChatModel with ChangeNotifier {
|
|||||||
await windowManager.setSizeAlignment(
|
await windowManager.setSizeAlignment(
|
||||||
kConnectionManagerWindowSize, Alignment.topRight);
|
kConnectionManagerWindowSize, Alignment.topRight);
|
||||||
} else {
|
} else {
|
||||||
|
requestChatInputFocus();
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight);
|
await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight);
|
||||||
_isShowCMChatPage = !_isShowCMChatPage;
|
_isShowCMChatPage = !_isShowCMChatPage;
|
||||||
@ -291,7 +303,6 @@ class ChatModel with ChangeNotifier {
|
|||||||
close() {
|
close() {
|
||||||
hideChatIconOverlay();
|
hideChatIconOverlay();
|
||||||
hideChatWindowOverlay();
|
hideChatWindowOverlay();
|
||||||
_overlayState = null;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +310,14 @@ class ChatModel with ChangeNotifier {
|
|||||||
_messages[clientModeID]?.clear();
|
_messages[clientModeID]?.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void requestChatInputFocus() {
|
||||||
|
Timer(Duration(milliseconds: 100), () {
|
||||||
|
if (inputNode.hasListeners && inputNode.canRequestFocus) {
|
||||||
|
inputNode.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void onVoiceCallWaiting() {
|
void onVoiceCallWaiting() {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
|
_voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import 'package:flutter_hbb/models/server_model.dart';
|
|||||||
import 'package:flutter_hbb/models/user_model.dart';
|
import 'package:flutter_hbb/models/user_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:image/image.dart' as img2;
|
import 'package:image/image.dart' as img2;
|
||||||
import 'package:flutter_custom_cursor/cursor_manager.dart';
|
import 'package:flutter_custom_cursor/cursor_manager.dart';
|
||||||
@ -26,7 +25,6 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../common/shared_state.dart';
|
|
||||||
import '../utils/image.dart' as img;
|
import '../utils/image.dart' as img;
|
||||||
import '../mobile/widgets/dialog.dart';
|
import '../mobile/widgets/dialog.dart';
|
||||||
import 'input_model.dart';
|
import 'input_model.dart';
|
||||||
@ -1393,13 +1391,13 @@ class FFI {
|
|||||||
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
|
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
|
||||||
}
|
}
|
||||||
bind.sessionClose(id: id);
|
bind.sessionClose(id: id);
|
||||||
id = '';
|
|
||||||
imageModel.update(null);
|
imageModel.update(null);
|
||||||
cursorModel.clear();
|
cursorModel.clear();
|
||||||
ffiModel.clear();
|
ffiModel.clear();
|
||||||
canvasModel.clear();
|
canvasModel.clear();
|
||||||
inputModel.resetModifiers();
|
inputModel.resetModifiers();
|
||||||
debugPrint('model $id closed');
|
debugPrint('model $id closed');
|
||||||
|
id = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMethodCallHandler(FMethod callback) {
|
void setMethodCallHandler(FMethod callback) {
|
||||||
|
@ -16,7 +16,7 @@ final testClients = [
|
|||||||
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
|
Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false)
|
||||||
];
|
];
|
||||||
|
|
||||||
/// -t lib/cm_main.dart to test cm
|
/// flutter run -d {platform} -t lib/cm_test.dart to test cm
|
||||||
void main(List<String> args) async {
|
void main(List<String> args) async {
|
||||||
isTest = true;
|
isTest = true;
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user