This commit is contained in:
2025-08-16 12:37:47 +09:00
commit e39e21b207
145 changed files with 8805 additions and 0 deletions

1
lib/flutter_rfb.dart Normal file
View File

@@ -0,0 +1 @@
export 'rfb/remote_frame_buffer_widget.dart';

365
lib/main.dart Normal file
View 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,
),
),
),
),
),
),
],
),
),
],
],
),
),
),
),
);
}
}

View 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;
}

View 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;
}
}
}

View 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,
));
});
}
}

View 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();
}

View 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(),
),
),
);
}

View 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;
}

File diff suppressed because it is too large Load Diff

View 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(),
),
),
);
}

View 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,
),
);
},
);
}
}

View 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');
// }
// }
// }