123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722 |
- import 'dart:async';
- import 'dart:ui' as ui;
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_hbb/common/shared_state.dart';
- import 'package:flutter_hbb/common/widgets/toolbar.dart';
- import 'package:flutter_hbb/consts.dart';
- import 'package:flutter_hbb/models/chat_model.dart';
- import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
- import 'package:flutter_svg/svg.dart';
- import 'package:get/get.dart';
- import 'package:provider/provider.dart';
- import 'package:wakelock_plus/wakelock_plus.dart';
- import '../../common.dart';
- import '../../common/widgets/overlay.dart';
- import '../../common/widgets/dialog.dart';
- import '../../common/widgets/remote_input.dart';
- import '../../models/input_model.dart';
- import '../../models/model.dart';
- import '../../models/platform_model.dart';
- import '../../utils/image.dart';
- final initText = '1' * 1024;
- // Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
- // When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
- // https://github.com/flutter/flutter/issues/159384
- // https://github.com/flutter/flutter/issues/159383
- void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
- if (isAndroid) {
- if (isKeyboardVisible != true) {
- // `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
- gFFI.invokeMethod("enable_soft_keyboard", false);
- }
- }
- }
- class ViewCameraPage extends StatefulWidget {
- ViewCameraPage(
- {Key? key, required this.id, this.password, this.isSharedPassword})
- : super(key: key);
- final String id;
- final String? password;
- final bool? isSharedPassword;
- @override
- State<ViewCameraPage> createState() => _ViewCameraPageState(id);
- }
- class _ViewCameraPageState extends State<ViewCameraPage>
- with WidgetsBindingObserver {
- Timer? _timer;
- bool _showBar = !isWebDesktop;
- bool _showGestureHelp = false;
- Orientation? _currentOrientation;
- double _viewInsetsBottom = 0;
- Timer? _timerDidChangeMetrics;
- final _blockableOverlayState = BlockableOverlayState();
- final keyboardVisibilityController = KeyboardVisibilityController();
- final FocusNode _mobileFocusNode = FocusNode();
- final FocusNode _physicalFocusNode = FocusNode();
- var _showEdit = false; // use soft keyboard
- InputModel get inputModel => gFFI.inputModel;
- SessionID get sessionId => gFFI.sessionId;
- final TextEditingController _textController =
- TextEditingController(text: initText);
- _ViewCameraPageState(String id) {
- initSharedStates(id);
- gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
- gFFI.dialogManager.loadMobileActionsOverlayVisible();
- }
- @override
- void initState() {
- super.initState();
- gFFI.ffiModel.updateEventListener(sessionId, widget.id);
- gFFI.start(
- widget.id,
- isViewCamera: true,
- password: widget.password,
- isSharedPassword: widget.isSharedPassword,
- );
- WidgetsBinding.instance.addPostFrameCallback((_) {
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
- gFFI.dialogManager
- .showLoading(translate('Connecting...'), onCancel: closeConnection);
- });
- if (!isWeb) {
- WakelockPlus.enable();
- }
- _physicalFocusNode.requestFocus();
- gFFI.inputModel.listenToMouse(true);
- gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
- gFFI.chatModel
- .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
- _blockableOverlayState.applyFfi(gFFI);
- gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
- gFFI.recordingModel
- .updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
- if (gFFI.recordingModel.start) {
- showToast(translate('Automatically record outgoing sessions'));
- }
- _disableAndroidSoftKeyboard(
- isKeyboardVisible: keyboardVisibilityController.isVisible);
- });
- WidgetsBinding.instance.addObserver(this);
- }
- @override
- Future<void> dispose() async {
- WidgetsBinding.instance.removeObserver(this);
- // https://github.com/flutter/flutter/issues/64935
- super.dispose();
- gFFI.dialogManager.hideMobileActionsOverlay(store: false);
- gFFI.inputModel.listenToMouse(false);
- gFFI.imageModel.disposeImage();
- gFFI.cursorModel.disposeImages();
- await gFFI.invokeMethod("enable_soft_keyboard", true);
- _mobileFocusNode.dispose();
- _physicalFocusNode.dispose();
- await gFFI.close();
- _timer?.cancel();
- _timerDidChangeMetrics?.cancel();
- gFFI.dialogManager.dismissAll();
- await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
- overlays: SystemUiOverlay.values);
- if (!isWeb) {
- await WakelockPlus.disable();
- }
- removeSharedStates(widget.id);
- // `on_voice_call_closed` should be called when the connection is ended.
- // The inner logic of `on_voice_call_closed` will check if the voice call is active.
- // Only one client is considered here for now.
- gFFI.chatModel.onVoiceCallClosed("End connetion");
- }
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) {}
- @override
- void didChangeMetrics() {
- // If the soft keyboard is visible and the canvas has been changed(panned or scaled)
- // Don't try reset the view style and focus the cursor.
- if (gFFI.cursorModel.lastKeyboardIsVisible &&
- gFFI.canvasModel.isMobileCanvasChanged) {
- return;
- }
- final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
- _timerDidChangeMetrics?.cancel();
- _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
- // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
- if (newBottom != _viewInsetsBottom) {
- gFFI.canvasModel.mobileFocusCanvasCursor();
- _viewInsetsBottom = newBottom;
- }
- });
- }
- // to-do: It should be better to use transparent color instead of the bgColor.
- // But for now, the transparent color will cause the canvas to be white.
- // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
- // But I don't know why and how to fix it.
- Widget emptyOverlay(Color bgColor) => BlockableOverlay(
- /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
- /// see override build() in [BlockableOverlay]
- state: _blockableOverlayState,
- underlying: Container(
- color: bgColor,
- ),
- );
- Widget _bottomWidget() => (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
- ? getBottomAppBar()
- : Offstage());
- @override
- Widget build(BuildContext context) {
- final keyboardIsVisible =
- keyboardVisibilityController.isVisible && _showEdit;
- final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
- return WillPopScope(
- onWillPop: () async {
- clientClose(sessionId, gFFI.dialogManager);
- return false;
- },
- child: Scaffold(
- // workaround for https://github.com/rustdesk/rustdesk/issues/3131
- floatingActionButtonLocation: keyboardIsVisible
- ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
- : null,
- floatingActionButton: !showActionButton
- ? null
- : FloatingActionButton(
- mini: !keyboardIsVisible,
- child: Icon(
- (keyboardIsVisible || _showGestureHelp)
- ? Icons.expand_more
- : Icons.expand_less,
- color: Colors.white,
- ),
- backgroundColor: MyTheme.accent,
- onPressed: () {
- setState(() {
- if (keyboardIsVisible) {
- _showEdit = false;
- gFFI.invokeMethod("enable_soft_keyboard", false);
- _mobileFocusNode.unfocus();
- _physicalFocusNode.requestFocus();
- } else if (_showGestureHelp) {
- _showGestureHelp = false;
- } else {
- _showBar = !_showBar;
- }
- });
- }),
- bottomNavigationBar: Obx(() => Stack(
- alignment: Alignment.bottomCenter,
- children: [
- gFFI.ffiModel.pi.isSet.isTrue &&
- gFFI.ffiModel.waitForFirstImage.isTrue
- ? emptyOverlay(MyTheme.canvasColor)
- : () {
- gFFI.ffiModel.tryShowAndroidActionsOverlay();
- return Offstage();
- }(),
- _bottomWidget(),
- gFFI.ffiModel.pi.isSet.isFalse
- ? emptyOverlay(MyTheme.canvasColor)
- : Offstage(),
- ],
- )),
- body: Obx(
- () => getRawPointerAndKeyBody(Overlay(
- initialEntries: [
- OverlayEntry(builder: (context) {
- return Container(
- color: kColorCanvas,
- child: SafeArea(
- child: OrientationBuilder(builder: (ctx, orientation) {
- if (_currentOrientation != orientation) {
- Timer(const Duration(milliseconds: 200), () {
- gFFI.dialogManager
- .resetMobileActionsOverlay(ffi: gFFI);
- _currentOrientation = orientation;
- gFFI.canvasModel.updateViewStyle();
- });
- }
- return Container(
- color: MyTheme.canvasColor,
- child: inputModel.isPhysicalMouse.value
- ? getBodyForMobile()
- : RawTouchGestureDetectorRegion(
- child: getBodyForMobile(),
- ffi: gFFI,
- isCamera: true,
- ),
- );
- }),
- ),
- );
- })
- ],
- )),
- )),
- );
- }
- Widget getRawPointerAndKeyBody(Widget child) {
- return CameraRawPointerMouseRegion(
- inputModel: inputModel,
- // Disable RawKeyFocusScope before the connecting is established.
- // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
- child: gFFI.ffiModel.pi.isSet.isTrue
- ? RawKeyFocusScope(
- focusNode: _physicalFocusNode,
- inputModel: inputModel,
- child: child)
- : child,
- );
- }
- Widget getBottomAppBar() {
- return BottomAppBar(
- elevation: 10,
- color: MyTheme.accent,
- child: Row(
- mainAxisSize: MainAxisSize.max,
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: <Widget>[
- Row(
- children: <Widget>[
- IconButton(
- color: Colors.white,
- icon: Icon(Icons.clear),
- onPressed: () {
- clientClose(sessionId, gFFI.dialogManager);
- },
- ),
- IconButton(
- color: Colors.white,
- icon: Icon(Icons.tv),
- onPressed: () {
- setState(() => _showEdit = false);
- showOptions(context, widget.id, gFFI.dialogManager);
- },
- )
- ] +
- (isWeb
- ? []
- : <Widget>[
- futureBuilder(
- future: gFFI.invokeMethod(
- "get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
- hasData: (isSupportVoiceCall) => IconButton(
- color: Colors.white,
- icon: isAndroid && isSupportVoiceCall
- ? SvgPicture.asset('assets/chat.svg',
- colorFilter: ColorFilter.mode(
- Colors.white, BlendMode.srcIn))
- : Icon(Icons.message),
- onPressed: () =>
- isAndroid && isSupportVoiceCall
- ? showChatOptions(widget.id)
- : onPressedTextChat(widget.id),
- ))
- ]) +
- [
- IconButton(
- color: Colors.white,
- icon: Icon(Icons.more_vert),
- onPressed: () {
- setState(() => _showEdit = false);
- showActions(widget.id);
- },
- ),
- ]),
- Obx(() => IconButton(
- color: Colors.white,
- icon: Icon(Icons.expand_more),
- onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
- ? null
- : () {
- setState(() => _showBar = !_showBar);
- },
- )),
- ],
- ),
- );
- }
- Widget getBodyForMobile() {
- return Container(
- color: MyTheme.canvasColor,
- child: Stack(children: () {
- final paints = [
- ImagePaint(),
- Positioned(
- top: 10,
- right: 10,
- child: QualityMonitor(gFFI.qualityMonitorModel),
- ),
- SizedBox(
- width: 0,
- height: 0,
- child: !_showEdit
- ? Container()
- : TextFormField(
- textInputAction: TextInputAction.newline,
- autocorrect: false,
- // Flutter 3.16.9 Android.
- // `enableSuggestions` causes secure keyboard to be shown.
- // https://github.com/flutter/flutter/issues/139143
- // https://github.com/flutter/flutter/issues/146540
- // enableSuggestions: false,
- autofocus: true,
- focusNode: _mobileFocusNode,
- maxLines: null,
- controller: _textController,
- // trick way to make backspace work always
- keyboardType: TextInputType.multiline,
- // `onChanged` may be called depending on the input method if this widget is wrapped in
- // `Focus(onKeyEvent: ..., child: ...)`
- // For `Backspace` button in the soft keyboard:
- // en/fr input method:
- // 1. The button will not trigger `onKeyEvent` if the text field is not empty.
- // 2. The button will trigger `onKeyEvent` if the text field is empty.
- // ko/zh/ja input method: the button will trigger `onKeyEvent`
- // and the event will not popup if `KeyEventResult.handled` is returned.
- onChanged: null,
- ).workaroundFreezeLinuxMint(),
- ),
- ];
- return paints;
- }()));
- }
- Widget getBodyForDesktopWithListener() {
- var paints = <Widget>[ImagePaint()];
- return Container(
- color: MyTheme.canvasColor, child: Stack(children: paints));
- }
- List<TTextMenu> _getMobileActionMenus() {
- if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
- !gFFI.ffiModel.keyboard) {
- return [];
- }
- final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
- if (!enabled) return [];
- return [
- TTextMenu(
- child: Text(translate('Back')),
- onPressed: () => gFFI.inputModel.onMobileBack(),
- ),
- TTextMenu(
- child: Text(translate('Home')),
- onPressed: () => gFFI.inputModel.onMobileHome(),
- ),
- TTextMenu(
- child: Text(translate('Apps')),
- onPressed: () => gFFI.inputModel.onMobileApps(),
- ),
- TTextMenu(
- child: Text(translate('Volume up')),
- onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
- ),
- TTextMenu(
- child: Text(translate('Volume down')),
- onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
- ),
- TTextMenu(
- child: Text(translate('Power')),
- onPressed: () => gFFI.inputModel.onMobilePower(),
- ),
- ];
- }
- void showActions(String id) async {
- final size = MediaQuery.of(context).size;
- final x = 120.0;
- final y = size.height;
- final mobileActionMenus = _getMobileActionMenus();
- final menus = toolbarControls(context, id, gFFI);
- final List<PopupMenuEntry<int>> more = [
- ...mobileActionMenus
- .asMap()
- .entries
- .map((e) =>
- PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
- .toList(),
- if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
- ...menus
- .asMap()
- .entries
- .map((e) => PopupMenuItem<int>(
- child: e.value.getChild(),
- value: e.key + mobileActionMenus.length))
- .toList(),
- ];
- () async {
- var index = await showMenu(
- context: context,
- position: RelativeRect.fromLTRB(x, y, x, y),
- items: more,
- elevation: 8,
- );
- if (index != null) {
- if (index < mobileActionMenus.length) {
- mobileActionMenus[index].onPressed?.call();
- } else if (index < mobileActionMenus.length + more.length) {
- menus[index - mobileActionMenus.length].onPressed?.call();
- }
- }
- }();
- }
- onPressedTextChat(String id) {
- gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
- gFFI.chatModel.toggleChatOverlay();
- }
- showChatOptions(String id) async {
- onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
- onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
- makeTextMenu(String label, Widget icon, VoidCallback onPressed,
- {TextStyle? labelStyle}) =>
- TTextMenu(
- child: Text(translate(label), style: labelStyle),
- trailingIcon: Transform.scale(
- scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
- child: IgnorePointer(
- child: IconButton(
- onPressed: null,
- icon: icon,
- ),
- ),
- ),
- onPressed: onPressed,
- );
- final isInVoice = [
- VoiceCallStatus.waitingForResponse,
- VoiceCallStatus.connected
- ].contains(gFFI.chatModel.voiceCallStatus.value);
- final menus = [
- makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
- () => onPressedTextChat(widget.id)),
- isInVoice
- ? makeTextMenu(
- 'End voice call',
- SvgPicture.asset(
- 'assets/call_wait.svg',
- colorFilter:
- ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
- ),
- onPressEndVoiceCall,
- labelStyle: TextStyle(color: Colors.redAccent))
- : makeTextMenu(
- 'Voice call',
- SvgPicture.asset(
- 'assets/call_wait.svg',
- colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
- ),
- onPressVoiceCall),
- ];
- final menuItems = menus
- .asMap()
- .entries
- .map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
- .toList();
- Future.delayed(Duration.zero, () async {
- final size = MediaQuery.of(context).size;
- final x = 120.0;
- final y = size.height;
- var index = await showMenu(
- context: context,
- position: RelativeRect.fromLTRB(x, y, x, y),
- items: menuItems,
- elevation: 8,
- );
- if (index != null && index < menus.length) {
- menus[index].onPressed?.call();
- }
- });
- }
- }
- class ImagePaint extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- final m = Provider.of<ImageModel>(context);
- final c = Provider.of<CanvasModel>(context);
- var s = c.scale;
- final adjust = c.getAdjustY();
- return CustomPaint(
- painter: ImagePainter(
- image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
- );
- }
- }
- void showOptions(
- BuildContext context, String id, OverlayDialogManager dialogManager) async {
- var displays = <Widget>[];
- final pi = gFFI.ffiModel.pi;
- final image = gFFI.ffiModel.getConnectionImage();
- if (image != null) {
- displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
- }
- if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
- final cur = pi.currentDisplay;
- final children = <Widget>[];
- for (var i = 0; i < pi.displays.length; ++i) {
- children.add(InkWell(
- onTap: () {
- if (i == cur) return;
- openMonitorInTheSameTab(i, gFFI, pi);
- gFFI.dialogManager.dismissAll();
- },
- child: Ink(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- border: Border.all(color: Theme.of(context).hintColor),
- borderRadius: BorderRadius.circular(2),
- color: i == cur
- ? Theme.of(context).primaryColor.withOpacity(0.6)
- : null),
- child: Center(
- child: Text((i + 1).toString(),
- style: TextStyle(
- color: i == cur ? Colors.white : Colors.black87,
- fontWeight: FontWeight.bold))))));
- }
- displays.add(Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Wrap(
- alignment: WrapAlignment.center,
- spacing: 8,
- children: children,
- )));
- }
- if (displays.isNotEmpty) {
- displays.add(const Divider(color: MyTheme.border));
- }
- List<TRadioMenu<String>> viewStyleRadios =
- await toolbarViewStyle(context, id, gFFI);
- List<TRadioMenu<String>> imageQualityRadios =
- await toolbarImageQuality(context, id, gFFI);
- List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
- List<TToggleMenu> displayToggles =
- await toolbarDisplayToggle(context, id, gFFI);
- dialogManager.show((setState, close, context) {
- var viewStyle =
- (viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
- var imageQuality =
- (imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '')
- .obs;
- var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
- final radios = [
- for (var e in viewStyleRadios)
- Obx(() => getRadio<String>(
- e.child,
- e.value,
- viewStyle.value,
- e.onChanged != null
- ? (v) {
- e.onChanged?.call(v);
- if (v != null) viewStyle.value = v;
- }
- : null)),
- const Divider(color: MyTheme.border),
- for (var e in imageQualityRadios)
- Obx(() => getRadio<String>(
- e.child,
- e.value,
- imageQuality.value,
- e.onChanged != null
- ? (v) {
- e.onChanged?.call(v);
- if (v != null) imageQuality.value = v;
- }
- : null)),
- const Divider(color: MyTheme.border),
- for (var e in codecRadios)
- Obx(() => getRadio<String>(
- e.child,
- e.value,
- codec.value,
- e.onChanged != null
- ? (v) {
- e.onChanged?.call(v);
- if (v != null) codec.value = v;
- }
- : null)),
- if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
- ];
- final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
- final displayTogglesList = displayToggles
- .asMap()
- .entries
- .map((e) => Obx(() => CheckboxListTile(
- contentPadding: EdgeInsets.zero,
- visualDensity: VisualDensity.compact,
- value: rxToggleValues[e.key].value,
- onChanged: e.value.onChanged != null
- ? (v) {
- e.value.onChanged?.call(v);
- if (v != null) rxToggleValues[e.key].value = v;
- }
- : null,
- title: e.value.child)))
- .toList();
- final toggles = [
- ...displayTogglesList,
- ];
- var popupDialogMenus = List<Widget>.empty(growable: true);
- if (popupDialogMenus.isNotEmpty) {
- popupDialogMenus.add(const Divider(color: MyTheme.border));
- }
- return CustomAlertDialog(
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: displays + radios + popupDialogMenus + toggles),
- );
- }, clickMaskDismiss: true, backDismiss: true).then((value) {
- _disableAndroidSoftKeyboard();
- });
- }
- class FABLocation extends FloatingActionButtonLocation {
- FloatingActionButtonLocation location;
- double offsetX;
- double offsetY;
- FABLocation(this.location, this.offsetX, this.offsetY);
- @override
- Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
- final offset = location.getOffset(scaffoldGeometry);
- return Offset(offset.dx + offsetX, offset.dy + offsetY);
- }
- }
|