mirror of
https://git.hmsn.ink/flutter/vnc_viewer.git
synced 2026-03-20 00:02:22 +09:00
first
This commit is contained in:
1
lib/flutter_rfb.dart
Normal file
1
lib/flutter_rfb.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'rfb/remote_frame_buffer_widget.dart';
|
||||
365
lib/main.dart
Normal file
365
lib/main.dart
Normal file
@@ -0,0 +1,365 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:vnc_viewer/flutter_rfb.dart';
|
||||
import 'package:vnc_viewer/rfb/remote_frame_buffer_isolate_messages.dart';
|
||||
import 'package:vnc_viewer/utils/winkey_blocker.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await windowManager.ensureInitialized();
|
||||
windowManager.waitUntilReadyToShow().then((_) async {
|
||||
// await windowManager.setSize(const Size(2560, 1440));
|
||||
await windowManager.setSize(const Size(1920, 1120));
|
||||
await windowManager.center();
|
||||
// await windowManager.setFullScreen(true);
|
||||
await windowManager.setTitle('Viewer');
|
||||
await windowManager.show();
|
||||
});
|
||||
|
||||
final logger = Logger(
|
||||
filter: ProductionFilter(),
|
||||
printer: PrettyPrinter()
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Viewer',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const MyHomePage(title: 'Viewer'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
bool _connected = false;
|
||||
bool _fullscreen = false;
|
||||
TextEditingController _host = TextEditingController(text: "hmsn.ink");
|
||||
TextEditingController _port = TextEditingController(text: "5910");
|
||||
TextEditingController _passwd = TextEditingController(text: "");
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CallbackShortcuts(
|
||||
bindings: <ShortcutActivator, VoidCallback>{
|
||||
const SingleActivator(LogicalKeyboardKey.f11): () {
|
||||
setState(() {
|
||||
_fullscreen = !_fullscreen;
|
||||
});
|
||||
windowManager.setFullScreen(_fullscreen);
|
||||
},
|
||||
},
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
child: Scaffold(
|
||||
body: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
..._connected
|
||||
? <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: InteractiveViewer(
|
||||
constrained: true,
|
||||
maxScale: 1,
|
||||
minScale: 1,
|
||||
child: Builder(
|
||||
builder: (final BuildContext context) =>
|
||||
RemoteFrameBufferWidget(
|
||||
hostName: _host.text,
|
||||
port: int.parse(_port.text),
|
||||
isFullscreen: _fullscreen,
|
||||
onError: (final Object error) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $error'),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_connected = false;
|
||||
});
|
||||
},
|
||||
password: _passwd.text,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
Container(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/background.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
left: 30,
|
||||
width: 80,
|
||||
height: 200,
|
||||
child: FadeInUp(
|
||||
duration: Duration(seconds: 1),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'assets/images/light-1.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 140,
|
||||
width: 80,
|
||||
height: 150,
|
||||
child: FadeInUp(
|
||||
duration: Duration(milliseconds: 1200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'assets/images/light-2.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 40,
|
||||
top: 40,
|
||||
width: 80,
|
||||
height: 150,
|
||||
child: FadeInUp(
|
||||
duration: Duration(milliseconds: 1300),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'assets/images/clock.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
child: FadeInUp(
|
||||
duration: Duration(milliseconds: 1600),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 50),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Vnc",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 530.0, right: 530.0, top: 30.0, bottom: 30.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
FadeInUp(
|
||||
duration: Duration(milliseconds: 1800),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Color.fromRGBO(143, 148, 251, 1),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromRGBO(
|
||||
143,
|
||||
148,
|
||||
251,
|
||||
.2,
|
||||
),
|
||||
blurRadius: 20.0,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color.fromRGBO(
|
||||
143,
|
||||
148,
|
||||
251,
|
||||
1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _host,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Host",
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color.fromRGBO(
|
||||
143,
|
||||
148,
|
||||
251,
|
||||
1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _port,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Port",
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: _passwd,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Password",
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
FadeInUp(
|
||||
duration: Duration(milliseconds: 1900),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_connected = true;
|
||||
windowManager.setTitle("Desktop");
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color.fromRGBO(143, 148, 251, 1),
|
||||
Color.fromRGBO(143, 148, 251, .6),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"접속",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
lib/rfb/child_size_notifier_widget.dart
Normal file
33
lib/rfb/child_size_notifier_widget.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Widget that exposes its child's size via a [ValueNotifier].
|
||||
///
|
||||
/// Inspired by: https://stackoverflow.com/a/58004112/373138
|
||||
class SizeTrackingWidget extends StatefulWidget {
|
||||
final Widget _child;
|
||||
final ValueNotifier<Size> _sizeValueNotifier;
|
||||
|
||||
const SizeTrackingWidget({
|
||||
super.key,
|
||||
required final ValueNotifier<Size> sizeValueNotifier,
|
||||
required final Widget child,
|
||||
}) : _child = child,
|
||||
_sizeValueNotifier = sizeValueNotifier;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SizeTackingState();
|
||||
}
|
||||
|
||||
class _SizeTackingState extends State<SizeTrackingWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((final _) {
|
||||
widget._sizeValueNotifier.value =
|
||||
(context.findRenderObject() as RenderBox?)!.size;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => widget._child;
|
||||
}
|
||||
81
lib/rfb/extensions/logical_keyboard_key_extensions.dart
Normal file
81
lib/rfb/extensions/logical_keyboard_key_extensions.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Useful extensions for the [LogicalKeyboardKey] class.
|
||||
extension LogicalKeyboardKeyExtensions on LogicalKeyboardKey {
|
||||
/// Convert this logical key to the corresponding X window system key code.
|
||||
///
|
||||
/// See: https://www.rfc-editor.org/rfc/rfc6143.html#section-7.5.4
|
||||
int asXWindowSystemKey() {
|
||||
if (this == LogicalKeyboardKey.backspace) {
|
||||
return 0xff08;
|
||||
} else if (this == LogicalKeyboardKey.tab) {
|
||||
return 0xff09;
|
||||
} else if (this == LogicalKeyboardKey.enter) {
|
||||
return 0xff0d;
|
||||
} else if (this == LogicalKeyboardKey.escape) {
|
||||
return 0xff1b;
|
||||
} else if (this == LogicalKeyboardKey.insert) {
|
||||
return 0xff63;
|
||||
} else if (this == LogicalKeyboardKey.delete) {
|
||||
return 0xffff;
|
||||
} else if (this == LogicalKeyboardKey.home) {
|
||||
return 0xff50;
|
||||
} else if (this == LogicalKeyboardKey.end) {
|
||||
return 0xff57;
|
||||
} else if (this == LogicalKeyboardKey.pageUp) {
|
||||
return 0xff55;
|
||||
} else if (this == LogicalKeyboardKey.pageDown) {
|
||||
return 0xff56;
|
||||
} else if (this == LogicalKeyboardKey.arrowLeft) {
|
||||
return 0xff51;
|
||||
} else if (this == LogicalKeyboardKey.arrowUp) {
|
||||
return 0xff52;
|
||||
} else if (this == LogicalKeyboardKey.arrowRight) {
|
||||
return 0xff53;
|
||||
} else if (this == LogicalKeyboardKey.arrowDown) {
|
||||
return 0xff54;
|
||||
} else if (this == LogicalKeyboardKey.f1) {
|
||||
return 0xffbe;
|
||||
} else if (this == LogicalKeyboardKey.f2) {
|
||||
return 0xffbf;
|
||||
} else if (this == LogicalKeyboardKey.f3) {
|
||||
return 0xffc0;
|
||||
} else if (this == LogicalKeyboardKey.f4) {
|
||||
return 0xffc1;
|
||||
} else if (this == LogicalKeyboardKey.f5) {
|
||||
return 0xffc2;
|
||||
} else if (this == LogicalKeyboardKey.f6) {
|
||||
return 0xffc3;
|
||||
} else if (this == LogicalKeyboardKey.f7) {
|
||||
return 0xffc4;
|
||||
} else if (this == LogicalKeyboardKey.f8) {
|
||||
return 0xffc5;
|
||||
} else if (this == LogicalKeyboardKey.f9) {
|
||||
return 0xffc6;
|
||||
} else if (this == LogicalKeyboardKey.f10) {
|
||||
return 0xffc7;
|
||||
} else if (this == LogicalKeyboardKey.f11) {
|
||||
return 0xffc8;
|
||||
} else if (this == LogicalKeyboardKey.f12) {
|
||||
return 0xffc9;
|
||||
} else if (this == LogicalKeyboardKey.shiftLeft) {
|
||||
return 0xffe1;
|
||||
} else if (this == LogicalKeyboardKey.shiftRight) {
|
||||
return 0xffe2;
|
||||
} else if (this == LogicalKeyboardKey.controlLeft) {
|
||||
return 0xffe3;
|
||||
} else if (this == LogicalKeyboardKey.controlRight) {
|
||||
return 0xffe4;
|
||||
} else if (this == LogicalKeyboardKey.metaLeft) {
|
||||
return 0xffe7;
|
||||
} else if (this == LogicalKeyboardKey.metaRight) {
|
||||
return 0xffe8;
|
||||
} else if (this == LogicalKeyboardKey.altLeft) {
|
||||
return 0xffe9;
|
||||
} else if (this == LogicalKeyboardKey.altRight) {
|
||||
return 0xffea;
|
||||
} else {
|
||||
return keyId;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
lib/rfb/remote_frame_buffer_Listener_detector.dart
Normal file
78
lib/rfb/remote_frame_buffer_Listener_detector.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart' hide Image;
|
||||
import 'remote_frame_buffer_isolate_messages.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
class RemoteFrameBufferListenerDetector extends Listener {
|
||||
final Option<SendPort> _sendPort;
|
||||
|
||||
RemoteFrameBufferListenerDetector({
|
||||
super.key,
|
||||
required final Option<SendPort> sendPort,
|
||||
super.child,
|
||||
}) : _sendPort = sendPort;
|
||||
|
||||
@override
|
||||
PointerSignalEventListener? get onPointerSignal =>
|
||||
(final PointerSignalEvent details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => details is PointerScrollEvent ? onScroll(sendPort, details): null,
|
||||
);
|
||||
|
||||
void onScroll(SendPort sendPort, PointerScrollEvent event) {
|
||||
|
||||
final int x = event.localPosition.dx.toInt();
|
||||
final int y = event.localPosition.dy.toInt();
|
||||
int buttonMask = 0;
|
||||
|
||||
if (event.scrollDelta.dy < 0) {
|
||||
sendPort.send(RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: true,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: x,
|
||||
y: y,
|
||||
));
|
||||
} else if (event.scrollDelta.dy > 0) {
|
||||
sendPort.send(RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: true,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: x,
|
||||
y: y,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future.microtask(() {
|
||||
sendPort.send(RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: x,
|
||||
y: y,
|
||||
));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
93
lib/rfb/remote_frame_buffer_client_isolate.dart
Normal file
93
lib/rfb/remote_frame_buffer_client_isolate.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:dart_rfb/dart_rfb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'remote_frame_buffer_isolate_messages.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
/// The isolate entry point for running the RFB client in the background.
|
||||
///
|
||||
/// [initMessage] contains the [SendPort] for communicating with the caller.
|
||||
/// It also contains the hostname and port of the server.
|
||||
Future<void> startRemoteFrameBufferClient(
|
||||
final RemoteFrameBufferIsolateInitMessage initMessage,
|
||||
) async {
|
||||
Logger.root
|
||||
..level = Level.WARNING
|
||||
..onRecord.listen(
|
||||
(final LogRecord logRecord) {
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'${logRecord.level} ${logRecord.loggerName}: ${logRecord.message}',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
final RemoteFrameBufferClient client = RemoteFrameBufferClient();
|
||||
final ReceivePort receivePort = ReceivePort();
|
||||
client.updateStream.listen(
|
||||
(final RemoteFrameBufferClientUpdate update) {
|
||||
initMessage.sendPort.send(
|
||||
RemoteFrameBufferIsolateReceiveMessage.frameBufferUpdate(
|
||||
frameBufferHeight: client.config
|
||||
.map((final Config config) => config.frameBufferHeight)
|
||||
.getOrElse(() => 0),
|
||||
frameBufferWidth: client.config
|
||||
.map((final Config config) => config.frameBufferWidth)
|
||||
.getOrElse(() => 0),
|
||||
sendPort: receivePort.sendPort,
|
||||
update: update,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
client.serverClipBoardStream.listen(
|
||||
(final String text) => initMessage.sendPort.send(
|
||||
RemoteFrameBufferIsolateReceiveMessage.clipBoardUpdate(text: text),
|
||||
),
|
||||
);
|
||||
receivePort
|
||||
.whereType<RemoteFrameBufferIsolateSendMessage>()
|
||||
.listen((final RemoteFrameBufferIsolateSendMessage message) {
|
||||
message.map(
|
||||
clipBoardUpdate:
|
||||
(final RemoteFrameBufferIsolateSendMessageClipBoardUpdate update) =>
|
||||
client.sendClientCutText(text: update.text),
|
||||
keyEvent: (final RemoteFrameBufferIsolateSendMessageKeyEvent keyEvent) =>
|
||||
client.sendKeyEvent(
|
||||
keyEvent: RemoteFrameBufferClientKeyEvent(
|
||||
down: keyEvent.down,
|
||||
key: keyEvent.key,
|
||||
),
|
||||
),
|
||||
pointerEvent: (
|
||||
final RemoteFrameBufferIsolateSendMessagePointerEvent pointerEvent,
|
||||
) =>
|
||||
client.sendPointerEvent(
|
||||
pointerEvent: RemoteFrameBufferClientPointerEvent(
|
||||
button1Down: pointerEvent.button1Down,
|
||||
button2Down: pointerEvent.button2Down,
|
||||
button3Down: pointerEvent.button3Down,
|
||||
button4Down: pointerEvent.button4Down,
|
||||
button5Down: pointerEvent.button5Down,
|
||||
button6Down: pointerEvent.button6Down,
|
||||
button7Down: pointerEvent.button7Down,
|
||||
button8Down: pointerEvent.button8Down,
|
||||
x: pointerEvent.x,
|
||||
y: pointerEvent.y,
|
||||
),
|
||||
),
|
||||
frameBufferUpdateRequest: (final _) => client.requestUpdate(),
|
||||
);
|
||||
});
|
||||
await client.connect(
|
||||
hostname: initMessage.hostName,
|
||||
password: initMessage.password.toNullable(),
|
||||
port: initMessage.port,
|
||||
);
|
||||
client
|
||||
..handleIncomingMessages()
|
||||
..requestUpdate();
|
||||
}
|
||||
129
lib/rfb/remote_frame_buffer_gesture_detector.dart
Normal file
129
lib/rfb/remote_frame_buffer_gesture_detector.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/widgets.dart' hide Image;
|
||||
import 'remote_frame_buffer_isolate_messages.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
class RemoteFrameBufferGestureDetector extends GestureDetector {
|
||||
final Image _image;
|
||||
final Size _remoteFrameBufferWidgetSize;
|
||||
final Option<SendPort> _sendPort;
|
||||
final bool _isFullscreen;
|
||||
|
||||
RemoteFrameBufferGestureDetector({
|
||||
super.key,
|
||||
required final Image image,
|
||||
required final Size remoteFrameBufferWidgetSize,
|
||||
required final Option<SendPort> sendPort,
|
||||
required final bool isFullscreen,
|
||||
super.child,
|
||||
}) : _image = image,
|
||||
_remoteFrameBufferWidgetSize = remoteFrameBufferWidgetSize,
|
||||
_sendPort = sendPort,
|
||||
_isFullscreen = isFullscreen;
|
||||
|
||||
@override
|
||||
GestureTapDownCallback? get onSecondaryTapDown =>
|
||||
(final TapDownDetails details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: true,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: _isFullscreen ? details.localPosition.dx.toInt() : (details.localPosition.dx /
|
||||
_remoteFrameBufferWidgetSize.width *
|
||||
_image.width)
|
||||
.toInt(),
|
||||
y: _isFullscreen ? details.localPosition.dy.toInt() : (details.localPosition.dy /
|
||||
_remoteFrameBufferWidgetSize.height *
|
||||
_image.height)
|
||||
.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
GestureTapUpCallback? get onSecondaryTapUp =>
|
||||
(final TapUpDetails details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: _isFullscreen ? details.localPosition.dx.toInt() : (details.localPosition.dx /
|
||||
_remoteFrameBufferWidgetSize.width *
|
||||
_image.width)
|
||||
.toInt(),
|
||||
y: _isFullscreen ? details.localPosition.dy.toInt() : (details.localPosition.dy /
|
||||
_remoteFrameBufferWidgetSize.height *
|
||||
_image.height)
|
||||
.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
GestureTapDownCallback? get onTapDown =>
|
||||
(final TapDownDetails details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: true,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: _isFullscreen ? details.localPosition.dx.toInt() : (details.localPosition.dx /
|
||||
_remoteFrameBufferWidgetSize.width *
|
||||
_image.width)
|
||||
.toInt(),
|
||||
y: _isFullscreen ? details.localPosition.dy.toInt() : (details.localPosition.dy /
|
||||
_remoteFrameBufferWidgetSize.height *
|
||||
_image.height)
|
||||
.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
GestureTapUpCallback? get onTapUp =>
|
||||
(final TapUpDetails details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: _isFullscreen ? details.localPosition.dx.toInt() : (details.localPosition.dx /
|
||||
_remoteFrameBufferWidgetSize.width *
|
||||
_image.width)
|
||||
.toInt(),
|
||||
y: _isFullscreen ? details.localPosition.dy.toInt() : (details.localPosition.dy /
|
||||
_remoteFrameBufferWidgetSize.height *
|
||||
_image.height)
|
||||
.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
73
lib/rfb/remote_frame_buffer_isolate_messages.dart
Normal file
73
lib/rfb/remote_frame_buffer_isolate_messages.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:dart_rfb/dart_rfb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'remote_frame_buffer_isolate_messages.freezed.dart';
|
||||
|
||||
/// The initialization message sent when creating the isolate.
|
||||
@freezed
|
||||
class RemoteFrameBufferIsolateInitMessage
|
||||
with _$RemoteFrameBufferIsolateInitMessage {
|
||||
const factory RemoteFrameBufferIsolateInitMessage({
|
||||
required final String hostName,
|
||||
required final Option<String> password,
|
||||
required final int port,
|
||||
|
||||
/// The [SendPort] used for communicating with the caller.
|
||||
required final SendPort sendPort,
|
||||
}) = _RemoteFrameBufferIsolateInitMessage;
|
||||
}
|
||||
|
||||
/// A message that providers a received client update to the caller.
|
||||
@freezed
|
||||
class RemoteFrameBufferIsolateReceiveMessage
|
||||
with _$RemoteFrameBufferIsolateReceiveMessage {
|
||||
const factory RemoteFrameBufferIsolateReceiveMessage.clipBoardUpdate({
|
||||
required final String text,
|
||||
}) = RemoteFrameBufferIsolateReceiveMessageClipBoardUpdate;
|
||||
|
||||
const factory RemoteFrameBufferIsolateReceiveMessage.frameBufferUpdate({
|
||||
required final int frameBufferHeight,
|
||||
required final int frameBufferWidth,
|
||||
required final SendPort sendPort,
|
||||
required final RemoteFrameBufferClientUpdate update,
|
||||
}) = RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate;
|
||||
}
|
||||
|
||||
/// A message that is sent to the isolate.
|
||||
@freezed
|
||||
class RemoteFrameBufferIsolateSendMessage
|
||||
with _$RemoteFrameBufferIsolateSendMessage {
|
||||
/// A message that is sent when the client's clipboard is updated.
|
||||
const factory RemoteFrameBufferIsolateSendMessage.clipBoardUpdate({
|
||||
required final String text,
|
||||
}) = RemoteFrameBufferIsolateSendMessageClipBoardUpdate;
|
||||
|
||||
/// A message that is sent when a key is pressed.
|
||||
const factory RemoteFrameBufferIsolateSendMessage.keyEvent({
|
||||
required final bool down,
|
||||
required final int key,
|
||||
}) = RemoteFrameBufferIsolateSendMessageKeyEvent;
|
||||
|
||||
/// A message that represents the state of all pointer buttons and coordinates.
|
||||
const factory RemoteFrameBufferIsolateSendMessage.pointerEvent({
|
||||
required final bool button1Down,
|
||||
required final bool button2Down,
|
||||
required final bool button3Down,
|
||||
required final bool button4Down,
|
||||
required final bool button5Down,
|
||||
required final bool button6Down,
|
||||
required final bool button7Down,
|
||||
required final bool button8Down,
|
||||
required final int x,
|
||||
required final int y,
|
||||
}) = RemoteFrameBufferIsolateSendMessagePointerEvent;
|
||||
|
||||
/// A message that indicates that the client wants to issue a new update
|
||||
/// request.
|
||||
const factory RemoteFrameBufferIsolateSendMessage.frameBufferUpdateRequest() =
|
||||
RemoteFrameBufferIsolateSendMessageUpdateRequest;
|
||||
}
|
||||
1832
lib/rfb/remote_frame_buffer_isolate_messages.freezed.dart
Normal file
1832
lib/rfb/remote_frame_buffer_isolate_messages.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
101
lib/rfb/remote_frame_buffer_mouse_detector.dart
Normal file
101
lib/rfb/remote_frame_buffer_mouse_detector.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart' hide Image;
|
||||
import 'remote_frame_buffer_isolate_messages.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
class RemoteFrameBufferMouseDetector extends MouseRegion {
|
||||
final Image _image;
|
||||
final Size _remoteFrameBufferWidgetSize;
|
||||
final Option<SendPort> _sendPort;
|
||||
|
||||
RemoteFrameBufferMouseDetector({
|
||||
super.key,
|
||||
required final Image image,
|
||||
required final Size remoteFrameBufferWidgetSize,
|
||||
required final Option<SendPort> sendPort,
|
||||
super.child,
|
||||
}) : _image = image,
|
||||
_remoteFrameBufferWidgetSize = remoteFrameBufferWidgetSize,
|
||||
_sendPort = sendPort;
|
||||
|
||||
// @override
|
||||
// PointerHoverEventListener? get onHover =>
|
||||
// (final PointerEvent details) => _sendPort.match(
|
||||
// () {},
|
||||
// (final SendPort sendPort) => sendPort.send(
|
||||
// RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
// button1Down: false,
|
||||
// button2Down: false,
|
||||
// button3Down: false,
|
||||
// button4Down: false,
|
||||
// button5Down: false,
|
||||
// button6Down: false,
|
||||
// button7Down: false,
|
||||
// button8Down: false,
|
||||
// x: (details.localPosition.dx /
|
||||
// _remoteFrameBufferWidgetSize.width *
|
||||
// _image.width)
|
||||
// .toInt(),
|
||||
// y: (details.localPosition.dy /
|
||||
// _remoteFrameBufferWidgetSize.height *
|
||||
// _image.height)
|
||||
// .toInt(),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
@override
|
||||
PointerEnterEventListener? get onEnter =>
|
||||
(final PointerEnterEvent details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: true,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: (details.localPosition.dx /
|
||||
_remoteFrameBufferWidgetSize.width *
|
||||
_image.width)
|
||||
.toInt(),
|
||||
y: (details.localPosition.dy /
|
||||
_remoteFrameBufferWidgetSize.height *
|
||||
_image.height)
|
||||
.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
PointerExitEventListener? get onExit =>
|
||||
(final PointerExitEvent details) => _sendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.pointerEvent(
|
||||
button1Down: false,
|
||||
button2Down: false,
|
||||
button3Down: false,
|
||||
button4Down: false,
|
||||
button5Down: false,
|
||||
button6Down: false,
|
||||
button7Down: false,
|
||||
button8Down: false,
|
||||
x: (details.localPosition.dx /
|
||||
_remoteFrameBufferWidgetSize.width *
|
||||
_image.width)
|
||||
.toInt(),
|
||||
y: (details.localPosition.dy /
|
||||
_remoteFrameBufferWidgetSize.height *
|
||||
_image.height)
|
||||
.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
456
lib/rfb/remote_frame_buffer_widget.dart
Normal file
456
lib/rfb/remote_frame_buffer_widget.dart
Normal file
@@ -0,0 +1,456 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dart_rfb/dart_rfb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vnc_viewer/rfb/remote_frame_buffer_Listener_detector.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'child_size_notifier_widget.dart';
|
||||
import 'extensions/logical_keyboard_key_extensions.dart';
|
||||
import 'remote_frame_buffer_client_isolate.dart';
|
||||
import 'remote_frame_buffer_gesture_detector.dart';
|
||||
import 'remote_frame_buffer_isolate_messages.dart';
|
||||
import 'package:fpdart/fpdart.dart' hide State;
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final Logger _logger = Logger('RemoteFrameBufferWidget');
|
||||
|
||||
/// This widget displays the framebuffer associated with the RFB session.
|
||||
/// On creation, it tries to establish a connection with the remote server
|
||||
/// in an isolate. On success, it runs the read loop in that isolate.
|
||||
class RemoteFrameBufferWidget extends StatefulWidget {
|
||||
final Option<Widget> _connectingWidget;
|
||||
final String _hostName;
|
||||
final Option<void Function(Object error)> _onError;
|
||||
final Option<String> _password;
|
||||
final int _port;
|
||||
final bool _isFullscreen;
|
||||
|
||||
/// Immediately tries to establish a connection to a remote server at
|
||||
/// [hostName]:[port], optionally using [password].
|
||||
RemoteFrameBufferWidget({
|
||||
super.key,
|
||||
final Widget? connectingWidget,
|
||||
required final String hostName,
|
||||
final void Function(Object error)? onError,
|
||||
final String? password,
|
||||
final int port = 5900,
|
||||
required final bool isFullscreen,
|
||||
}) : _connectingWidget = optionOf(connectingWidget),
|
||||
_hostName = hostName,
|
||||
_onError = optionOf(onError),
|
||||
_password = optionOf(password),
|
||||
_port = port,
|
||||
_isFullscreen = isFullscreen;
|
||||
|
||||
@override
|
||||
State<RemoteFrameBufferWidget> createState() =>
|
||||
RemoteFrameBufferWidgetState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class RemoteFrameBufferWidgetState extends State<RemoteFrameBufferWidget> {
|
||||
late Timer _clipBoardMonitorTimer;
|
||||
Option<ByteData> _frameBuffer = none();
|
||||
Option<Image> _image = none();
|
||||
Option<Isolate> _isolate = none();
|
||||
Option<SendPort> _isolateSendPort = none();
|
||||
final ValueNotifier<Size> _sizeValueNotifier = ValueNotifier<Size>(Size.zero);
|
||||
Option<StreamSubscription<Object?>> _streamSubscription = none();
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => _frameBuffer
|
||||
.flatMap(
|
||||
(final ByteData frameBuffer) => frameBuffer.buffer
|
||||
.asUint8List(
|
||||
frameBuffer.offsetInBytes,
|
||||
frameBuffer.lengthInBytes,
|
||||
)
|
||||
.where((final int byte) => byte != 0)
|
||||
.isNotEmpty
|
||||
? _image
|
||||
: none<Image>(),
|
||||
)
|
||||
.match(
|
||||
_buildConnecting,
|
||||
(final Image image) => _buildImage(image: image),
|
||||
);
|
||||
|
||||
bool _rawKeyEventListener(KeyEvent keyEvent) {
|
||||
|
||||
if(keyEvent is! KeyDownEvent && keyEvent is! KeyUpEvent ) {
|
||||
return false;
|
||||
}
|
||||
if(keyEvent.logicalKey == LogicalKeyboardKey.f11) {
|
||||
if(keyEvent is KeyDownEvent) {
|
||||
windowManager.isFullScreen().then((flag) {windowManager.setFullScreen(!flag);});
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
_isolateSendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.keyEvent(
|
||||
down: keyEvent is KeyDownEvent,
|
||||
key: keyEvent.logicalKey.asXWindowSystemKey(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_clipBoardMonitorTimer.cancel();
|
||||
_streamSubscription.match(
|
||||
() {},
|
||||
(final StreamSubscription<Object?> subscription) =>
|
||||
unawaited(subscription.cancel()),
|
||||
);
|
||||
_image.match(
|
||||
() {},
|
||||
(final Image image) => image.dispose(),
|
||||
);
|
||||
_isolate.match(
|
||||
() {},
|
||||
(final Isolate isolate) => isolate.kill(),
|
||||
);
|
||||
HardwareKeyboard.instance.removeHandler(_rawKeyEventListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_monitorClipBoard();
|
||||
HardwareKeyboard.instance.addHandler(_rawKeyEventListener);
|
||||
unawaited(_initAsync());
|
||||
VncKeyboardHandler();
|
||||
}
|
||||
|
||||
Widget _buildConnecting() => widget._connectingWidget.getOrElse(
|
||||
() => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
SizeTrackingWidget _buildImage({required final Image image}) =>
|
||||
SizeTrackingWidget(
|
||||
sizeValueNotifier: _sizeValueNotifier,
|
||||
child: RemoteFrameBufferListenerDetector(
|
||||
sendPort: _isolateSendPort,
|
||||
child: RemoteFrameBufferGestureDetector(
|
||||
image: image,
|
||||
remoteFrameBufferWidgetSize: MediaQuery.of(context).size,
|
||||
sendPort: _isolateSendPort,
|
||||
isFullscreen: widget._isFullscreen,
|
||||
child: RawImage(image: image, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height,),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void _decodeAndUpdateImage({
|
||||
required final ByteData frameBuffer,
|
||||
required final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate
|
||||
message,
|
||||
}) =>
|
||||
decodeImageFromPixels(
|
||||
frameBuffer.buffer.asUint8List(),
|
||||
message.frameBufferWidth,
|
||||
message.frameBufferHeight,
|
||||
PixelFormat.bgra8888,
|
||||
(final Image result) {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() {
|
||||
_image.match(
|
||||
() {},
|
||||
(final Image image) => image.dispose(),
|
||||
);
|
||||
_image = some(result);
|
||||
},
|
||||
);
|
||||
_isolateSendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) => sendPort.send(
|
||||
const RemoteFrameBufferIsolateSendMessage
|
||||
.frameBufferUpdateRequest(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
void _decodeAndResizeImage({
|
||||
required ByteData frameBuffer,
|
||||
required int srcWidth,
|
||||
required int srcHeight,
|
||||
required int dstWidth,
|
||||
required int dstHeight,
|
||||
}) {
|
||||
decodeImageFromPixels(
|
||||
frameBuffer.buffer.asUint8List(),
|
||||
srcWidth,
|
||||
srcHeight,
|
||||
PixelFormat.bgra8888,
|
||||
(Image originalImage) {
|
||||
final pictureRecorder = PictureRecorder();
|
||||
final canvas = Canvas(pictureRecorder);
|
||||
final paint = Paint()..filterQuality = FilterQuality.low;
|
||||
|
||||
// 축소해서 그리기
|
||||
canvas.drawImageRect(
|
||||
originalImage,
|
||||
Rect.fromLTWH(0, 0, srcWidth.toDouble(), srcHeight.toDouble()),
|
||||
Rect.fromLTWH(0, 0, dstWidth.toDouble(), dstHeight.toDouble()),
|
||||
paint,
|
||||
);
|
||||
|
||||
final picture = pictureRecorder.endRecording();
|
||||
picture.toImage(dstWidth, dstHeight).then((resizedImage) {
|
||||
originalImage.dispose(); // 메모리 해제
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_image.match(() {}, (img) => img.dispose());
|
||||
_image = some(resizedImage);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Task<void> _handleFrameBufferUpdateMessage({
|
||||
required final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate
|
||||
update,
|
||||
}) =>
|
||||
Task<void>(() async {
|
||||
_logger.finer(
|
||||
'Received new update message with ${update.update.rectangles.length} rectangles',
|
||||
);
|
||||
_isolateSendPort = some(update.sendPort);
|
||||
if (_frameBuffer.isNone()) {
|
||||
_frameBuffer = some(
|
||||
ByteData(
|
||||
update.frameBufferHeight * update.frameBufferWidth * 4,
|
||||
),
|
||||
);
|
||||
}
|
||||
unawaited(
|
||||
_frameBuffer.match(
|
||||
() async {},
|
||||
(final ByteData frameBuffer) async {
|
||||
for (final RemoteFrameBufferClientUpdateRectangle rectangle
|
||||
in update.update.rectangles) {
|
||||
await rectangle.encodingType.when(
|
||||
copyRect: () async {
|
||||
final int sourceX = rectangle.byteData.getUint16(0);
|
||||
final int sourceY = rectangle.byteData.getUint16(2);
|
||||
final BytesBuilder bytesBuilder = BytesBuilder();
|
||||
for (int row = 0; row < rectangle.height; row++) {
|
||||
for (int column = 0; column < rectangle.width; column++) {
|
||||
bytesBuilder.add(
|
||||
frameBuffer.buffer.asUint8List(
|
||||
((sourceY + row) * update.frameBufferWidth +
|
||||
sourceX +
|
||||
column) *
|
||||
4,
|
||||
4,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return (await updateFrameBuffer(
|
||||
frameBuffer: frameBuffer,
|
||||
frameBufferSize: Size(
|
||||
update.frameBufferWidth.toDouble(),
|
||||
update.frameBufferHeight.toDouble(),
|
||||
),
|
||||
rectangle: rectangle.copyWith(
|
||||
encodingType: const RemoteFrameBufferEncodingType.copyRect(),
|
||||
byteData: ByteData.sublistView(
|
||||
bytesBuilder.toBytes(),
|
||||
),
|
||||
),
|
||||
).run())
|
||||
.match(
|
||||
(final Object error) =>
|
||||
// ignore: avoid_print
|
||||
print('Error updating frame buffer: $error'),
|
||||
(final _) {},
|
||||
);
|
||||
},
|
||||
raw: () async => (await updateFrameBuffer(
|
||||
frameBuffer: frameBuffer,
|
||||
frameBufferSize: Size(
|
||||
update.frameBufferWidth.toDouble(),
|
||||
update.frameBufferHeight.toDouble(),
|
||||
),
|
||||
rectangle: rectangle,
|
||||
).run())
|
||||
.match(
|
||||
(final Object error) =>
|
||||
// ignore: avoid_print
|
||||
print('Error updating frame buffer: $error'),
|
||||
(final _) {},
|
||||
),
|
||||
unsupported: (final ByteData bytes) async {},
|
||||
);
|
||||
}
|
||||
_decodeAndUpdateImage(
|
||||
frameBuffer: frameBuffer,
|
||||
message: update,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
/// Initializes logic that requires to be run asynchronous.
|
||||
Future<void> _initAsync() async {
|
||||
final ReceivePort receivePort = ReceivePort();
|
||||
_streamSubscription = some(
|
||||
receivePort.listen(
|
||||
(final Object? message) {
|
||||
// Error, first is error, second is stacktrace or null
|
||||
if (message is List) {
|
||||
widget._onError.match(
|
||||
() {},
|
||||
(final void Function(Object error) onError) =>
|
||||
onError(message.first),
|
||||
);
|
||||
} else if (message is RemoteFrameBufferIsolateReceiveMessage) {
|
||||
message.map(
|
||||
clipBoardUpdate: (
|
||||
final RemoteFrameBufferIsolateReceiveMessageClipBoardUpdate
|
||||
update,
|
||||
) =>
|
||||
Clipboard.setData(ClipboardData(text: update.text)),
|
||||
frameBufferUpdate: (
|
||||
final RemoteFrameBufferIsolateReceiveMessageFrameBufferUpdate
|
||||
update,
|
||||
) {
|
||||
_handleFrameBufferUpdateMessage(update: update).run();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
_logger.info('Spawning new isolate for RFB client');
|
||||
_isolate = some(
|
||||
await Isolate.spawn(
|
||||
startRemoteFrameBufferClient,
|
||||
RemoteFrameBufferIsolateInitMessage(
|
||||
hostName: widget._hostName,
|
||||
password: widget._password,
|
||||
port: widget._port,
|
||||
sendPort: receivePort.sendPort,
|
||||
),
|
||||
onError: receivePort.sendPort,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _monitorClipBoard() {
|
||||
Option<String> lastClipBoardContent = none();
|
||||
_clipBoardMonitorTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(final _) async {
|
||||
optionOf(await Clipboard.getData(Clipboard.kTextPlain))
|
||||
.flatMap((final ClipboardData data) => optionOf(data.text))
|
||||
.filter(
|
||||
(final String text) => lastClipBoardContent.match(
|
||||
() => true,
|
||||
(final String lastClipBoardContent) =>
|
||||
lastClipBoardContent != text,
|
||||
),
|
||||
)
|
||||
.match(
|
||||
() {},
|
||||
(final String text) => _isolateSendPort.match(
|
||||
() {},
|
||||
(final SendPort sendPort) {
|
||||
lastClipBoardContent = some(text);
|
||||
sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.clipBoardUpdate(
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates [frameBuffer] with the given [rectangle]s.
|
||||
@visibleForTesting
|
||||
static TaskEither<Object, void> updateFrameBuffer({
|
||||
required final ByteData frameBuffer,
|
||||
required final Size frameBufferSize,
|
||||
required final RemoteFrameBufferClientUpdateRectangle rectangle,
|
||||
}) =>
|
||||
TaskEither<Object, void>.tryCatch(
|
||||
() async {
|
||||
for (int y = 0; y < rectangle.height; y++) {
|
||||
for (int x = 0; x < rectangle.width; x++) {
|
||||
final int frameBufferX = rectangle.x + x;
|
||||
final int frameBufferY = rectangle.y + y;
|
||||
final int pixelBytes =
|
||||
rectangle.byteData.getUint32((y * rectangle.width + x) * 4);
|
||||
frameBuffer.setUint32(
|
||||
((frameBufferY * frameBufferSize.width + frameBufferX) * 4)
|
||||
.toInt(),
|
||||
pixelBytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(final Object error, final _) => error,
|
||||
);
|
||||
|
||||
|
||||
final MethodChannel _channel = const MethodChannel('com.example.vnc_viewer/keyboard');
|
||||
|
||||
void VncKeyboardHandler() {
|
||||
_channel.setMethodCallHandler(_handleMethod);
|
||||
}
|
||||
|
||||
Map<dynamic, dynamic> prev = {};
|
||||
Future<void> _handleMethod(MethodCall call) async {
|
||||
|
||||
if (call.method == 'onPhysicalKeyEvent') {
|
||||
final args = call.arguments as Map<dynamic, dynamic>;
|
||||
final bool down = args['down'];
|
||||
final int key = args['key'];
|
||||
final int vkCode = args['vkCode'];
|
||||
_sendToVnc(key, vkCode, down);
|
||||
}
|
||||
}
|
||||
|
||||
void _sendToVnc(int key, int vkCode, bool down) {
|
||||
_isolateSendPort.match(
|
||||
() {},
|
||||
(SendPort sendPort) {
|
||||
sendPort.send(
|
||||
RemoteFrameBufferIsolateSendMessage.keyEvent(
|
||||
down: down,
|
||||
key: key,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
166
lib/utils/winkey_blocker.dart
Normal file
166
lib/utils/winkey_blocker.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
// // lib/utils/winkey_blocker.dart
|
||||
// import 'dart:ffi';
|
||||
// import 'dart:io';
|
||||
// import 'package:ffi/ffi.dart';
|
||||
//
|
||||
// // Windows API 상수
|
||||
// const int WH_KEYBOARD_LL = 13;
|
||||
// const int HC_ACTION = 0;
|
||||
// const int WM_KEYDOWN = 0x0100;
|
||||
// const int WM_SYSKEYDOWN = 0x0104;
|
||||
//
|
||||
// const int VK_LWIN = 0x5B; // Left Windows Key
|
||||
// const int VK_RWIN = 0x5C; // Right Windows Key
|
||||
//
|
||||
// // KBDLLHOOKSTRUCT 정의
|
||||
// base class KBDLLHOOKSTRUCT extends Struct {
|
||||
// @Int32()
|
||||
// external int vkCode;
|
||||
// @Int32()
|
||||
// external int scanCode;
|
||||
// @Int32()
|
||||
// external int flags;
|
||||
// @Int32()
|
||||
// external int time;
|
||||
// @Uint64()
|
||||
// external int dwExtraInfo;
|
||||
// }
|
||||
//
|
||||
// // HHOOK 타입 (void*)
|
||||
// typedef HHOOK = Pointer<Void>;
|
||||
// typedef HMODULE = Pointer<Void>;
|
||||
// typedef HWND = Pointer<Void>;
|
||||
//
|
||||
// // SetWindowsHookEx 타입
|
||||
// typedef SetWindowsHookExFunc = Pointer Function(
|
||||
// Int32 idHook,
|
||||
// Pointer callback,
|
||||
// HMODULE hMod,
|
||||
// Int32 dwThreadId,
|
||||
// );
|
||||
// typedef SetWindowsHookExDart = Pointer Function(
|
||||
// int idHook,
|
||||
// Pointer callback,
|
||||
// HMODULE hMod,
|
||||
// int dwThreadId,
|
||||
// );
|
||||
//
|
||||
// // CallNextHookEx 타입
|
||||
// typedef CallNextHookExFunc = Int32 Function(
|
||||
// HHOOK hhk,
|
||||
// Int32 nCode,
|
||||
// Int32 wParam,
|
||||
// Pointer lParam,
|
||||
// );
|
||||
// typedef CallNextHookExDart = int Function(
|
||||
// Pointer hhk,
|
||||
// int nCode,
|
||||
// int wParam,
|
||||
// Pointer lParam,
|
||||
// );
|
||||
//
|
||||
// // GetModuleHandle 타입
|
||||
// typedef GetModuleHandleFunc = HMODULE Function(Pointer<Utf16> lpModuleName);
|
||||
// typedef GetModuleHandleDart = Pointer Function(Pointer<Utf16> lpModuleName);
|
||||
//
|
||||
// // UnhookWindowsHookEx 타입
|
||||
// typedef UnhookWindowsHookExFunc = Int32 Function(HHOOK hhk);
|
||||
// typedef UnhookWindowsHookExDart = int Function(Pointer hhk);
|
||||
// typedef KeyboardHookProc = Int32 Function(
|
||||
// Int32 nCode,
|
||||
// Int32 wParam,
|
||||
// Pointer<KBDLLHOOKSTRUCT> lParam,
|
||||
// );
|
||||
//
|
||||
// /// 윈도우키 차단기
|
||||
// class WinKeyBlocker {
|
||||
// static Pointer<HHOOK>? _hookHandle;
|
||||
// static Pointer<NativeFunction<KeyboardHookProc>>? _callbackPtr;
|
||||
//
|
||||
// // 저수준 키보드 콜백 타입
|
||||
//
|
||||
// static int _keyboardHookCallback(
|
||||
// int nCode,
|
||||
// int wParam,
|
||||
// Pointer<KBDLLHOOKSTRUCT> lParam,
|
||||
// ) {
|
||||
// if (nCode == HC_ACTION) {
|
||||
// final vkCode = lParam.ref.vkCode;
|
||||
//
|
||||
// if (vkCode == VK_LWIN || vkCode == VK_RWIN) {
|
||||
// print('[BLOCKED] Windows Key (VK: $vkCode)');
|
||||
// return 1; // ✅ 차단
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 다음 훅으로 전달
|
||||
// return _callNextHookEx!.call(
|
||||
// _hookHandle!.cast(),
|
||||
// nCode,
|
||||
// wParam,
|
||||
// lParam.cast(),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // API 포인터
|
||||
// static SetWindowsHookExDart? _setHook;
|
||||
// static CallNextHookExDart? _callNextHookEx;
|
||||
// static GetModuleHandleDart? _getModuleHandle;
|
||||
// static UnhookWindowsHookExDart? _unhookHook;
|
||||
//
|
||||
// // DLL 핸들
|
||||
// static DynamicLibrary? _user32;
|
||||
//
|
||||
// /// 초기화: DLL 로드 및 함수 바인딩
|
||||
// static void _loadLibraries() {
|
||||
// if (_setHook != null) return; // 이미 로드됨
|
||||
//
|
||||
// _user32 = DynamicLibrary.open('user32.dll');
|
||||
//
|
||||
// _setHook = _user32!
|
||||
// .lookup<NativeFunction<SetWindowsHookExFunc>>('SetWindowsHookExW')
|
||||
// .asFunction<SetWindowsHookExDart>();
|
||||
//
|
||||
// // _callNextHookEx = _user32!
|
||||
// // .lookup<NativeFunction<CallNextHookExFunc>>('CallNextHookEx')
|
||||
// // .asFunction<CallNextHookExDart>();
|
||||
//
|
||||
// _getModuleHandle = _user32!
|
||||
// .lookup<NativeFunction<GetModuleHandleFunc>>('GetModuleHandleW')
|
||||
// .asFunction<GetModuleHandleDart>();
|
||||
//
|
||||
// // _unhookHook = _user32!
|
||||
// // .lookup<NativeFunction<UnhookWindowsHookExFunc>>('UnhookWindowsHookEx')
|
||||
// // .asFunction<UnhookWindowsHookExDart>();
|
||||
// }
|
||||
//
|
||||
// /// 윈도우키 차단 시작
|
||||
// static void start() {
|
||||
// if (!Platform.isWindows) return;
|
||||
//
|
||||
// _loadLibraries();
|
||||
//
|
||||
// final hModule = _getModuleHandle!.call(nullptr);
|
||||
//
|
||||
// final callback = Pointer.fromFunction<KeyboardHookProc>(_keyboardHookCallback);
|
||||
// _callbackPtr = callback;
|
||||
//
|
||||
// _hookHandle = _setHook!.call(WH_KEYBOARD_LL, callback, hModule, 0);
|
||||
//
|
||||
// if (_hookHandle!.address == 0) {
|
||||
// print('❌ Failed to install keyboard hook!');
|
||||
// } else {
|
||||
// print('✅ Windows Key Hook Installed');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// 훅 제거
|
||||
// static void stop() {
|
||||
// if (_hookHandle != null && _hookHandle!.address != 0) {
|
||||
// _unhookHook!.call(_hookHandle!.cast());
|
||||
// _hookHandle = nullptr;
|
||||
// _callbackPtr = null;
|
||||
// print('🛑 Windows Key Hook Removed');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Reference in New Issue
Block a user