diff --git a/flutter_hbb/lib/common.dart b/flutter_hbb/lib/common.dart index a9cf38f5a..9f25f8fb1 100644 --- a/flutter_hbb/lib/common.dart +++ b/flutter_hbb/lib/common.dart @@ -1,19 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:ffi/ffi.dart'; -import 'package:path_provider/path_provider.dart'; -import 'dart:io'; -import 'dart:ffi'; import 'dart:async'; -import 'dart:convert'; import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'dart:typed_data'; -import 'dart:ui' as ui; - -class RgbaFrame extends Struct { - @Uint32() - int len; - Pointer data; -} +import 'model.dart'; class HexColor extends Color { HexColor(final String hexColor) : super(_getColorFromHex(hexColor)); @@ -33,199 +21,6 @@ class MyTheme { static const Color accent = Color(0xFF0071FF); } -typedef F1 = void Function(Pointer); -typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = void Function(Pointer, Pointer); -typedef F4 = void Function(Pointer); -typedef F5 = Pointer Function(); - -// https://juejin.im/post/6844903864852807694 -class FfiModel with ChangeNotifier { - FfiModel() { - init(); - } - - Future init() async { - await FFI.init(); - notifyListeners(); - } -} - -class ImageModel with ChangeNotifier { - ui.Image _image; - - ui.Image get image => _image; - - void update(ui.Image image) { - _image = image; - notifyListeners(); - } -} - -class CursorModel with ChangeNotifier { - ui.Image _image; - final _images = Map(); - double _x = 0; - double _y = 0; - double _hotx = 0; - double _hoty = 0; - - ui.Image get image => _image; - double get x => _x; - double get y => _y; - - void updateCursorData(Map evt) { - var id = int.parse(evt['id']); - _hotx = double.parse(evt['hotx']); - _hoty = double.parse(evt['hoty']); - var width = int.parse(evt['width']); - var height = int.parse(evt['height']); - List colors = json.decode(evt['colors']); - final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); - ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, - (image) { - _image = image; - _images[id] = image; - notifyListeners(); - }); - } - - void updateCursorId(Map evt) { - final tmp = _images[int.parse(evt['id'])]; - if (tmp != null) { - _image = tmp; - notifyListeners(); - } - } - - void updateCursorPosition(Map evt) { - _x = double.parse(evt['x']); - _y = double.parse(evt['y']); - notifyListeners(); - } - - void clear() { - _image = null; - _images.clear(); - } -} - -class FFI { - static F1 _freeCString; - static F2 _getByName; - static F3 _setByName; - static F4 _freeRgba; - static F5 _getRgba; - static Pointer _lastRgbaFrame; - static final imageModel = ImageModel(); - static final ffiModel = FfiModel(); - static final cursorModel = CursorModel(); - - static String getId() { - return getByName('remote_id'); - } - - static List peers() { - try { - List peers = json.decode(getByName('peers')); - return peers - .map((s) => s as List) - .map((s) => - Peer.fromJson(s[0] as String, s[1] as Map)) - .toList(); - } catch (e) { - print(e); - } - return []; - } - - static void connect(String id) { - setByName('connect', id); - } - - static void clearRgbaFrame() { - if (_lastRgbaFrame != null && _lastRgbaFrame != nullptr) - _freeRgba(_lastRgbaFrame); - } - - static Uint8List getRgba() { - _lastRgbaFrame = _getRgba(); - if (_lastRgbaFrame == null || _lastRgbaFrame == nullptr) return null; - final ref = _lastRgbaFrame.ref; - return Uint8List.sublistView(ref.data.asTypedList(ref.len)); - } - - static Map popEvent() { - var s = getByName('event'); - if (s == '') return null; - try { - Map event = json.decode(s); - return event; - } catch (e) { - print(e); - } - return null; - } - - static void login(String password, bool remember) { - setByName( - 'login', - json.encode({ - 'password': password, - 'remember': remember ? 'true' : 'false', - })); - } - - static void close() { - setByName('close', ''); - FFI.imageModel.update(null); - FFI.cursorModel.clear(); - } - - static void setByName(String name, String value) { - _setByName(Utf8.toUtf8(name), Utf8.toUtf8(value)); - } - - static String getByName(String name, {String arg = ''}) { - var p = _getByName(Utf8.toUtf8(name), Utf8.toUtf8(arg)); - assert(p != nullptr && p != null); - var res = Utf8.fromUtf8(p); - // https://github.com/brickpop/flutter-rust-ffi - _freeCString(p); - return res; - } - - static Future init() async { - final dylib = Platform.isAndroid - ? DynamicLibrary.open('librustdesk.so') - : DynamicLibrary.process(); - _getByName = dylib.lookupFunction('get_by_name'); - _setByName = - dylib.lookupFunction, Pointer), F3>( - 'set_by_name'); - _freeCString = dylib - .lookupFunction), F1>('rust_cstr_free'); - _freeRgba = dylib - .lookupFunction), F4>('free_rgba'); - _getRgba = dylib.lookupFunction('get_rgba'); - final dir = (await getApplicationDocumentsDirectory()).path; - setByName('init', dir); - } -} - -class Peer { - final String id; - final String username; - final String hostname; - final String platform; - - Peer.fromJson(String id, Map json) - : id = id, - username = json['username'], - hostname = json['hostname'], - platform = json['platform']; -} - // https://github.com/huangjianke/flutter_easyloading void showLoading(String text) { dismissLoading(); diff --git a/flutter_hbb/lib/home_page.dart b/flutter_hbb/lib/home_page.dart index a04198e15..e22903611 100644 --- a/flutter_hbb/lib/home_page.dart +++ b/flutter_hbb/lib/home_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'common.dart'; +import 'model.dart'; import 'remote_page.dart'; class HomePage extends StatefulWidget { diff --git a/flutter_hbb/lib/main.dart b/flutter_hbb/lib/main.dart index 9e32c5bf5..99767935a 100644 --- a/flutter_hbb/lib/main.dart +++ b/flutter_hbb/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'common.dart'; +import 'model.dart'; import 'home_page.dart'; void main() { diff --git a/flutter_hbb/lib/model.dart b/flutter_hbb/lib/model.dart new file mode 100644 index 000000000..8a4132e00 --- /dev/null +++ b/flutter_hbb/lib/model.dart @@ -0,0 +1,327 @@ +import 'package:ffi/ffi.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +import 'dart:ffi'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'common.dart'; + +class RgbaFrame extends Struct { + @Uint32() + int len; + Pointer data; +} + +typedef F1 = void Function(Pointer); +typedef F2 = Pointer Function(Pointer, Pointer); +typedef F3 = void Function(Pointer, Pointer); +typedef F4 = void Function(Pointer); +typedef F5 = Pointer Function(); + +// https://juejin.im/post/6844903864852807694 +class FfiModel with ChangeNotifier { + PeerInfo _pi = PeerInfo(); + Display _display = Display(); + bool _decoding = false; + + FfiModel() { + init(); + } + + Future init() async { + await FFI.init(); + notifyListeners(); + } + + void clear() { + _decoding = false; + } + + void update(String id, BuildContext context) { + var evt = FFI.popEvent(); + if (evt != null) { + var name = evt['name']; + if (name == 'msgbox') { + handleMsgbox(evt, id, context); + } else if (name == 'peer_info') { + handlePeerInfo(evt); + } else if (name == 'switch_display') { + handleSwitchDisplay(evt); + } else if (name == 'cursor_data') { + FFI.cursorModel.updateCursorData(evt); + } else if (name == 'cursor_id') { + FFI.cursorModel.updateCursorId(evt); + } else if (name == 'cursor_position') { + FFI.cursorModel.updateCursorPosition(evt); + } + } + if (!_decoding) { + var rgba = FFI.getRgba(); + if (rgba != null) { + _decoding = true; + ui.decodeImageFromPixels( + rgba, _display.width, _display.height, ui.PixelFormat.bgra8888, + (image) { + FFI.clearRgbaFrame(); + _decoding = false; + try { + // my throw exception, because the listener maybe already dispose + FFI.imageModel.update(image); + } catch (e) {} + }); + } + } + } + + void handleMsgbox(Map evt, String id, BuildContext context) { + var type = evt['type']; + var title = evt['title']; + var text = evt['text']; + if (type == 're-input-password') { + wrongPasswordDialog(id, context); + } else if (type == 'input-password') { + enterPasswordDialog(id, context); + } else { + msgbox(type, title, text, context); + } + } + + void handleSwitchDisplay(Map evt) { + _pi.currentDisplay = int.parse(evt['display']); + _display.x = double.parse(evt['x']); + _display.y = double.parse(evt['y']); + _display.width = int.parse(evt['width']); + _display.height = int.parse(evt['height']); + FFI.cursorModel.updateDisplayOrigin(_display.x, _display.y); + } + + void handlePeerInfo(Map evt) { + dismissLoading(); + _pi.username = evt['username']; + _pi.hostname = evt['hostname']; + _pi.platform = evt['platform']; + _pi.sasEnabled = evt['sas_enabled'] == "true"; + _pi.currentDisplay = int.parse(evt['current_display']); + List displays = json.decode(evt['displays']); + _pi.displays = List(); + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + _pi.displays.add(d); + } + if (_pi.currentDisplay < _pi.displays.length) { + _display = _pi.displays[_pi.currentDisplay]; + FFI.cursorModel.updateDisplayOrigin(_display.x, _display.y); + } + } +} + +class ImageModel with ChangeNotifier { + ui.Image _image; + + ui.Image get image => _image; + + void update(ui.Image image) { + _image = image; + notifyListeners(); + } +} + +class CursorModel with ChangeNotifier { + ui.Image _image; + final _images = Map(); + double _x = 0; + double _y = 0; + double _hotx = 0; + double _hoty = 0; + double _displayOriginX = 0; + double _displayOriginY = 0; + + ui.Image get image => _image; + double get x => _x - _displayOriginX - _hotx; + double get y => _y - _displayOriginY - _hoty; + + void updateCursorData(Map evt) { + var id = int.parse(evt['id']); + _hotx = double.parse(evt['hotx']); + _hoty = double.parse(evt['hoty']); + var width = int.parse(evt['width']); + var height = int.parse(evt['height']); + List colors = json.decode(evt['colors']); + final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); + ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, + (image) { + _image = image; + _images[id] = image; + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) {} + }); + } + + void updateCursorId(Map evt) { + final tmp = _images[int.parse(evt['id'])]; + if (tmp != null) { + _image = tmp; + notifyListeners(); + } + } + + void updateCursorPosition(Map evt) { + _x = double.parse(evt['x']); + _y = double.parse(evt['y']); + notifyListeners(); + } + + void updateDisplayOrigin(double x, double y) { + _displayOriginX = x; + _displayOriginY = y; + notifyListeners(); + } + + void clear() { + _image = null; + _images.clear(); + } +} + +class FFI { + static F1 _freeCString; + static F2 _getByName; + static F3 _setByName; + static F4 _freeRgba; + static F5 _getRgba; + static Pointer _lastRgbaFrame; + static final imageModel = ImageModel(); + static final ffiModel = FfiModel(); + static final cursorModel = CursorModel(); + + static String getId() { + return getByName('remote_id'); + } + + static List peers() { + try { + List peers = json.decode(getByName('peers')); + return peers + .map((s) => s as List) + .map((s) => + Peer.fromJson(s[0] as String, s[1] as Map)) + .toList(); + } catch (e) { + print(e); + } + return []; + } + + static void connect(String id) { + setByName('connect', id); + } + + static void clearRgbaFrame() { + if (_lastRgbaFrame != null && _lastRgbaFrame != nullptr) + _freeRgba(_lastRgbaFrame); + } + + static Uint8List getRgba() { + _lastRgbaFrame = _getRgba(); + if (_lastRgbaFrame == null || _lastRgbaFrame == nullptr) return null; + final ref = _lastRgbaFrame.ref; + return Uint8List.sublistView(ref.data.asTypedList(ref.len)); + } + + static Map popEvent() { + var s = getByName('event'); + if (s == '') return null; + try { + Map event = json.decode(s); + return event; + } catch (e) { + print(e); + } + return null; + } + + static void login(String password, bool remember) { + setByName( + 'login', + json.encode({ + 'password': password, + 'remember': remember ? 'true' : 'false', + })); + } + + static void close() { + setByName('close', ''); + FFI.imageModel.update(null); + FFI.cursorModel.clear(); + FFI.ffiModel.clear(); + } + + static void setByName(String name, String value) { + _setByName(Utf8.toUtf8(name), Utf8.toUtf8(value)); + } + + static String getByName(String name, {String arg = ''}) { + var p = _getByName(Utf8.toUtf8(name), Utf8.toUtf8(arg)); + assert(p != nullptr && p != null); + var res = Utf8.fromUtf8(p); + // https://github.com/brickpop/flutter-rust-ffi + _freeCString(p); + return res; + } + + static Future init() async { + final dylib = Platform.isAndroid + ? DynamicLibrary.open('librustdesk.so') + : DynamicLibrary.process(); + _getByName = dylib.lookupFunction('get_by_name'); + _setByName = + dylib.lookupFunction, Pointer), F3>( + 'set_by_name'); + _freeCString = dylib + .lookupFunction), F1>('rust_cstr_free'); + _freeRgba = dylib + .lookupFunction), F4>('free_rgba'); + _getRgba = dylib.lookupFunction('get_rgba'); + final dir = (await getApplicationDocumentsDirectory()).path; + setByName('init', dir); + } +} + +class Peer { + final String id; + final String username; + final String hostname; + final String platform; + + Peer.fromJson(String id, Map json) + : id = id, + username = json['username'], + hostname = json['hostname'], + platform = json['platform']; +} + +class Display { + double x = 0; + double y = 0; + int width = 0; + int height = 0; +} + +class PeerInfo { + String username; + String hostname; + String platform; + bool sasEnabled; + int currentDisplay; + List displays; +} diff --git a/flutter_hbb/lib/remote_page.dart b/flutter_hbb/lib/remote_page.dart index d1a16a921..6c9f63702 100644 --- a/flutter_hbb/lib/remote_page.dart +++ b/flutter_hbb/lib/remote_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'common.dart'; import 'package:flutter/services.dart'; import 'dart:ui' as ui; import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'dart:convert'; import 'dart:async'; +import 'common.dart'; +import 'model.dart'; class RemotePage extends StatefulWidget { RemotePage({Key key, this.id}) : super(key: key); @@ -19,9 +19,6 @@ class RemotePage extends StatefulWidget { // https://github.com/hanxu317317/flutter_plan_demo/blob/master/lib/src/enter.dart class _RemotePageState extends State { Timer _interval; - PeerInfo _pi = PeerInfo(); - Display _display = Display(); - bool _decoding = false; @override void initState() { @@ -42,88 +39,11 @@ class _RemotePageState extends State { FFI.close(); _interval.cancel(); dismissLoading(); - _decoding = null; SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); } void interval() { - var evt = FFI.popEvent(); - if (evt != null) { - var name = evt['name']; - if (name == 'msgbox') { - handleMsgbox(evt); - } else if (name == 'peer_info') { - handlePeerInfo(evt); - } else if (name == 'switch_display') { - handleSwitchDisplay(evt); - } else if (name == 'cursor_data') { - FFI.cursorModel.updateCursorData(evt); - } else if (name == 'cursor_id') { - FFI.cursorModel.updateCursorId(evt); - } else if (name == 'cursor_position') { - FFI.cursorModel.updateCursorPosition(evt); - } - } - if (!_decoding) { - var rgba = FFI.getRgba(); - if (rgba != null) { - _decoding = true; - ui.decodeImageFromPixels( - rgba, _display.width, _display.height, ui.PixelFormat.bgra8888, - (image) { - FFI.clearRgbaFrame(); - if (_decoding == null) return; - _decoding = false; - FFI.imageModel.update(image); - }); - } - } - } - - void handleSwitchDisplay(Map evt) { - _pi.currentDisplay = int.parse(evt['display']); - _display.x = int.parse(evt['x']); - _display.y = int.parse(evt['y']); - _display.width = int.parse(evt['width']); - _display.height = int.parse(evt['height']); - setState(() {}); - } - - void handlePeerInfo(Map evt) { - dismissLoading(); - _pi.username = evt['username']; - _pi.hostname = evt['hostname']; - _pi.platform = evt['platform']; - _pi.sasEnabled = evt['sas_enabled'] == "true"; - _pi.currentDisplay = int.parse(evt['current_display']); - List displays = json.decode(evt['displays']); - _pi.displays = List(); - for (int i = 0; i < displays.length; ++i) { - Map d0 = displays[i]; - var d = Display(); - d.x = d0['x']; - d.y = d0['y']; - d.width = d0['width']; - d.height = d0['height']; - _pi.displays.add(d); - } - if (_pi.currentDisplay < _pi.displays.length) { - _display = _pi.displays[_pi.currentDisplay]; - } - setState(() {}); - } - - void handleMsgbox(Map evt) { - var type = evt['type']; - var title = evt['title']; - var text = evt['text']; - if (type == 're-input-password') { - wrongPasswordDialog(widget.id, context); - } else if (type == 'input-password') { - enterPasswordDialog(widget.id, context); - } else { - msgbox(type, title, text, context); - } + FFI.ffiModel.update(widget.id, context); } @override @@ -170,19 +90,3 @@ class ImagePainter extends CustomPainter { return oldDelegate != this; } } - -class Display { - int x = 0; - int y = 0; - int width = 0; - int height = 0; -} - -class PeerInfo { - String username; - String hostname; - String platform; - bool sasEnabled; - int currentDisplay; - List displays; -}