mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-05 16:38:17 +00:00
Add Without password actions and pass the selection through desktop, mobile and Web sessions. Omit the password when constructing LoginRequest, preserve saved credentials, and reuse the existing approval and OS login dialogs. Keep protocol and controlled-side authentication changes separate.
720 lines
23 KiB
Dart
720 lines
23 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hbb/common/widgets/remote_input.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:flutter_hbb/models/state_model.dart';
|
|
|
|
import '../../consts.dart';
|
|
import '../../common/widgets/overlay.dart';
|
|
import '../../common.dart';
|
|
import '../../common/widgets/dialog.dart';
|
|
import '../../common/widgets/toolbar.dart';
|
|
import '../../models/model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../../common/shared_state.dart';
|
|
import '../../utils/image.dart';
|
|
import '../widgets/remote_toolbar.dart';
|
|
import '../widgets/kb_layout_type_chooser.dart';
|
|
import '../widgets/tabbar_widget.dart';
|
|
|
|
import 'package:flutter_hbb/native/custom_cursor.dart'
|
|
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
|
|
|
|
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
|
|
|
// Used to skip session close if "move to new window" is clicked.
|
|
final Map<String, bool> closeSessionOnDispose = {};
|
|
|
|
class ViewCameraPage extends StatefulWidget {
|
|
ViewCameraPage({
|
|
Key? key,
|
|
required this.id,
|
|
required this.toolbarState,
|
|
this.sessionId,
|
|
this.tabWindowId,
|
|
this.password,
|
|
this.loginMethod = '',
|
|
this.display,
|
|
this.displays,
|
|
this.tabController,
|
|
this.connToken,
|
|
this.forceRelay,
|
|
this.isSharedPassword,
|
|
}) : super(key: key) {
|
|
initSharedStates(id);
|
|
}
|
|
|
|
final String id;
|
|
final SessionID? sessionId;
|
|
final int? tabWindowId;
|
|
final int? display;
|
|
final List<int>? displays;
|
|
final String? password;
|
|
final String loginMethod;
|
|
final ToolbarState toolbarState;
|
|
final bool? forceRelay;
|
|
final bool? isSharedPassword;
|
|
final String? connToken;
|
|
final SimpleWrapper<State<ViewCameraPage>?> _lastState = SimpleWrapper(null);
|
|
final DesktopTabController? tabController;
|
|
|
|
FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi;
|
|
|
|
@override
|
|
State<ViewCameraPage> createState() {
|
|
final state = _ViewCameraPageState(id);
|
|
_lastState.value = state;
|
|
return state;
|
|
}
|
|
}
|
|
|
|
class _ViewCameraPageState extends State<ViewCameraPage>
|
|
with AutomaticKeepAliveClientMixin, MultiWindowListener {
|
|
Timer? _timer;
|
|
String keyboardMode = "legacy";
|
|
bool _isWindowBlur = false;
|
|
final _cursorOverImage = false.obs;
|
|
final _uniqueKey = UniqueKey();
|
|
|
|
var _blockableOverlayState = BlockableOverlayState();
|
|
|
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
|
|
|
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
|
// to identify the toolbar instance and its callback function.
|
|
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
|
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
|
|
|
late FFI _ffi;
|
|
|
|
SessionID get sessionId => _ffi.sessionId;
|
|
|
|
_ViewCameraPageState(String id) {
|
|
_initStates(id);
|
|
}
|
|
|
|
void _initStates(String id) {}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ffi = FFI(widget.sessionId);
|
|
Get.put<FFI>(_ffi, tag: widget.id);
|
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
|
showKBLayoutTypeChooserIfNeeded(
|
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
|
_ffi.recordingModel
|
|
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
|
});
|
|
_ffi.start(
|
|
widget.id,
|
|
isViewCamera: true,
|
|
password: widget.password,
|
|
loginMethod: widget.loginMethod,
|
|
isSharedPassword: widget.isSharedPassword,
|
|
forceRelay: widget.forceRelay,
|
|
tabWindowId: widget.tabWindowId,
|
|
display: widget.display,
|
|
displays: widget.displays,
|
|
connToken: widget.connToken,
|
|
);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
|
_ffi.dialogManager
|
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
|
});
|
|
WakelockManager.enable(_uniqueKey);
|
|
|
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
|
DesktopMultiWindow.addListener(this);
|
|
// if (!_isCustomCursorInited) {
|
|
// customCursorController.registerNeedUpdateCursorCallback(
|
|
// (String? lastKey, String? currentKey) async {
|
|
// if (_firstEnterImage.value) {
|
|
// _firstEnterImage.value = false;
|
|
// return true;
|
|
// }
|
|
// return lastKey == null || lastKey != currentKey;
|
|
// });
|
|
// _isCustomCursorInited = true;
|
|
// }
|
|
|
|
_blockableOverlayState.applyFfi(_ffi);
|
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
widget.tabController?.onSelected?.call(widget.id);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void onWindowBlur() {
|
|
super.onWindowBlur();
|
|
// On windows, we use `focus` way to handle keyboard better.
|
|
// Now on Linux, there's some rdev issues which will break the input.
|
|
// We disable the `focus` way for non-Windows temporarily.
|
|
if (isWindows) {
|
|
_isWindowBlur = true;
|
|
// unfocus the primary-focus when the whole window is lost focus,
|
|
// and let OS to handle events instead.
|
|
_rawKeyFocusNode.unfocus();
|
|
}
|
|
stateGlobal.isFocused.value = false;
|
|
}
|
|
|
|
@override
|
|
void onWindowFocus() {
|
|
super.onWindowFocus();
|
|
// See [onWindowBlur].
|
|
if (isWindows) {
|
|
_isWindowBlur = false;
|
|
}
|
|
stateGlobal.isFocused.value = true;
|
|
}
|
|
|
|
@override
|
|
void onWindowRestore() {
|
|
super.onWindowRestore();
|
|
// On windows, we use `onWindowRestore` way to handle window restore from
|
|
// a minimized state.
|
|
if (isWindows) {
|
|
_isWindowBlur = false;
|
|
}
|
|
WakelockManager.enable(_uniqueKey);
|
|
}
|
|
|
|
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
|
|
@override
|
|
void onWindowMaximize() {
|
|
super.onWindowMaximize();
|
|
WakelockManager.enable(_uniqueKey);
|
|
}
|
|
|
|
@override
|
|
void onWindowMinimize() {
|
|
super.onWindowMinimize();
|
|
WakelockManager.disable(_uniqueKey);
|
|
}
|
|
|
|
@override
|
|
void onWindowEnterFullScreen() {
|
|
super.onWindowEnterFullScreen();
|
|
if (isMacOS) {
|
|
stateGlobal.setFullscreen(true);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onWindowLeaveFullScreen() {
|
|
super.onWindowLeaveFullScreen();
|
|
if (isMacOS) {
|
|
stateGlobal.setFullscreen(false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
|
|
|
// https://github.com/flutter/flutter/issues/64935
|
|
super.dispose();
|
|
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
|
_ffi.textureModel.onViewCameraPageDispose(closeSession);
|
|
if (closeSession) {
|
|
// ensure we leave this session, this is a double check
|
|
_ffi.inputModel.enterOrLeave(false);
|
|
}
|
|
DesktopMultiWindow.removeListener(this);
|
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
|
_ffi.imageModel.disposeImage();
|
|
_ffi.cursorModel.disposeImages();
|
|
_rawKeyFocusNode.dispose();
|
|
await _ffi.close(closeSession: closeSession);
|
|
_timer?.cancel();
|
|
_ffi.dialogManager.dismissAll();
|
|
if (closeSession) {
|
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
}
|
|
WakelockManager.disable(_uniqueKey);
|
|
await Get.delete<FFI>(tag: widget.id);
|
|
removeSharedStates(widget.id);
|
|
}
|
|
|
|
Widget emptyOverlay() => BlockableOverlay(
|
|
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
|
|
/// see override build() in [BlockableOverlay]
|
|
state: _blockableOverlayState,
|
|
underlying: Container(
|
|
color: Colors.transparent,
|
|
),
|
|
);
|
|
|
|
Widget buildBody(BuildContext context) {
|
|
remoteToolbar(BuildContext context) => RemoteToolbar(
|
|
id: widget.id,
|
|
ffi: _ffi,
|
|
state: widget.toolbarState,
|
|
onEnterOrLeaveImageSetter: (id, func) {
|
|
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
|
|
_onEnterOrLeaveImage4Toolbar = func;
|
|
},
|
|
onEnterOrLeaveImageCleaner: (id) {
|
|
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
|
|
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
|
|
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
|
|
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
|
|
_onEnterOrLeaveImage4Toolbar = null;
|
|
}
|
|
},
|
|
setRemoteState: setState,
|
|
);
|
|
|
|
bodyWidget() {
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
color: kColorCanvas,
|
|
child: getBodyForDesktop(context),
|
|
),
|
|
Stack(
|
|
children: [
|
|
_ffi.ffiModel.pi.isSet.isTrue &&
|
|
_ffi.ffiModel.waitForFirstImage.isTrue
|
|
? emptyOverlay()
|
|
: () {
|
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
|
return Offstage();
|
|
} else {
|
|
return Obx(() => Offstage(
|
|
offstage: _ffi.dialogManager
|
|
.mobileActionsOverlayVisible.isFalse,
|
|
child: Overlay(initialEntries: [
|
|
makeMobileActionsOverlayEntry(
|
|
() => _ffi.dialogManager
|
|
.setMobileActionsOverlayVisible(false),
|
|
ffi: _ffi,
|
|
)
|
|
]),
|
|
));
|
|
}
|
|
}(),
|
|
// Use Overlay to enable rebuild every time on menu button click.
|
|
_ffi.ffiModel.pi.isSet.isTrue
|
|
? Overlay(
|
|
initialEntries: [OverlayEntry(builder: remoteToolbar)])
|
|
: remoteToolbar(context),
|
|
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
body: Obx(() {
|
|
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
|
|
_ffi.ffiModel.waitForFirstImage.isFalse;
|
|
if (imageReady) {
|
|
// If the privacy mode(disable physical displays) is switched,
|
|
// we should not dismiss the dialog immediately.
|
|
if (DateTime.now().difference(togglePrivacyModeTime) >
|
|
const Duration(milliseconds: 3000)) {
|
|
// `dismissAll()` is to ensure that the state is clean.
|
|
// It's ok to call dismissAll() here.
|
|
_ffi.dialogManager.dismissAll();
|
|
// Recreate the block state to refresh the state.
|
|
_blockableOverlayState = BlockableOverlayState();
|
|
_blockableOverlayState.applyFfi(_ffi);
|
|
}
|
|
// Block the whole `bodyWidget()` when dialog shows.
|
|
return BlockableOverlay(
|
|
underlying: bodyWidget(),
|
|
state: _blockableOverlayState,
|
|
);
|
|
} else {
|
|
// `_blockableOverlayState` is not recreated here.
|
|
// The toolbar's block state won't work properly when reconnecting, but that's okay.
|
|
return bodyWidget();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
clientClose(sessionId, _ffi);
|
|
return false;
|
|
},
|
|
child: MultiProvider(providers: [
|
|
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
|
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
|
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
|
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
|
ChangeNotifierProvider.value(value: _ffi.recordingModel),
|
|
], child: buildBody(context)));
|
|
}
|
|
|
|
void enterView(PointerEnterEvent evt) {
|
|
_cursorOverImage.value = true;
|
|
_firstEnterImage.value = true;
|
|
if (_onEnterOrLeaveImage4Toolbar != null) {
|
|
try {
|
|
_onEnterOrLeaveImage4Toolbar!(true);
|
|
} catch (e) {
|
|
//
|
|
}
|
|
}
|
|
// See [onWindowBlur].
|
|
if (!isWindows) {
|
|
if (!_rawKeyFocusNode.hasFocus) {
|
|
_rawKeyFocusNode.requestFocus();
|
|
}
|
|
_ffi.inputModel.enterOrLeave(true);
|
|
}
|
|
}
|
|
|
|
void leaveView(PointerExitEvent evt) {
|
|
if (_ffi.ffiModel.keyboard) {
|
|
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
|
}
|
|
|
|
_cursorOverImage.value = false;
|
|
_firstEnterImage.value = false;
|
|
if (_onEnterOrLeaveImage4Toolbar != null) {
|
|
try {
|
|
_onEnterOrLeaveImage4Toolbar!(false);
|
|
} catch (e) {
|
|
//
|
|
}
|
|
}
|
|
// See [onWindowBlur].
|
|
if (!isWindows) {
|
|
_ffi.inputModel.enterOrLeave(false);
|
|
}
|
|
}
|
|
|
|
Widget _buildRawTouchAndPointerRegion(
|
|
Widget child,
|
|
PointerEnterEventListener? onEnter,
|
|
PointerExitEventListener? onExit,
|
|
) {
|
|
return RawTouchGestureDetectorRegion(
|
|
child: _buildRawPointerMouseRegion(child, onEnter, onExit),
|
|
ffi: _ffi,
|
|
isCamera: true,
|
|
);
|
|
}
|
|
|
|
Widget _buildRawPointerMouseRegion(
|
|
Widget child,
|
|
PointerEnterEventListener? onEnter,
|
|
PointerExitEventListener? onExit,
|
|
) {
|
|
return CameraRawPointerMouseRegion(
|
|
onEnter: onEnter,
|
|
onExit: onExit,
|
|
onPointerDown: (event) {
|
|
// A double check for blur status.
|
|
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
|
|
// Sometimes the system does not send the necessary focus event to flutter. We should manually
|
|
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
|
|
// ensure the grab-key thread is running when our users are clicking the remote canvas.
|
|
if (_isWindowBlur) {
|
|
debugPrint(
|
|
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
|
|
_isWindowBlur = false;
|
|
}
|
|
if (!_rawKeyFocusNode.hasFocus) {
|
|
_rawKeyFocusNode.requestFocus();
|
|
}
|
|
},
|
|
inputModel: _ffi.inputModel,
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget getBodyForDesktop(BuildContext context) {
|
|
var paints = <Widget>[
|
|
MouseRegion(onEnter: (evt) {
|
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
|
}, onExit: (evt) {
|
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
|
}, child: LayoutBuilder(builder: (context, constraints) {
|
|
final c = Provider.of<CanvasModel>(context, listen: false);
|
|
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
|
final peerDisplay = CurrentDisplayState.find(widget.id);
|
|
return Obx(
|
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
|
? Container(color: Colors.transparent)
|
|
: Obx(() {
|
|
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
|
return ImagePaint(
|
|
id: widget.id,
|
|
cursorOverImage: _cursorOverImage,
|
|
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
|
|
child, enterView, leaveView),
|
|
ffi: _ffi,
|
|
);
|
|
}),
|
|
);
|
|
}))
|
|
];
|
|
|
|
paints.add(
|
|
Positioned(
|
|
top: 10,
|
|
right: 10,
|
|
child: _buildRawTouchAndPointerRegion(
|
|
QualityMonitor(_ffi.qualityMonitorModel), null, null),
|
|
),
|
|
);
|
|
return Stack(
|
|
children: paints,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
class ImagePaint extends StatefulWidget {
|
|
final FFI ffi;
|
|
final String id;
|
|
final RxBool cursorOverImage;
|
|
final Widget Function(Widget)? listenerBuilder;
|
|
|
|
ImagePaint(
|
|
{Key? key,
|
|
required this.ffi,
|
|
required this.id,
|
|
required this.cursorOverImage,
|
|
this.listenerBuilder})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _ImagePaintState();
|
|
}
|
|
|
|
class _ImagePaintState extends State<ImagePaint> {
|
|
String get id => widget.id;
|
|
RxBool get cursorOverImage => widget.cursorOverImage;
|
|
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final m = Provider.of<ImageModel>(context);
|
|
var c = Provider.of<CanvasModel>(context);
|
|
final s = c.scale;
|
|
|
|
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
|
|
|
|
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
|
final paintWidth = c.getDisplayWidth() * s;
|
|
final paintHeight = c.getDisplayHeight() * s;
|
|
final paintSize = Size(paintWidth, paintHeight);
|
|
final paintWidget =
|
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
|
? _BuildPaintTextureRender(
|
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
|
return NotificationListener<ScrollNotification>(
|
|
onNotification: (notification) {
|
|
c.updateScrollPercent();
|
|
return false;
|
|
},
|
|
child: Container(
|
|
child: _buildCrossScrollbarFromLayout(
|
|
context,
|
|
_buildListener(paintWidget),
|
|
c.size,
|
|
paintSize,
|
|
c.scrollHorizontal,
|
|
c.scrollVertical,
|
|
)),
|
|
);
|
|
} else {
|
|
if (c.size.width > 0 && c.size.height > 0) {
|
|
final paintWidget =
|
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
|
? _BuildPaintTextureRender(
|
|
c,
|
|
s,
|
|
Offset(
|
|
isLinux ? c.x.toInt().toDouble() : c.x,
|
|
isLinux ? c.y.toInt().toDouble() : c.y,
|
|
),
|
|
c.size,
|
|
isViewOriginal())
|
|
: _buildScrollAutoNonTextureRender(m, c, s);
|
|
return Container(child: _buildListener(paintWidget));
|
|
} else {
|
|
return Container();
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildScrollbarNonTextureRender(
|
|
ImageModel m, Size imageSize, double s) {
|
|
return CustomPaint(
|
|
size: imageSize,
|
|
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
|
);
|
|
}
|
|
|
|
Widget _buildScrollAutoNonTextureRender(
|
|
ImageModel m, CanvasModel c, double s) {
|
|
return CustomPaint(
|
|
size: Size(c.size.width, c.size.height),
|
|
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
|
);
|
|
}
|
|
|
|
Widget _BuildPaintTextureRender(
|
|
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
|
|
final ffiModel = c.parent.target!.ffiModel;
|
|
final displays = ffiModel.pi.getCurDisplays();
|
|
final children = <Widget>[];
|
|
final rect = ffiModel.rect;
|
|
if (rect == null) {
|
|
return Container();
|
|
}
|
|
final curDisplay = ffiModel.pi.currentDisplay;
|
|
for (var i = 0; i < displays.length; i++) {
|
|
final textureId = widget.ffi.textureModel
|
|
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
|
|
if (true) {
|
|
// both "textureId.value != -1" and "true" seems ok
|
|
children.add(Positioned(
|
|
left: (displays[i].x - rect.left) * s + offset.dx,
|
|
top: (displays[i].y - rect.top) * s + offset.dy,
|
|
width: displays[i].width * s,
|
|
height: displays[i].height * s,
|
|
child: Obx(() => Texture(
|
|
textureId: textureId.value,
|
|
filterQuality:
|
|
isViewOriginal ? FilterQuality.none : FilterQuality.low,
|
|
)),
|
|
));
|
|
}
|
|
}
|
|
return SizedBox(
|
|
width: size.width,
|
|
height: size.height,
|
|
child: Stack(children: children),
|
|
);
|
|
}
|
|
|
|
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
|
|
final cursor = Provider.of<CursorModel>(context);
|
|
final cache = cursor.cache ?? preDefaultCursor.cache;
|
|
return buildCursorOfCache(cursor, scale, cache);
|
|
}
|
|
|
|
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
|
|
final cursor = Provider.of<CursorModel>(context);
|
|
final cache = preForbiddenCursor.cache;
|
|
return buildCursorOfCache(cursor, scale, cache);
|
|
}
|
|
|
|
Widget _buildCrossScrollbarFromLayout(
|
|
BuildContext context,
|
|
Widget child,
|
|
Size layoutSize,
|
|
Size size,
|
|
ScrollController horizontal,
|
|
ScrollController vertical,
|
|
) {
|
|
var widget = child;
|
|
if (layoutSize.width < size.width) {
|
|
widget = ScrollConfiguration(
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
child: SingleChildScrollView(
|
|
controller: horizontal,
|
|
scrollDirection: Axis.horizontal,
|
|
physics: cursorOverImage.isTrue
|
|
? const NeverScrollableScrollPhysics()
|
|
: null,
|
|
child: widget,
|
|
),
|
|
);
|
|
} else {
|
|
widget = Row(
|
|
children: [
|
|
Container(
|
|
width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
|
|
),
|
|
widget,
|
|
],
|
|
);
|
|
}
|
|
if (layoutSize.height < size.height) {
|
|
widget = ScrollConfiguration(
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
child: SingleChildScrollView(
|
|
controller: vertical,
|
|
physics: cursorOverImage.isTrue
|
|
? const NeverScrollableScrollPhysics()
|
|
: null,
|
|
child: widget,
|
|
),
|
|
);
|
|
} else {
|
|
widget = Column(
|
|
children: [
|
|
Container(
|
|
height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
|
|
),
|
|
widget,
|
|
],
|
|
);
|
|
}
|
|
if (layoutSize.width < size.width) {
|
|
widget = RawScrollbar(
|
|
thickness: kScrollbarThickness,
|
|
thumbColor: Colors.grey,
|
|
controller: horizontal,
|
|
thumbVisibility: false,
|
|
trackVisibility: false,
|
|
notificationPredicate: layoutSize.height < size.height
|
|
? (notification) => notification.depth == 1
|
|
: defaultScrollNotificationPredicate,
|
|
child: widget,
|
|
);
|
|
}
|
|
if (layoutSize.height < size.height) {
|
|
widget = RawScrollbar(
|
|
thickness: kScrollbarThickness,
|
|
thumbColor: Colors.grey,
|
|
controller: vertical,
|
|
thumbVisibility: false,
|
|
trackVisibility: false,
|
|
child: widget,
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
child: widget,
|
|
width: layoutSize.width,
|
|
height: layoutSize.height,
|
|
);
|
|
}
|
|
|
|
Widget _buildListener(Widget child) {
|
|
if (listenerBuilder != null) {
|
|
return listenerBuilder!(child);
|
|
} else {
|
|
return child;
|
|
}
|
|
}
|
|
}
|