fix: mobile actions, position ()

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2024-06-23 11:06:47 +08:00 committed by GitHub
parent a9e0ea8520
commit 40cb59336f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 164 additions and 48 deletions

@ -718,7 +718,21 @@ class OverlayDialogManager {
int _tagCount = 0; int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry; OverlayEntry? _mobileActionsOverlayEntry;
RxBool mobileActionsOverlayVisible = false.obs; RxBool mobileActionsOverlayVisible = true.obs;
setMobileActionsOverlayVisible(bool v, {store = true}) {
if (store) {
bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
}
// No need to read the value from local storage after setting it.
// It better to toggle the value directly.
mobileActionsOverlayVisible.value = v;
}
loadMobileActionsOverlayVisible() {
mobileActionsOverlayVisible.value =
bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
}
void setOverlayState(OverlayKeyState overlayKeyState) { void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState; _overlayKeyState = overlayKeyState;
@ -865,14 +879,14 @@ class OverlayDialogManager {
); );
overlayState.insert(overlay); overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay; _mobileActionsOverlayEntry = overlay;
mobileActionsOverlayVisible.value = true; setMobileActionsOverlayVisible(true);
} }
void hideMobileActionsOverlay() { void hideMobileActionsOverlay({store = true}) {
if (_mobileActionsOverlayEntry != null) { if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove(); _mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null; _mobileActionsOverlayEntry = null;
mobileActionsOverlayVisible.value = false; setMobileActionsOverlayVisible(false, store: store);
return; return;
} }
} }
@ -891,21 +905,27 @@ class OverlayDialogManager {
} }
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) { makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
final position = SimpleWrapper(Offset(0, 0));
makeMobileActions(BuildContext context, double s) { makeMobileActions(BuildContext context, double s) {
final scale = s < 0.85 ? 0.85 : s; final scale = s < 0.85 ? 0.85 : s;
final session = ffi ?? gFFI; final session = ffi ?? gFFI;
// compute overlay position
final screenW = MediaQuery.of(context).size.width;
final screenH = MediaQuery.of(context).size.height;
const double overlayW = 200; const double overlayW = 200;
const double overlayH = 45; const double overlayH = 45;
final left = (screenW - overlayW * scale) / 2; computeOverlayPosition() {
final top = screenH - (overlayH + 80) * scale; final screenW = MediaQuery.of(context).size.width;
position.value = Offset(left, top); final screenH = MediaQuery.of(context).size.height;
final left = (screenW - overlayW * scale) / 2;
final top = screenH - (overlayH + 80) * scale;
return Offset(left, top);
}
if (draggablePositions.mobileActions.isInvalid()) {
draggablePositions.mobileActions.update(computeOverlayPosition());
} else {
draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
}
return DraggableMobileActions( return DraggableMobileActions(
scale: scale, scale: scale,
position: position, position: draggablePositions.mobileActions,
width: overlayW, width: overlayW,
height: overlayH, height: overlayH,
onBackPressed: () => session.inputModel.tap(MouseButtons.right), onBackPressed: () => session.inputModel.tap(MouseButtons.right),

@ -1,6 +1,8 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:debounce_throttle/debounce_throttle.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:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (draggablePositions.chatWindow.isInvalid()) {
draggablePositions.chatWindow.update(position);
}
return isIOS return isIOS
? IOSDraggable( ? IOSDraggable(
position: position, position: draggablePositions.chatWindow,
chatModel: chatModel, chatModel: chatModel,
width: width, width: width,
height: height, height: height,
@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
) )
: Draggable( : Draggable(
checkKeyboard: true, checkKeyboard: true,
position: SimpleWrapper(position), position: draggablePositions.chatWindow,
width: width, width: width,
height: height, height: height,
chatModel: chatModel, chatModel: chatModel,
@ -176,7 +181,7 @@ class DraggableMobileActions extends StatelessWidget {
required this.scale}); required this.scale});
final double scale; final double scale;
final SimpleWrapper<Offset> position; final DraggableKeyPosition position;
final double width; final double width;
final double height; final double height;
final VoidCallback? onBackPressed; final VoidCallback? onBackPressed;
@ -241,6 +246,92 @@ class DraggableMobileActions extends StatelessWidget {
} }
} }
class DraggableKeyPosition {
final String key;
Offset _pos;
late Debouncer<int> _debouncerStore;
DraggableKeyPosition(this.key)
: _pos = DraggablePositions.kInvalidDraggablePosition;
get pos => _pos;
_loadPosition(String k) {
final value = bind.getLocalFlutterOption(k: k);
if (value.isNotEmpty) {
final parts = value.split(',');
if (parts.length == 2) {
return Offset(double.parse(parts[0]), double.parse(parts[1]));
}
}
return DraggablePositions.kInvalidDraggablePosition;
}
load() {
_pos = _loadPosition(key);
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
onChanged: (v) => _store(), initialValue: 0);
}
update(Offset pos) {
_pos = pos;
_triggerStore();
}
// Adjust position to keep it in the screen
// Only used for desktop and web desktop
tryAdjust(double w, double h, double scale) {
final size = MediaQuery.of(Get.context!).size;
w = w * scale;
h = h * scale;
double x = _pos.dx;
double y = _pos.dy;
if (x + w > size.width) {
x = size.width - w;
}
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
if (y + h > (size.height - tabBarHeight)) {
y = size.height - tabBarHeight - h;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x != _pos.dx || y != _pos.dy) {
update(Offset(x, y));
}
}
isInvalid() {
return _pos == DraggablePositions.kInvalidDraggablePosition;
}
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
_store() {
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
}
}
class DraggablePositions {
static const kChatWindow = 'draggablePositionChat';
static const kMobileActions = 'draggablePositionMobile';
static const kIOSDraggable = 'draggablePositionIOS';
static const kInvalidDraggablePosition = Offset(-999999, -999999);
final chatWindow = DraggableKeyPosition(kChatWindow);
final mobileActions = DraggableKeyPosition(kMobileActions);
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
load() {
chatWindow.load();
mobileActions.load();
iOSDraggable.load();
}
}
DraggablePositions draggablePositions = DraggablePositions();
class Draggable extends StatefulWidget { class Draggable extends StatefulWidget {
Draggable( Draggable(
{Key? key, {Key? key,
@ -255,7 +346,7 @@ class Draggable extends StatefulWidget {
final bool checkKeyboard; final bool checkKeyboard;
final bool checkScreenSize; final bool checkScreenSize;
final SimpleWrapper<Offset> position; final DraggableKeyPosition position;
final double width; final double width;
final double height; final double height;
final ChatModel? chatModel; final ChatModel? chatModel;
@ -277,7 +368,7 @@ class _DraggableState extends State<Draggable> {
_chatModel = widget.chatModel; _chatModel = widget.chatModel;
} }
get position => widget.position.value; get position => widget.position.pos;
void onPanUpdate(DragUpdateDetails d) { void onPanUpdate(DragUpdateDetails d) {
final offset = d.delta; final offset = d.delta;
@ -301,7 +392,7 @@ class _DraggableState extends State<Draggable> {
y = position.dy + offset.dy; y = position.dy + offset.dy;
} }
setState(() { setState(() {
widget.position.value = Offset(x, y); widget.position.update(Offset(x, y));
}); });
_chatModel?.setChatWindowPosition(position); _chatModel?.setChatWindowPosition(position);
} }
@ -320,7 +411,7 @@ class _DraggableState extends State<Draggable> {
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
widget.position.value = Offset(position.dx, _saveHeight); widget.position.update(Offset(position.dx, _saveHeight));
}); });
} }
@ -331,7 +422,7 @@ class _DraggableState extends State<Draggable> {
if (sumHeight + position.dy > contextHeight) { if (sumHeight + position.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
widget.position.value = Offset(position.dx, y); widget.position.update(Offset(position.dx, y));
}); });
} }
} }
@ -362,14 +453,14 @@ class _DraggableState extends State<Draggable> {
class IOSDraggable extends StatefulWidget { class IOSDraggable extends StatefulWidget {
const IOSDraggable( const IOSDraggable(
{Key? key, {Key? key,
this.position = Offset.zero,
this.chatModel, this.chatModel,
required this.position,
required this.width, required this.width,
required this.height, required this.height,
required this.builder}) required this.builder})
: super(key: key); : super(key: key);
final Offset position; final DraggableKeyPosition position;
final ChatModel? chatModel; final ChatModel? chatModel;
final double width; final double width;
final double height; final double height;
@ -380,7 +471,6 @@ class IOSDraggable extends StatefulWidget {
} }
class IOSDraggableState extends State<IOSDraggable> { class IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel; late ChatModel? _chatModel;
late double _width; late double _width;
late double _height; late double _height;
@ -391,25 +481,26 @@ class IOSDraggableState extends State<IOSDraggable> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_position = widget.position;
_chatModel = widget.chatModel; _chatModel = widget.chatModel;
_width = widget.width; _width = widget.width;
_height = widget.height; _height = widget.height;
} }
get position => widget.position;
checkKeyboard() { checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0; final currentVisible = bottomHeight != 0;
// save // save
if (!_keyboardVisible && currentVisible) { if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy; _saveHeight = position.value.dy;
} }
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
_position = Offset(_position.dx, _saveHeight); position.value = Offset(position.value.dx, _saveHeight);
}); });
} }
@ -417,10 +508,10 @@ class IOSDraggableState extends State<IOSDraggable> {
if (_keyboardVisible && currentVisible) { if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height; final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height; final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) { if (sumHeight + position.value.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
_position = Offset(_position.dx, y); position.value = Offset(position.value.dx, y);
}); });
} }
} }
@ -435,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
left: _position.dx, left: position.value.dx,
top: _position.dy, top: position.value.dy,
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
setState(() { setState(() {
_position += details.delta; position.value += details.delta;
}); });
_chatModel?.setChatWindowPosition(_position); _chatModel?.setChatWindowPosition(position.value);
}, },
child: Material( child: Material(
child: Container( child: Container(

@ -142,6 +142,8 @@ const String kOptionDisableFloatingWindow = "disable-floating-window";
const String kOptionKeepScreenOn = "keep-screen-on"; const String kOptionKeepScreenOn = "keep-screen-on";
const String kOptionShowMobileAction = "showMobileActions";
const String kUrlActionClose = "close"; const String kUrlActionClose = "close";
const String kTabLabelHomePage = "Home"; const String kTabLabelHomePage = "Home";

@ -134,6 +134,7 @@ class _RemotePageState extends State<RemotePage>
_ffi.ffiModel.updateEventListener(sessionId, widget.id); _ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
// Session option should be set after models.dart/FFI.start // Session option should be set after models.dart/FFI.start
_showRemoteCursor.value = bind.sessionGetToggleOptionSync( _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor'); sessionId: sessionId, arg: 'show-remote-cursor');
@ -322,13 +323,6 @@ class _RemotePageState extends State<RemotePage>
if (!_ffi.ffiModel.isPeerAndroid) { if (!_ffi.ffiModel.isPeerAndroid) {
return Offstage(); return Offstage();
} else { } else {
if (_ffi.connType == ConnType.defaultConn &&
_ffi.ffiModel.permissions['keyboard'] != false) {
Timer(
Duration(milliseconds: 10),
() => _ffi.dialogManager
.mobileActionsOverlayVisible.value = true);
}
return Obx(() => Offstage( return Obx(() => Offstage(
offstage: _ffi.dialogManager offstage: _ffi.dialogManager
.mobileActionsOverlayVisible.isFalse, .mobileActionsOverlayVisible.isFalse,

@ -579,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget {
return Obx(() => _IconMenuButton( return Obx(() => _IconMenuButton(
assetName: 'assets/actions_mobile.svg', assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions', tooltip: 'Mobile Actions',
onPressed: () => onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
ffi.dialogManager.mobileActionsOverlayVisible.toggle(), !ffi.dialogManager.mobileActionsOverlayVisible.value),
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.blueColor ? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor, : _ToolbarTheme.inactiveColor,

@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/overlay.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart';
@ -156,6 +157,7 @@ void runMobileApp() async {
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit(); if (isAndroid) androidChannelInit();
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath(); if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
draggablePositions.load();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
@ -176,6 +178,7 @@ void runMultiWindow(
late Widget widget; late Widget widget;
switch (appType) { switch (appType) {
case kAppTypeDesktopRemote: case kAppTypeDesktopRemote:
draggablePositions.load();
widget = DesktopRemoteScreen( widget = DesktopRemoteScreen(
params: argument, params: argument,
); );

@ -82,13 +82,14 @@ class _RemotePageState extends State<RemotePage> {
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_blockableOverlayState.applyFfi(gFFI); _blockableOverlayState.applyFfi(gFFI);
gFFI.dialogManager.loadMobileActionsOverlayVisible();
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
// https://github.com/flutter/flutter/issues/64935 // https://github.com/flutter/flutter/issues/64935
super.dispose(); super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(); gFFI.dialogManager.hideMobileActionsOverlay(store: false);
gFFI.inputModel.listenToMouse(false); gFFI.inputModel.listenToMouse(false);
gFFI.imageModel.disposeImage(); gFFI.imageModel.disposeImage();
gFFI.cursorModel.disposeImages(); gFFI.cursorModel.disposeImages();

@ -917,10 +917,12 @@ class FfiModel with ChangeNotifier {
if (parent.target?.connType == ConnType.defaultConn && if (parent.target?.connType == ConnType.defaultConn &&
parent.target != null && parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) { parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer( Timer(Duration(milliseconds: delayMSecs), () {
Duration(milliseconds: delayMSecs), if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) {
() => parent.target!.dialogManager parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!)); .showMobileActionsOverlay(ffi: parent.target!);
}
});
} }
} }
} }
@ -1587,10 +1589,13 @@ class CanvasModel with ChangeNotifier {
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2 // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale; _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
final adjustForKeyboard = parent.target?.cursorModel.adjustForKeyboard() ?? 0.0; final adjustForKeyboard =
parent.target?.cursorModel.adjustForKeyboard() ?? 0.0;
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY // (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2 // _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2
_y = focalPoint.dy + adjustForKeyboard - (focalPoint.dy - _y + adjustForKeyboard) / s * _scale; _y = focalPoint.dy +
adjustForKeyboard -
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale;
notifyListeners(); notifyListeners();
} }