mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-30 17:07:54 +08:00
6e44a91d0b
* Refact. Flutter web desktop Signed-off-by: fufesou <shuanglongchen@yeah.net> * Flutter web, prevent default context menu Signed-off-by: fufesou <shuanglongchen@yeah.net> --------- Signed-off-by: fufesou <shuanglongchen@yeah.net>
568 lines
17 KiB
Dart
568 lines
17 KiB
Dart
import 'package:auto_size_text/auto_size_text.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hbb/common.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../consts.dart';
|
|
import '../../desktop/widgets/tabbar_widget.dart';
|
|
import '../../models/chat_model.dart';
|
|
import '../../models/model.dart';
|
|
import 'chat_page.dart';
|
|
|
|
class DraggableChatWindow extends StatelessWidget {
|
|
const DraggableChatWindow(
|
|
{Key? key,
|
|
this.position = Offset.zero,
|
|
required this.width,
|
|
required this.height,
|
|
required this.chatModel})
|
|
: super(key: key);
|
|
|
|
final Offset position;
|
|
final double width;
|
|
final double height;
|
|
final ChatModel chatModel;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return isIOS
|
|
? IOSDraggable(
|
|
position: position,
|
|
chatModel: chatModel,
|
|
width: width,
|
|
height: height,
|
|
builder: (context) {
|
|
return Column(
|
|
children: [
|
|
_buildMobileAppBar(context),
|
|
Expanded(
|
|
child: ChatPage(chatModel: chatModel),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
)
|
|
: Draggable(
|
|
checkKeyboard: true,
|
|
position: position,
|
|
width: width,
|
|
height: height,
|
|
chatModel: chatModel,
|
|
builder: (context, onPanUpdate) {
|
|
final child = Scaffold(
|
|
resizeToAvoidBottomInset: false,
|
|
appBar: CustomAppBar(
|
|
onPanUpdate: onPanUpdate,
|
|
appBar: (isDesktop || isWebDesktop)
|
|
? _buildDesktopAppBar(context)
|
|
: _buildMobileAppBar(context),
|
|
),
|
|
body: ChatPage(chatModel: chatModel),
|
|
);
|
|
return Container(
|
|
decoration:
|
|
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
|
child: child);
|
|
});
|
|
}
|
|
|
|
Widget _buildMobileAppBar(BuildContext context) {
|
|
return Container(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
height: 50,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
child: Text(
|
|
translate("Chat"),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontFamily: 'WorkSans',
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20),
|
|
)),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {
|
|
chatModel.hideChatWindowOverlay();
|
|
},
|
|
icon: const Icon(
|
|
Icons.keyboard_arrow_down,
|
|
color: Colors.white,
|
|
)),
|
|
IconButton(
|
|
onPressed: () {
|
|
chatModel.hideChatWindowOverlay();
|
|
chatModel.hideChatIconOverlay();
|
|
},
|
|
icon: const Icon(
|
|
Icons.close,
|
|
color: Colors.white,
|
|
))
|
|
],
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDesktopAppBar(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: Theme.of(context).hintColor.withOpacity(0.4)))),
|
|
height: 38,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
|
child: Obx(() => Opacity(
|
|
opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4,
|
|
child: Row(children: [
|
|
Icon(Icons.chat_bubble_outline,
|
|
size: 20, color: Theme.of(context).colorScheme.primary),
|
|
SizedBox(width: 6),
|
|
Text(translate("Chat"))
|
|
])))),
|
|
Padding(
|
|
padding: EdgeInsets.all(2),
|
|
child: ActionIcon(
|
|
message: 'Close',
|
|
icon: IconFont.close,
|
|
onTap: chatModel.hideChatWindowOverlay,
|
|
isClose: true,
|
|
boxSize: 32,
|
|
))
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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 => const Size.fromHeight(kToolbarHeight);
|
|
}
|
|
|
|
/// floating buttons of back/home/recent actions for android
|
|
class DraggableMobileActions extends StatelessWidget {
|
|
DraggableMobileActions(
|
|
{this.position = Offset.zero,
|
|
this.onBackPressed,
|
|
this.onRecentPressed,
|
|
this.onHomePressed,
|
|
this.onHidePressed,
|
|
required this.width,
|
|
required this.height});
|
|
|
|
final Offset position;
|
|
final double width;
|
|
final double height;
|
|
final VoidCallback? onBackPressed;
|
|
final VoidCallback? onHomePressed;
|
|
final VoidCallback? onRecentPressed;
|
|
final VoidCallback? onHidePressed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Draggable(
|
|
position: position,
|
|
width: width,
|
|
height: height,
|
|
builder: (_, onPanUpdate) {
|
|
return GestureDetector(
|
|
onPanUpdate: onPanUpdate,
|
|
child: Card(
|
|
color: Colors.transparent,
|
|
shadowColor: Colors.transparent,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: MyTheme.accent.withOpacity(0.4),
|
|
borderRadius: BorderRadius.all(Radius.circular(15))),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
IconButton(
|
|
color: Colors.white,
|
|
onPressed: onBackPressed,
|
|
splashRadius: kDesktopIconButtonSplashRadius,
|
|
icon: const Icon(Icons.arrow_back)),
|
|
IconButton(
|
|
color: Colors.white,
|
|
onPressed: onHomePressed,
|
|
splashRadius: kDesktopIconButtonSplashRadius,
|
|
icon: const Icon(Icons.home)),
|
|
IconButton(
|
|
color: Colors.white,
|
|
onPressed: onRecentPressed,
|
|
splashRadius: kDesktopIconButtonSplashRadius,
|
|
icon: const Icon(Icons.more_horiz)),
|
|
const VerticalDivider(
|
|
width: 0,
|
|
thickness: 2,
|
|
indent: 10,
|
|
endIndent: 10,
|
|
),
|
|
IconButton(
|
|
color: Colors.white,
|
|
onPressed: onHidePressed,
|
|
splashRadius: kDesktopIconButtonSplashRadius,
|
|
icon: const Icon(Icons.keyboard_arrow_down)),
|
|
],
|
|
),
|
|
)));
|
|
});
|
|
}
|
|
}
|
|
|
|
class Draggable extends StatefulWidget {
|
|
const Draggable(
|
|
{Key? key,
|
|
this.checkKeyboard = false,
|
|
this.checkScreenSize = false,
|
|
this.position = Offset.zero,
|
|
required this.width,
|
|
required this.height,
|
|
this.chatModel,
|
|
required this.builder})
|
|
: super(key: key);
|
|
|
|
final bool checkKeyboard;
|
|
final bool checkScreenSize;
|
|
final Offset position;
|
|
final double width;
|
|
final double height;
|
|
final ChatModel? chatModel;
|
|
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _DraggableState();
|
|
}
|
|
|
|
class _DraggableState extends State<Draggable> {
|
|
late Offset _position;
|
|
late ChatModel? _chatModel;
|
|
bool _keyboardVisible = false;
|
|
double _saveHeight = 0;
|
|
double _lastBottomHeight = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_position = widget.position;
|
|
_chatModel = widget.chatModel;
|
|
}
|
|
|
|
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);
|
|
});
|
|
_chatModel?.setChatWindowPosition(_position);
|
|
}
|
|
|
|
checkScreenSize() {}
|
|
|
|
checkKeyboard() {
|
|
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
|
final currentVisible = bottomHeight != 0;
|
|
|
|
// 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.checkScreenSize) {
|
|
checkScreenSize();
|
|
}
|
|
return Stack(children: [
|
|
Positioned(
|
|
top: _position.dy,
|
|
left: _position.dx,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
child: widget.builder(context, onPanUpdate))
|
|
]);
|
|
}
|
|
}
|
|
|
|
class IOSDraggable extends StatefulWidget {
|
|
const IOSDraggable(
|
|
{Key? key,
|
|
this.position = Offset.zero,
|
|
this.chatModel,
|
|
required this.width,
|
|
required this.height,
|
|
required this.builder})
|
|
: super(key: key);
|
|
|
|
final Offset position;
|
|
final ChatModel? chatModel;
|
|
final double width;
|
|
final double height;
|
|
final Widget Function(BuildContext) builder;
|
|
|
|
@override
|
|
IOSDraggableState createState() => IOSDraggableState();
|
|
}
|
|
|
|
class IOSDraggableState extends State<IOSDraggable> {
|
|
late Offset _position;
|
|
late ChatModel? _chatModel;
|
|
late double _width;
|
|
late double _height;
|
|
bool _keyboardVisible = false;
|
|
double _saveHeight = 0;
|
|
double _lastBottomHeight = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_position = widget.position;
|
|
_chatModel = widget.chatModel;
|
|
_width = widget.width;
|
|
_height = widget.height;
|
|
}
|
|
|
|
checkKeyboard() {
|
|
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
|
final currentVisible = bottomHeight != 0;
|
|
|
|
// 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 + _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) {
|
|
checkKeyboard();
|
|
return Stack(
|
|
children: [
|
|
Positioned(
|
|
left: _position.dx,
|
|
top: _position.dy,
|
|
child: GestureDetector(
|
|
onPanUpdate: (details) {
|
|
setState(() {
|
|
_position += details.delta;
|
|
});
|
|
_chatModel?.setChatWindowPosition(_position);
|
|
},
|
|
child: Material(
|
|
child: Container(
|
|
width: _width,
|
|
height: _height,
|
|
decoration:
|
|
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
|
child: widget.builder(context),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class QualityMonitor extends StatelessWidget {
|
|
final QualityMonitorModel qualityMonitorModel;
|
|
QualityMonitor(this.qualityMonitorModel);
|
|
|
|
Widget _row(String info, String? value, {Color? rightColor}) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 8,
|
|
child: AutoSizeText(info,
|
|
style: TextStyle(color: Color.fromARGB(255, 210, 210, 210)),
|
|
textAlign: TextAlign.right,
|
|
maxLines: 1)),
|
|
Spacer(flex: 1),
|
|
Expanded(
|
|
flex: 8,
|
|
child: AutoSizeText(value ?? '',
|
|
style: TextStyle(color: rightColor ?? Colors.white),
|
|
maxLines: 1)),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
|
value: qualityMonitorModel,
|
|
child: Consumer<QualityMonitorModel>(
|
|
builder: (context, qualityMonitorModel, child) => qualityMonitorModel
|
|
.show
|
|
? Container(
|
|
constraints: BoxConstraints(maxWidth: 200),
|
|
padding: const EdgeInsets.all(8),
|
|
color: MyTheme.canvasColor.withAlpha(150),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
|
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
|
_row(
|
|
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
|
|
rightColor: Colors.green),
|
|
_row("Target Bitrate",
|
|
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
|
_row(
|
|
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
|
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
|
],
|
|
),
|
|
)
|
|
: const SizedBox.shrink()));
|
|
}
|
|
|
|
class BlockableOverlayState extends OverlayKeyState {
|
|
final _middleBlocked = false.obs;
|
|
|
|
VoidCallback? onMiddleBlockedClick; // to-do use listener
|
|
|
|
RxBool get middleBlocked => _middleBlocked;
|
|
|
|
void addMiddleBlockedListener(void Function(bool) cb) {
|
|
_middleBlocked.listen(cb);
|
|
}
|
|
|
|
void setMiddleBlocked(bool blocked) {
|
|
if (blocked != _middleBlocked.value) {
|
|
_middleBlocked.value = blocked;
|
|
}
|
|
}
|
|
|
|
void applyFfi(FFI ffi) {
|
|
ffi.dialogManager.setOverlayState(this);
|
|
ffi.chatModel.setOverlayState(this);
|
|
// make remote page penetrable automatically, effective for chat over remote
|
|
onMiddleBlockedClick = () {
|
|
setMiddleBlocked(false);
|
|
};
|
|
}
|
|
}
|
|
|
|
class BlockableOverlay extends StatelessWidget {
|
|
final Widget underlying;
|
|
final List<OverlayEntry>? upperLayer;
|
|
|
|
final BlockableOverlayState state;
|
|
|
|
BlockableOverlay(
|
|
{required this.underlying, required this.state, this.upperLayer});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final initialEntries = [
|
|
OverlayEntry(builder: (_) => underlying),
|
|
|
|
/// middle layer
|
|
OverlayEntry(
|
|
builder: (context) => Obx(() => Listener(
|
|
onPointerDown: (_) {
|
|
state.onMiddleBlockedClick?.call();
|
|
},
|
|
child: Container(
|
|
color:
|
|
state.middleBlocked.value ? Colors.transparent : null)))),
|
|
];
|
|
|
|
if (upperLayer != null) {
|
|
initialEntries.addAll(upperLayer!);
|
|
}
|
|
|
|
/// set key
|
|
return Overlay(key: state.key, initialEntries: initialEntries);
|
|
}
|
|
}
|