mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-12-14 03:56:27 +00:00
refactor: split desktop & mobile
This commit is contained in:
228
flutter/lib/mobile/widgets/dialog.dart
Normal file
228
flutter/lib/mobile/widgets/dialog.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
void clientClose() {
|
||||
msgBox('', 'Close', 'Are you sure to close the connection?');
|
||||
}
|
||||
|
||||
const SEC1 = Duration(seconds: 1);
|
||||
void showSuccess({Duration duration = SEC1}) {
|
||||
SmartDialog.dismiss();
|
||||
showToast(translate("Successful"), duration: SEC1);
|
||||
}
|
||||
|
||||
void showError({Duration duration = SEC1}) {
|
||||
SmartDialog.dismiss();
|
||||
showToast(translate("Error"), duration: SEC1);
|
||||
}
|
||||
|
||||
void updatePasswordDialog() {
|
||||
final p0 = TextEditingController();
|
||||
final p1 = TextEditingController();
|
||||
var validateLength = false;
|
||||
var validateSame = false;
|
||||
DialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Set your own password')),
|
||||
content: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Password'),
|
||||
),
|
||||
controller: p0,
|
||||
validator: (v) {
|
||||
if (v == null) return null;
|
||||
final val = v.trim().length > 5;
|
||||
if (validateLength != val) {
|
||||
// use delay to make setState success
|
||||
Future.delayed(Duration(microseconds: 1),
|
||||
() => setState(() => validateLength = val));
|
||||
}
|
||||
return val
|
||||
? null
|
||||
: translate('Too short, at least 6 characters.');
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Confirmation'),
|
||||
),
|
||||
controller: p1,
|
||||
validator: (v) {
|
||||
if (v == null) return null;
|
||||
final val = p0.text == v;
|
||||
if (validateSame != val) {
|
||||
Future.delayed(Duration(microseconds: 1),
|
||||
() => setState(() => validateSame = val));
|
||||
}
|
||||
return val
|
||||
? null
|
||||
: translate('The confirmation is not identical.');
|
||||
},
|
||||
),
|
||||
])),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: (validateLength && validateSame)
|
||||
? () async {
|
||||
close();
|
||||
showLoading(translate("Waiting"));
|
||||
if (await FFI.serverModel.updatePassword(p0.text)) {
|
||||
showSuccess();
|
||||
} else {
|
||||
showError();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void enterPasswordDialog(String id) {
|
||||
final controller = TextEditingController();
|
||||
var remember = FFI.getByName('remember', id) == 'true';
|
||||
DialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Password Required')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Remember password'),
|
||||
),
|
||||
value: remember,
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
setState(() => remember = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
backToHome();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
var text = controller.text.trim();
|
||||
if (text == '') return;
|
||||
FFI.login(text, remember);
|
||||
close();
|
||||
showLoading(translate('Logging in...'));
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void wrongPasswordDialog(String id) {
|
||||
DialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate('Wrong Password')),
|
||||
content: Text(translate('Do you want to enter again?')),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
backToHome();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
enterPasswordDialog(id);
|
||||
},
|
||||
child: Text(translate('Retry')),
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
class PasswordWidget extends StatefulWidget {
|
||||
PasswordWidget({Key? key, required this.controller}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
_PasswordWidgetState createState() => _PasswordWidgetState();
|
||||
}
|
||||
|
||||
class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
bool _passwordVisible = false;
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.unfocus();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
focusNode: _focusNode,
|
||||
controller: widget.controller,
|
||||
obscureText: !_passwordVisible,
|
||||
//This will obscure text dynamically
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: Translator.call('Password'),
|
||||
hintText: Translator.call('Enter your password'),
|
||||
// Here is key idea
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
// Based on passwordVisible state choose the icon
|
||||
_passwordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: Theme.of(context).primaryColorDark,
|
||||
),
|
||||
onPressed: () {
|
||||
// Update the state i.e. toogle the state of passwordVisible variable
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
210
flutter/lib/mobile/widgets/gesture_help.dart
Normal file
210
flutter/lib/mobile/widgets/gesture_help.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
import '../../models/model.dart';
|
||||
|
||||
class GestureIcons {
|
||||
static const String _family = 'gestureicons';
|
||||
|
||||
GestureIcons._();
|
||||
|
||||
static const IconData icon_mouse = IconData(0xe65c, fontFamily: _family);
|
||||
static const IconData icon_Tablet_Touch =
|
||||
IconData(0xe9ce, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_drag =
|
||||
IconData(0xe686, fontFamily: _family);
|
||||
static const IconData icon_Mobile_Touch =
|
||||
IconData(0xe9cd, fontFamily: _family);
|
||||
static const IconData icon_gesture_press =
|
||||
IconData(0xe66c, fontFamily: _family);
|
||||
static const IconData icon_gesture_tap =
|
||||
IconData(0xe66f, fontFamily: _family);
|
||||
static const IconData icon_gesture_pinch =
|
||||
IconData(0xe66a, fontFamily: _family);
|
||||
static const IconData icon_gesture_press_hold =
|
||||
IconData(0xe66b, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_drag_up_down_ =
|
||||
IconData(0xe685, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_tap_ =
|
||||
IconData(0xe68e, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_swipe_right =
|
||||
IconData(0xe68f, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_double_tap =
|
||||
IconData(0xe691, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_three_fingers =
|
||||
IconData(0xe687, fontFamily: _family);
|
||||
}
|
||||
|
||||
typedef OnTouchModeChange = void Function(bool);
|
||||
|
||||
class GestureHelp extends StatefulWidget {
|
||||
GestureHelp(
|
||||
{Key? key, required this.touchMode, required this.onTouchModeChange})
|
||||
: super(key: key);
|
||||
final bool touchMode;
|
||||
final OnTouchModeChange onTouchModeChange;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GestureHelpState();
|
||||
}
|
||||
|
||||
class _GestureHelpState extends State<GestureHelp> {
|
||||
var _selectedIndex;
|
||||
var _touchMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_touchMode = widget.touchMode;
|
||||
_selectedIndex = _touchMode ? 1 : 0;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final space = 12.0;
|
||||
var width = size.width - 2 * space;
|
||||
final minWidth = 90;
|
||||
if (size.width > minWidth + 2 * space) {
|
||||
final n = (size.width / (minWidth + 2 * space)).floor();
|
||||
width = size.width / n - 2 * space;
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ToggleSwitch(
|
||||
initialLabelIndex: _selectedIndex,
|
||||
inactiveBgColor: MyTheme.darkGray,
|
||||
totalSwitches: 2,
|
||||
minWidth: 150,
|
||||
fontSize: 15,
|
||||
iconSize: 18,
|
||||
labels: [translate("Mouse mode"), translate("Touch mode")],
|
||||
icons: [Icons.mouse, Icons.touch_app],
|
||||
onToggle: (index) {
|
||||
setState(() {
|
||||
if (_selectedIndex != index) {
|
||||
_selectedIndex = index ?? 0;
|
||||
_touchMode = index == 0 ? false : true;
|
||||
widget.onTouchModeChange(_touchMode);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
child: Wrap(
|
||||
spacing: space,
|
||||
runSpacing: 2 * space,
|
||||
children: _touchMode
|
||||
? [
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_Mobile_Touch,
|
||||
translate("One-Finger Tap"),
|
||||
translate("Left Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_press_hold,
|
||||
translate("One-Long Tap"),
|
||||
translate("Right Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_swipe_right,
|
||||
translate("One-Finger Move"),
|
||||
translate("Mouse Drag")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_three_fingers,
|
||||
translate("Three-Finger vertically"),
|
||||
translate("Mouse Wheel")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_drag,
|
||||
translate("Two-Finger Move"),
|
||||
translate("Canvas Move")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_pinch,
|
||||
translate("Pinch to Zoom"),
|
||||
translate("Canvas Zoom")),
|
||||
]
|
||||
: [
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_Mobile_Touch,
|
||||
translate("One-Finger Tap"),
|
||||
translate("Left Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_press_hold,
|
||||
translate("One-Long Tap"),
|
||||
translate("Right Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_swipe_right,
|
||||
translate("Double Tap & Move"),
|
||||
translate("Mouse Drag")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_three_fingers,
|
||||
translate("Three-Finger vertically"),
|
||||
translate("Mouse Wheel")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_drag,
|
||||
translate("Two-Finger Move"),
|
||||
translate("Canvas Move")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_pinch,
|
||||
translate("Pinch to Zoom"),
|
||||
translate("Canvas Zoom")),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class GestureInfo extends StatelessWidget {
|
||||
const GestureInfo(this.width, this.icon, this.fromText, this.toText,
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
|
||||
final String fromText;
|
||||
final String toText;
|
||||
final IconData icon;
|
||||
final double width;
|
||||
|
||||
final iconSize = 35.0;
|
||||
final iconColor = MyTheme.accent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: this.width,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: iconColor,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(fromText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 9, color: Colors.grey)),
|
||||
SizedBox(height: 3),
|
||||
Text(toText,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, color: Colors.black))
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
730
flutter/lib/mobile/widgets/gestures.dart
Normal file
730
flutter/lib/mobile/widgets/gestures.dart
Normal file
@@ -0,0 +1,730 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum CustomTouchGestureState {
|
||||
none,
|
||||
oneFingerPan,
|
||||
twoFingerScale,
|
||||
threeFingerVerticalDrag
|
||||
}
|
||||
|
||||
class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
CustomTouchGestureRecognizer({
|
||||
Object? debugOwner,
|
||||
Set<PointerDeviceKind>? supportedDevices,
|
||||
}) : super(
|
||||
debugOwner: debugOwner,
|
||||
supportedDevices: supportedDevices,
|
||||
) {
|
||||
_init();
|
||||
}
|
||||
|
||||
// oneFingerPan
|
||||
GestureDragStartCallback? onOneFingerPanStart;
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate;
|
||||
GestureDragEndCallback? onOneFingerPanEnd;
|
||||
|
||||
// twoFingerScale : scale + pan event
|
||||
GestureScaleStartCallback? onTwoFingerScaleStart;
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate;
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd;
|
||||
|
||||
// threeFingerVerticalDrag
|
||||
GestureDragStartCallback? onThreeFingerVerticalDragStart;
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
|
||||
GestureDragEndCallback? onThreeFingerVerticalDragEnd;
|
||||
|
||||
var _currentState = CustomTouchGestureState.none;
|
||||
Timer? _startEventDebounceTimer;
|
||||
|
||||
void _init() {
|
||||
debugPrint("CustomTouchGestureRecognizer init");
|
||||
onStart = (d) {
|
||||
_startEventDebounceTimer?.cancel();
|
||||
if (d.pointerCount == 1) {
|
||||
_currentState = CustomTouchGestureState.oneFingerPan;
|
||||
if (onOneFingerPanStart != null) {
|
||||
onOneFingerPanStart!(DragStartDetails(
|
||||
localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
|
||||
}
|
||||
debugPrint("start oneFingerPan");
|
||||
} else if (d.pointerCount == 2) {
|
||||
if (_currentState == CustomTouchGestureState.threeFingerVerticalDrag) {
|
||||
// 3 -> 2 debounce
|
||||
_startEventDebounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
_currentState = CustomTouchGestureState.twoFingerScale;
|
||||
if (onTwoFingerScaleStart != null) {
|
||||
onTwoFingerScaleStart!(ScaleStartDetails(
|
||||
localFocalPoint: d.localFocalPoint,
|
||||
focalPoint: d.focalPoint));
|
||||
}
|
||||
debugPrint("debounce start twoFingerScale success");
|
||||
});
|
||||
}
|
||||
_currentState = CustomTouchGestureState.twoFingerScale;
|
||||
// startWatchTimer();
|
||||
if (onTwoFingerScaleStart != null) {
|
||||
onTwoFingerScaleStart!(ScaleStartDetails(
|
||||
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
|
||||
}
|
||||
debugPrint("start twoFingerScale");
|
||||
} else if (d.pointerCount == 3) {
|
||||
_currentState = CustomTouchGestureState.threeFingerVerticalDrag;
|
||||
if (onThreeFingerVerticalDragStart != null) {
|
||||
onThreeFingerVerticalDragStart!(
|
||||
DragStartDetails(globalPosition: d.localFocalPoint));
|
||||
}
|
||||
debugPrint("start threeFingerScale");
|
||||
// _reset();
|
||||
}
|
||||
};
|
||||
onUpdate = (d) {
|
||||
if (_currentState != CustomTouchGestureState.none) {
|
||||
switch (_currentState) {
|
||||
case CustomTouchGestureState.oneFingerPan:
|
||||
if (onOneFingerPanUpdate != null) {
|
||||
onOneFingerPanUpdate!(_getDragUpdateDetails(d));
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.twoFingerScale:
|
||||
if (onTwoFingerScaleUpdate != null) {
|
||||
onTwoFingerScaleUpdate!(d);
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.threeFingerVerticalDrag:
|
||||
if (onThreeFingerVerticalDragUpdate != null) {
|
||||
onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
onEnd = (d) {
|
||||
debugPrint("ScaleGestureRecognizer onEnd");
|
||||
// end
|
||||
switch (_currentState) {
|
||||
case CustomTouchGestureState.oneFingerPan:
|
||||
debugPrint("TwoFingerState.pan onEnd");
|
||||
if (onOneFingerPanEnd != null) {
|
||||
onOneFingerPanEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.twoFingerScale:
|
||||
debugPrint("TwoFingerState.scale onEnd");
|
||||
if (onTwoFingerScaleEnd != null) {
|
||||
onTwoFingerScaleEnd!(d);
|
||||
}
|
||||
break;
|
||||
case CustomTouchGestureState.threeFingerVerticalDrag:
|
||||
debugPrint("ThreeFingerState.vertical onEnd");
|
||||
if (onThreeFingerVerticalDragEnd != null) {
|
||||
onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_currentState = CustomTouchGestureState.none;
|
||||
};
|
||||
}
|
||||
|
||||
DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) =>
|
||||
DragUpdateDetails(
|
||||
globalPosition: d.focalPoint,
|
||||
localPosition: d.localFocalPoint,
|
||||
delta: d.focalPointDelta);
|
||||
|
||||
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
|
||||
DragEndDetails(velocity: d.velocity);
|
||||
}
|
||||
|
||||
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
|
||||
HoldTapMoveGestureRecognizer({
|
||||
Object? debugOwner,
|
||||
Set<PointerDeviceKind>? supportedDevices,
|
||||
}) : super(
|
||||
debugOwner: debugOwner,
|
||||
supportedDevices: supportedDevices,
|
||||
);
|
||||
|
||||
GestureDragStartCallback? onHoldDragStart;
|
||||
GestureDragUpdateCallback? onHoldDragUpdate;
|
||||
GestureDragDownCallback? onHoldDragDown;
|
||||
GestureDragCancelCallback? onHoldDragCancel;
|
||||
GestureDragEndCallback? onHoldDragEnd;
|
||||
|
||||
bool _isStart = false;
|
||||
|
||||
Timer? _firstTapUpTimer;
|
||||
Timer? _secondTapDownTimer;
|
||||
_TapTracker? _firstTap;
|
||||
_TapTracker? _secondTap;
|
||||
|
||||
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
|
||||
|
||||
@override
|
||||
bool isPointerAllowed(PointerDownEvent event) {
|
||||
if (_firstTap == null) {
|
||||
switch (event.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onHoldDragStart == null &&
|
||||
onHoldDragUpdate == null &&
|
||||
onHoldDragCancel == null &&
|
||||
onHoldDragEnd == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.isPointerAllowed(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
if (_firstTap != null) {
|
||||
if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
|
||||
// Ignore out-of-bounds second taps.
|
||||
return;
|
||||
} else if (!_firstTap!.hasElapsedMinTime() ||
|
||||
!_firstTap!.hasSameButton(event)) {
|
||||
// Restart when the second tap is too close to the first (touch screens
|
||||
// often detect touches intermittently), or when buttons mismatch.
|
||||
_reset();
|
||||
return _trackTap(event);
|
||||
} else if (onHoldDragDown != null) {
|
||||
invokeCallback<void>(
|
||||
'onHoldDragDown',
|
||||
() => onHoldDragDown!(DragDownDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition)));
|
||||
}
|
||||
}
|
||||
_trackTap(event);
|
||||
}
|
||||
|
||||
void _trackTap(PointerDownEvent event) {
|
||||
_stopFirstTapUpTimer();
|
||||
_stopSecondTapDownTimer();
|
||||
final _TapTracker tracker = _TapTracker(
|
||||
event: event,
|
||||
entry: GestureBinding.instance!.gestureArena.add(event.pointer, this),
|
||||
doubleTapMinTime: kDoubleTapMinTime,
|
||||
gestureSettings: gestureSettings,
|
||||
);
|
||||
_trackers[event.pointer] = tracker;
|
||||
tracker.startTrackingPointer(_handleEvent, event.transform);
|
||||
}
|
||||
|
||||
void _handleEvent(PointerEvent event) {
|
||||
final _TapTracker tracker = _trackers[event.pointer]!;
|
||||
if (event is PointerUpEvent) {
|
||||
if (_firstTap == null && _secondTap == null) {
|
||||
_registerFirstTap(tracker);
|
||||
} else if (_secondTap != null) {
|
||||
if (event.pointer == _secondTap!.pointer) {
|
||||
if (onHoldDragEnd != null) onHoldDragEnd!(DragEndDetails());
|
||||
}
|
||||
} else {
|
||||
_reject(tracker);
|
||||
}
|
||||
} else if (event is PointerDownEvent) {
|
||||
if (_firstTap != null && _secondTap == null) {
|
||||
_registerSecondTap(tracker);
|
||||
}
|
||||
} else if (event is PointerMoveEvent) {
|
||||
if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
|
||||
if (_firstTap != null && _firstTap!.pointer == event.pointer) {
|
||||
// first tap move
|
||||
_reject(tracker);
|
||||
} else if (_secondTap != null && _secondTap!.pointer == event.pointer) {
|
||||
// debugPrint("_secondTap move");
|
||||
// second tap move
|
||||
if (!_isStart) {
|
||||
_resolve();
|
||||
}
|
||||
if (onHoldDragUpdate != null)
|
||||
onHoldDragUpdate!(DragUpdateDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
delta: event.delta));
|
||||
}
|
||||
}
|
||||
} else if (event is PointerCancelEvent) {
|
||||
_reject(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
_TapTracker? tracker = _trackers[pointer];
|
||||
// If tracker isn't in the list, check if this is the first tap tracker
|
||||
if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) {
|
||||
tracker = _firstTap;
|
||||
}
|
||||
// If tracker is still null, we rejected ourselves already
|
||||
if (tracker != null) {
|
||||
_reject(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
void _resolve() {
|
||||
_stopSecondTapDownTimer();
|
||||
_firstTap?.entry.resolve(GestureDisposition.accepted);
|
||||
_secondTap?.entry.resolve(GestureDisposition.accepted);
|
||||
_isStart = true;
|
||||
// TODO start details
|
||||
if (onHoldDragStart != null) onHoldDragStart!(DragStartDetails());
|
||||
}
|
||||
|
||||
void _reject(_TapTracker tracker) {
|
||||
try {
|
||||
_checkCancel();
|
||||
_isStart = false;
|
||||
_trackers.remove(tracker.pointer);
|
||||
tracker.entry.resolve(GestureDisposition.rejected);
|
||||
_freezeTracker(tracker);
|
||||
_reset();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to _reject:$e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reset();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_isStart = false;
|
||||
// debugPrint("reset");
|
||||
_stopFirstTapUpTimer();
|
||||
_stopSecondTapDownTimer();
|
||||
if (_firstTap != null) {
|
||||
if (_trackers.isNotEmpty) {
|
||||
_checkCancel();
|
||||
}
|
||||
// Note, order is important below in order for the resolve -> reject logic
|
||||
// to work properly.
|
||||
final _TapTracker tracker = _firstTap!;
|
||||
_firstTap = null;
|
||||
_reject(tracker);
|
||||
GestureBinding.instance!.gestureArena.release(tracker.pointer);
|
||||
|
||||
if (_secondTap != null) {
|
||||
final _TapTracker tracker = _secondTap!;
|
||||
_secondTap = null;
|
||||
_reject(tracker);
|
||||
GestureBinding.instance!.gestureArena.release(tracker.pointer);
|
||||
}
|
||||
}
|
||||
_firstTap = null;
|
||||
_secondTap = null;
|
||||
_clearTrackers();
|
||||
}
|
||||
|
||||
void _registerFirstTap(_TapTracker tracker) {
|
||||
_startFirstTapUpTimer();
|
||||
GestureBinding.instance!.gestureArena.hold(tracker.pointer);
|
||||
// Note, order is important below in order for the clear -> reject logic to
|
||||
// work properly.
|
||||
_freezeTracker(tracker);
|
||||
_trackers.remove(tracker.pointer);
|
||||
_firstTap = tracker;
|
||||
}
|
||||
|
||||
void _registerSecondTap(_TapTracker tracker) {
|
||||
if (_firstTap != null) {
|
||||
_stopFirstTapUpTimer();
|
||||
_freezeTracker(_firstTap!);
|
||||
_firstTap = null;
|
||||
}
|
||||
|
||||
_startSecondTapDownTimer();
|
||||
GestureBinding.instance!.gestureArena.hold(tracker.pointer);
|
||||
|
||||
_secondTap = tracker;
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
void _clearTrackers() {
|
||||
_trackers.values.toList().forEach(_reject);
|
||||
assert(_trackers.isEmpty);
|
||||
}
|
||||
|
||||
void _freezeTracker(_TapTracker tracker) {
|
||||
tracker.stopTrackingPointer(_handleEvent);
|
||||
}
|
||||
|
||||
void _startFirstTapUpTimer() {
|
||||
_firstTapUpTimer ??= Timer(kDoubleTapTimeout, _reset);
|
||||
}
|
||||
|
||||
void _startSecondTapDownTimer() {
|
||||
_secondTapDownTimer ??= Timer(kDoubleTapTimeout, _resolve);
|
||||
}
|
||||
|
||||
void _stopFirstTapUpTimer() {
|
||||
if (_firstTapUpTimer != null) {
|
||||
_firstTapUpTimer!.cancel();
|
||||
_firstTapUpTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _stopSecondTapDownTimer() {
|
||||
if (_secondTapDownTimer != null) {
|
||||
_secondTapDownTimer!.cancel();
|
||||
_secondTapDownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _checkCancel() {
|
||||
if (onHoldDragCancel != null) {
|
||||
invokeCallback<void>('onHoldDragCancel', onHoldDragCancel!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'double tap';
|
||||
}
|
||||
|
||||
class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
|
||||
DoubleFinerTapGestureRecognizer({
|
||||
Object? debugOwner,
|
||||
Set<PointerDeviceKind>? supportedDevices,
|
||||
}) : super(
|
||||
debugOwner: debugOwner,
|
||||
supportedDevices: supportedDevices,
|
||||
);
|
||||
|
||||
GestureTapDownCallback? onDoubleFinerTapDown;
|
||||
GestureTapDownCallback? onDoubleFinerTap;
|
||||
GestureTapCancelCallback? onDoubleFinerTapCancel;
|
||||
|
||||
Timer? _firstTapTimer;
|
||||
_TapTracker? _firstTap;
|
||||
|
||||
var _isStart = false;
|
||||
|
||||
final Set<int> _upTap = {};
|
||||
|
||||
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
|
||||
|
||||
@override
|
||||
bool isPointerAllowed(PointerDownEvent event) {
|
||||
if (_firstTap == null) {
|
||||
switch (event.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onDoubleFinerTapDown == null &&
|
||||
onDoubleFinerTap == null &&
|
||||
onDoubleFinerTapCancel == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.isPointerAllowed(event);
|
||||
}
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
debugPrint("addAllowedPointer");
|
||||
if (_isStart) {
|
||||
// second
|
||||
if (onDoubleFinerTapDown != null) {
|
||||
final TapDownDetails details = TapDownDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
kind: getKindForPointer(event.pointer),
|
||||
);
|
||||
invokeCallback<void>(
|
||||
'onDoubleFinerTapDown', () => onDoubleFinerTapDown!(details));
|
||||
}
|
||||
} else {
|
||||
// first tap
|
||||
_isStart = true;
|
||||
_startFirstTapDownTimer();
|
||||
}
|
||||
_trackTap(event);
|
||||
}
|
||||
|
||||
void _trackTap(PointerDownEvent event) {
|
||||
final _TapTracker tracker = _TapTracker(
|
||||
event: event,
|
||||
entry: GestureBinding.instance!.gestureArena.add(event.pointer, this),
|
||||
doubleTapMinTime: kDoubleTapMinTime,
|
||||
gestureSettings: gestureSettings,
|
||||
);
|
||||
_trackers[event.pointer] = tracker;
|
||||
// debugPrint("_trackers:$_trackers");
|
||||
tracker.startTrackingPointer(_handleEvent, event.transform);
|
||||
|
||||
_registerTap(tracker);
|
||||
}
|
||||
|
||||
void _handleEvent(PointerEvent event) {
|
||||
final _TapTracker tracker = _trackers[event.pointer]!;
|
||||
if (event is PointerUpEvent) {
|
||||
debugPrint("PointerUpEvent");
|
||||
_upTap.add(tracker.pointer);
|
||||
} else if (event is PointerMoveEvent) {
|
||||
if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
|
||||
_reject(tracker);
|
||||
} else if (event is PointerCancelEvent) {
|
||||
_reject(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
_TapTracker? tracker = _trackers[pointer];
|
||||
// If tracker isn't in the list, check if this is the first tap tracker
|
||||
if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) {
|
||||
tracker = _firstTap;
|
||||
}
|
||||
// If tracker is still null, we rejected ourselves already
|
||||
if (tracker != null) {
|
||||
_reject(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
void _reject(_TapTracker tracker) {
|
||||
_trackers.remove(tracker.pointer);
|
||||
tracker.entry.resolve(GestureDisposition.rejected);
|
||||
_freezeTracker(tracker);
|
||||
if (_firstTap != null) {
|
||||
if (tracker == _firstTap) {
|
||||
_reset();
|
||||
} else {
|
||||
_checkCancel();
|
||||
if (_trackers.isEmpty) {
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reset();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_stopFirstTapUpTimer();
|
||||
_firstTap = null;
|
||||
_clearTrackers();
|
||||
}
|
||||
|
||||
void _registerTap(_TapTracker tracker) {
|
||||
GestureBinding.instance!.gestureArena.hold(tracker.pointer);
|
||||
// Note, order is important below in order for the clear -> reject logic to
|
||||
// work properly.
|
||||
}
|
||||
|
||||
void _clearTrackers() {
|
||||
_trackers.values.toList().forEach(_reject);
|
||||
assert(_trackers.isEmpty);
|
||||
}
|
||||
|
||||
void _freezeTracker(_TapTracker tracker) {
|
||||
tracker.stopTrackingPointer(_handleEvent);
|
||||
}
|
||||
|
||||
void _startFirstTapDownTimer() {
|
||||
_firstTapTimer ??= Timer(kDoubleTapTimeout, _timeoutCheck);
|
||||
}
|
||||
|
||||
void _stopFirstTapUpTimer() {
|
||||
if (_firstTapTimer != null) {
|
||||
_firstTapTimer!.cancel();
|
||||
_firstTapTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _timeoutCheck() {
|
||||
_isStart = false;
|
||||
if (_upTap.length == 2) {
|
||||
_resolve();
|
||||
} else {
|
||||
_reset();
|
||||
}
|
||||
_upTap.clear();
|
||||
}
|
||||
|
||||
void _resolve() {
|
||||
// TODO tap down details
|
||||
if (onDoubleFinerTap != null) onDoubleFinerTap!(TapDownDetails());
|
||||
_trackers.forEach((key, value) {
|
||||
value.entry.resolve(GestureDisposition.accepted);
|
||||
});
|
||||
_reset();
|
||||
}
|
||||
|
||||
void _checkCancel() {
|
||||
if (onDoubleFinerTapCancel != null) {
|
||||
invokeCallback<void>('onHoldDragCancel', onDoubleFinerTapCancel!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'double tap';
|
||||
}
|
||||
|
||||
/// TapTracker helps track individual tap sequences as part of a
|
||||
/// larger gesture.
|
||||
class _TapTracker {
|
||||
_TapTracker({
|
||||
required PointerDownEvent event,
|
||||
required this.entry,
|
||||
required Duration doubleTapMinTime,
|
||||
required this.gestureSettings,
|
||||
}) : assert(doubleTapMinTime != null),
|
||||
assert(event != null),
|
||||
assert(event.buttons != null),
|
||||
pointer = event.pointer,
|
||||
_initialGlobalPosition = event.position,
|
||||
initialButtons = event.buttons,
|
||||
_doubleTapMinTimeCountdown =
|
||||
_CountdownZoned(duration: doubleTapMinTime);
|
||||
|
||||
final DeviceGestureSettings? gestureSettings;
|
||||
final int pointer;
|
||||
final GestureArenaEntry entry;
|
||||
final Offset _initialGlobalPosition;
|
||||
final int initialButtons;
|
||||
final _CountdownZoned _doubleTapMinTimeCountdown;
|
||||
|
||||
bool _isTrackingPointer = false;
|
||||
|
||||
void startTrackingPointer(PointerRoute route, Matrix4? transform) {
|
||||
if (!_isTrackingPointer) {
|
||||
_isTrackingPointer = true;
|
||||
GestureBinding.instance!.pointerRouter
|
||||
.addRoute(pointer, route, transform);
|
||||
}
|
||||
}
|
||||
|
||||
void stopTrackingPointer(PointerRoute route) {
|
||||
if (_isTrackingPointer) {
|
||||
_isTrackingPointer = false;
|
||||
GestureBinding.instance!.pointerRouter.removeRoute(pointer, route);
|
||||
}
|
||||
}
|
||||
|
||||
bool isWithinGlobalTolerance(PointerEvent event, double tolerance) {
|
||||
final Offset offset = event.position - _initialGlobalPosition;
|
||||
return offset.distance <= tolerance;
|
||||
}
|
||||
|
||||
bool hasElapsedMinTime() {
|
||||
return _doubleTapMinTimeCountdown.timeout;
|
||||
}
|
||||
|
||||
bool hasSameButton(PointerDownEvent event) {
|
||||
return event.buttons == initialButtons;
|
||||
}
|
||||
}
|
||||
|
||||
/// CountdownZoned tracks whether the specified duration has elapsed since
|
||||
/// creation, honoring [Zone].
|
||||
class _CountdownZoned {
|
||||
_CountdownZoned({required Duration duration}) : assert(duration != null) {
|
||||
Timer(duration, _onTimeout);
|
||||
}
|
||||
|
||||
bool _timeout = false;
|
||||
|
||||
bool get timeout => _timeout;
|
||||
|
||||
void _onTimeout() {
|
||||
_timeout = true;
|
||||
}
|
||||
}
|
||||
|
||||
RawGestureDetector getMixinGestureDetector({
|
||||
Widget? child,
|
||||
GestureTapUpCallback? onTapUp,
|
||||
GestureTapDownCallback? onDoubleTapDown,
|
||||
GestureDoubleTapCallback? onDoubleTap,
|
||||
GestureLongPressDownCallback? onLongPressDown,
|
||||
GestureLongPressCallback? onLongPress,
|
||||
GestureDragStartCallback? onHoldDragStart,
|
||||
GestureDragUpdateCallback? onHoldDragUpdate,
|
||||
GestureDragCancelCallback? onHoldDragCancel,
|
||||
GestureDragEndCallback? onHoldDragEnd,
|
||||
GestureTapDownCallback? onDoubleFinerTap,
|
||||
GestureDragStartCallback? onOneFingerPanStart,
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate,
|
||||
GestureDragEndCallback? onOneFingerPanEnd,
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd,
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
|
||||
}) {
|
||||
return RawGestureDetector(
|
||||
child: child,
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
// Official
|
||||
TapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(), (instance) {
|
||||
instance.onTapUp = onTapUp;
|
||||
}),
|
||||
DoubleTapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||
() => DoubleTapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onDoubleTapDown = onDoubleTapDown
|
||||
..onDoubleTap = onDoubleTap;
|
||||
}),
|
||||
LongPressGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onLongPressDown = onLongPressDown
|
||||
..onLongPress = onLongPress;
|
||||
}),
|
||||
// Customized
|
||||
HoldTapMoveGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||
() => HoldTapMoveGestureRecognizer(),
|
||||
(instance) => {
|
||||
instance
|
||||
..onHoldDragStart = onHoldDragStart
|
||||
..onHoldDragUpdate = onHoldDragUpdate
|
||||
..onHoldDragCancel = onHoldDragCancel
|
||||
..onHoldDragEnd = onHoldDragEnd
|
||||
}),
|
||||
DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
|
||||
DoubleFinerTapGestureRecognizer>(
|
||||
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
||||
instance.onDoubleFinerTap = onDoubleFinerTap;
|
||||
}),
|
||||
CustomTouchGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||
() => CustomTouchGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onOneFingerPanStart = onOneFingerPanStart
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
||||
}),
|
||||
});
|
||||
}
|
||||
380
flutter/lib/mobile/widgets/overlay.dart
Normal file
380
flutter/lib/mobile/widgets/overlay.dart
Normal file
@@ -0,0 +1,380 @@
|
||||
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
|
||||
import '../../models/model.dart';
|
||||
import '../pages/chat_page.dart';
|
||||
|
||||
OverlayEntry? chatIconOverlayEntry;
|
||||
OverlayEntry? chatWindowOverlayEntry;
|
||||
|
||||
OverlayEntry? mobileActionsOverlayEntry;
|
||||
|
||||
class DraggableChatWindow extends StatelessWidget {
|
||||
DraggableChatWindow(
|
||||
{this.position = Offset.zero, required this.width, required this.height});
|
||||
|
||||
final Offset position;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (_, onPanUpdate) {
|
||||
return isIOS
|
||||
? chatPage
|
||||
: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
appBar: Container(
|
||||
color: MyTheme.accent50,
|
||||
height: 50,
|
||||
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),
|
||||
)),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
hideChatWindowOverlay();
|
||||
},
|
||||
icon: Icon(Icons.keyboard_arrow_down)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
hideChatWindowOverlay();
|
||||
hideChatIconOverlay();
|
||||
},
|
||||
icon: Icon(Icons.close))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: chatPage,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final GestureDragUpdateCallback onPanUpdate;
|
||||
final Widget 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);
|
||||
}
|
||||
|
||||
showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
|
||||
if (chatIconOverlayEntry != null) {
|
||||
chatIconOverlayEntry!.remove();
|
||||
}
|
||||
if (globalKey.currentState == null || globalKey.currentState!.overlay == null)
|
||||
return;
|
||||
final bar = navigationBarKey.currentWidget;
|
||||
if (bar != null) {
|
||||
if ((bar as BottomNavigationBar).currentIndex == 1) {
|
||||
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 (chatWindowOverlayEntry == null) {
|
||||
showChatWindowOverlay();
|
||||
} else {
|
||||
hideChatWindowOverlay();
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.message)));
|
||||
});
|
||||
globalOverlayState.insert(overlay);
|
||||
chatIconOverlayEntry = overlay;
|
||||
}
|
||||
|
||||
hideChatIconOverlay() {
|
||||
if (chatIconOverlayEntry != null) {
|
||||
chatIconOverlayEntry!.remove();
|
||||
chatIconOverlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
showChatWindowOverlay() {
|
||||
if (chatWindowOverlayEntry != null) return;
|
||||
if (globalKey.currentState == null || globalKey.currentState!.overlay == null)
|
||||
return;
|
||||
final globalOverlayState = globalKey.currentState!.overlay!;
|
||||
|
||||
final overlay = OverlayEntry(builder: (context) {
|
||||
return DraggableChatWindow(
|
||||
position: Offset(20, 80), width: 250, height: 350);
|
||||
});
|
||||
globalOverlayState.insert(overlay);
|
||||
chatWindowOverlayEntry = overlay;
|
||||
}
|
||||
|
||||
hideChatWindowOverlay() {
|
||||
if (chatWindowOverlayEntry != null) {
|
||||
chatWindowOverlayEntry!.remove();
|
||||
chatWindowOverlayEntry = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toggleChatOverlay() {
|
||||
if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) {
|
||||
FFI.invokeMethod("enable_soft_keyboard", true);
|
||||
showChatIconOverlay();
|
||||
showChatWindowOverlay();
|
||||
} else {
|
||||
hideChatIconOverlay();
|
||||
hideChatWindowOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/// floating buttons of back/home/recent actions for android
|
||||
class DraggableMobileActions extends StatelessWidget {
|
||||
DraggableMobileActions(
|
||||
{this.position = Offset.zero,
|
||||
this.onBackPressed,
|
||||
this.onRecentPressed,
|
||||
this.onHomePressed,
|
||||
required this.width,
|
||||
required this.height});
|
||||
|
||||
final Offset position;
|
||||
final double width;
|
||||
final double height;
|
||||
final VoidCallback? onBackPressed;
|
||||
final VoidCallback? onHomePressed;
|
||||
final VoidCallback? onRecentPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable(
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (_, onPanUpdate) {
|
||||
return GestureDetector(
|
||||
onPanUpdate: onPanUpdate,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.accent.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.all(Radius.circular(15))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
color: MyTheme.white,
|
||||
onPressed: onBackPressed,
|
||||
icon: Icon(Icons.arrow_back)),
|
||||
IconButton(
|
||||
color: MyTheme.white,
|
||||
onPressed: onHomePressed,
|
||||
icon: Icon(Icons.home)),
|
||||
IconButton(
|
||||
color: MyTheme.white,
|
||||
onPressed: onRecentPressed,
|
||||
icon: Icon(Icons.more_horiz)),
|
||||
VerticalDivider(
|
||||
width: 0,
|
||||
thickness: 2,
|
||||
indent: 10,
|
||||
endIndent: 10,
|
||||
),
|
||||
IconButton(
|
||||
color: MyTheme.white,
|
||||
onPressed: hideMobileActionsOverlay,
|
||||
icon: Icon(Icons.keyboard_arrow_down)),
|
||||
],
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showMobileActionsOverlay() {
|
||||
if (mobileActionsOverlayEntry != null) return;
|
||||
if (globalKey.currentContext == null ||
|
||||
globalKey.currentState == null ||
|
||||
globalKey.currentState!.overlay == null) return;
|
||||
final globalOverlayState = globalKey.currentState!.overlay!;
|
||||
|
||||
// compute overlay position
|
||||
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
||||
final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
|
||||
final double overlayW = 200;
|
||||
final double overlayH = 45;
|
||||
final left = (screenW - overlayW) / 2;
|
||||
final top = screenH - overlayH - 80;
|
||||
|
||||
final overlay = OverlayEntry(builder: (context) {
|
||||
return DraggableMobileActions(
|
||||
position: Offset(left, top),
|
||||
width: overlayW,
|
||||
height: overlayH,
|
||||
onBackPressed: () => FFI.tap(MouseButtons.right),
|
||||
onHomePressed: () => FFI.tap(MouseButtons.wheel),
|
||||
onRecentPressed: () async {
|
||||
FFI.sendMouse('down', MouseButtons.wheel);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
FFI.sendMouse('up', MouseButtons.wheel);
|
||||
},
|
||||
);
|
||||
});
|
||||
globalOverlayState.insert(overlay);
|
||||
mobileActionsOverlayEntry = overlay;
|
||||
}
|
||||
|
||||
hideMobileActionsOverlay() {
|
||||
if (mobileActionsOverlayEntry != null) {
|
||||
mobileActionsOverlayEntry!.remove();
|
||||
mobileActionsOverlayEntry = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class Draggable extends StatefulWidget {
|
||||
Draggable(
|
||||
{this.checkKeyboard = false,
|
||||
this.checkScreenSize = false,
|
||||
this.position = Offset.zero,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder});
|
||||
|
||||
final bool checkKeyboard;
|
||||
final bool checkScreenSize;
|
||||
final Offset position;
|
||||
final double width;
|
||||
final double height;
|
||||
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DraggableState();
|
||||
}
|
||||
|
||||
class _DraggableState extends State<Draggable> {
|
||||
late Offset _position;
|
||||
bool _keyboardVisible = false;
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_position = widget.position;
|
||||
}
|
||||
|
||||
void onPanUpdate(DragUpdateDetails d) {
|
||||
final offset = d.delta;
|
||||
final size = MediaQuery.of(context).size;
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
|
||||
if (_position.dx + offset.dx + widget.width > size.width) {
|
||||
x = size.width - widget.width;
|
||||
} else if (_position.dx + offset.dx < 0) {
|
||||
x = 0;
|
||||
} else {
|
||||
x = _position.dx + offset.dx;
|
||||
}
|
||||
|
||||
if (_position.dy + offset.dy + widget.height > size.height) {
|
||||
y = size.height - widget.height;
|
||||
} else if (_position.dy + offset.dy < 0) {
|
||||
y = 0;
|
||||
} else {
|
||||
y = _position.dy + offset.dy;
|
||||
}
|
||||
setState(() {
|
||||
_position = Offset(x, y);
|
||||
});
|
||||
}
|
||||
|
||||
checkScreenSize() {}
|
||||
|
||||
checkKeyboard() {
|
||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
final currentVisible = bottomHeight != 0;
|
||||
|
||||
debugPrint(bottomHeight.toString() + currentVisible.toString());
|
||||
// save
|
||||
if (!_keyboardVisible && currentVisible) {
|
||||
_saveHeight = _position.dy;
|
||||
}
|
||||
|
||||
// reset
|
||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, _saveHeight);
|
||||
});
|
||||
}
|
||||
|
||||
// onKeyboardVisible
|
||||
if (_keyboardVisible && currentVisible) {
|
||||
final sumHeight = bottomHeight + widget.height;
|
||||
final contextHeight = MediaQuery.of(context).size.height;
|
||||
if (sumHeight + _position.dy > contextHeight) {
|
||||
final y = contextHeight - sumHeight;
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_keyboardVisible = currentVisible;
|
||||
_lastBottomHeight = bottomHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.checkKeyboard) {
|
||||
checkKeyboard();
|
||||
}
|
||||
if (widget.checkKeyboard) {
|
||||
checkScreenSize();
|
||||
}
|
||||
return Positioned(
|
||||
top: _position.dy,
|
||||
left: _position.dx,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: widget.builder(context, onPanUpdate));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user