diff --git a/flutter/lib/desktop/plugin/common.dart b/flutter/lib/desktop/plugin/common.dart new file mode 100644 index 000000000..b1b3dbfa0 --- /dev/null +++ b/flutter/lib/desktop/plugin/common.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +typedef PluginId = String; + +// ui location +const String kLocationHostMainDisplayOthers = + 'host|main|settings|display|others'; +const String kLocationClientRemoteToolbarDisplay = + 'client|remote|toolbar|display'; + +class MsgFromUi { + String remotePeerId; + String localPeerId; + String id; + String name; + String location; + String key; + String value; + String action; + + MsgFromUi({ + required this.remotePeerId, + required this.localPeerId, + required this.id, + required this.name, + required this.location, + required this.key, + required this.value, + required this.action, + }); + + Map toJson() { + return { + 'remote_peer_id': remotePeerId, + 'local_peer_id': localPeerId, + 'id': id, + 'name': name, + 'location': location, + 'key': key, + 'value': value, + 'action': action, + }; + } + + @override + String toString() { + return jsonEncode(toJson()); + } +} diff --git a/flutter/lib/plugin/desc.dart b/flutter/lib/desktop/plugin/desc.dart similarity index 64% rename from flutter/lib/plugin/desc.dart rename to flutter/lib/desktop/plugin/desc.dart index c6b04d11c..8c0de4625 100644 --- a/flutter/lib/plugin/desc.dart +++ b/flutter/lib/desktop/plugin/desc.dart @@ -1,49 +1,75 @@ import 'dart:collection'; -class UiButton { - String key; - String text; - String icon; - String tooltip; - String action; - - UiButton(this.key, this.text, this.icon, this.tooltip, this.action); - UiButton.fromJson(Map json) - : key = json['key'] ?? '', - text = json['text'] ?? '', - icon = json['icon'] ?? '', - tooltip = json['tooltip'] ?? '', - action = json['action'] ?? ''; -} - -class UiCheckbox { - String key; - String text; - String tooltip; - String action; - - UiCheckbox(this.key, this.text, this.tooltip, this.action); - UiCheckbox.fromJson(Map json) - : key = json['key'] ?? '', - text = json['text'] ?? '', - tooltip = json['tooltip'] ?? '', - action = json['action'] ?? ''; -} +const String kValueTrue = '1'; +const String kValueFalse = '0'; class UiType { - UiButton? button; - UiCheckbox? checkbox; + String key; + String text; + String tooltip; + String action; + + UiType(this.key, this.text, this.tooltip, this.action); UiType.fromJson(Map json) - : button = json['t'] == 'Button' ? UiButton.fromJson(json['c']) : null, - checkbox = - json['t'] != 'Checkbox' ? UiCheckbox.fromJson(json['c']) : null; + : key = json['key'] ?? '', + text = json['text'] ?? '', + tooltip = json['tooltip'] ?? '', + action = json['action'] ?? ''; + + static UiType? create(Map json) { + if (json['t'] == 'Button') { + return UiButton.fromJson(json['c']); + } else if (json['t'] == 'Checkbox') { + return UiCheckbox.fromJson(json['c']); + } else { + return null; + } + } +} + +class UiButton extends UiType { + String icon; + + UiButton( + {required String key, + required String text, + required this.icon, + required String tooltip, + required String action}) + : super(key, text, tooltip, action); + + UiButton.fromJson(Map json) + : icon = json['icon'] ?? '', + super.fromJson(json); +} + +class UiCheckbox extends UiType { + UiCheckbox( + {required String key, + required String text, + required String tooltip, + required String action}) + : super(key, text, tooltip, action); + + UiCheckbox.fromJson(Map json) : super.fromJson(json); } class Location { + // location key: + // host|main|settings|display|others + // client|remote|toolbar|display HashMap ui; Location(this.ui); + Location.fromJson(Map json) : ui = HashMap() { + json.forEach((key, value) { + var ui = UiType.create(value); + if (ui != null) { + this.ui[ui.key] = ui; + } + }); + } } class ConfigItem { @@ -58,6 +84,11 @@ class ConfigItem { value = json['value'] ?? '', description = json['description'] ?? '', defaultValue = json['default'] ?? ''; + + static String get trueValue => kValueTrue; + static String get falseValue => kValueFalse; + static bool isTrue(String value) => value == kValueTrue; + static bool isFalse(String value) => value == kValueFalse; } class Config { diff --git a/flutter/lib/plugin/event.dart b/flutter/lib/desktop/plugin/event.dart similarity index 100% rename from flutter/lib/plugin/event.dart rename to flutter/lib/desktop/plugin/event.dart diff --git a/flutter/lib/desktop/plugin/model.dart b/flutter/lib/desktop/plugin/model.dart new file mode 100644 index 000000000..a823844aa --- /dev/null +++ b/flutter/lib/desktop/plugin/model.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import './common.dart'; +import './desc.dart'; + +final Map locationModels = {}; + +class PluginModel with ChangeNotifier { + final List uiList = []; + + void add(UiType ui) { + uiList.add(ui); + notifyListeners(); + } + + bool get isEmpty => uiList.isEmpty; +} + +class LocationModel with ChangeNotifier { + final Map pluginModels = {}; + + void add(PluginId id, UiType ui) { + if (pluginModels[id] != null) { + pluginModels[id]!.add(ui); + } else { + var model = PluginModel(); + model.add(ui); + pluginModels[id] = model; + notifyListeners(); + } + } + + bool get isEmpty => pluginModels.isEmpty; +} + +void addLocationUi(String location, PluginId id, UiType ui) { + locationModels[location]?.add(id, ui); +} + +LocationModel addLocation(String location) { + if (locationModels[location] == null) { + locationModels[location] = LocationModel(); + } + return locationModels[location]!; +} diff --git a/flutter/lib/desktop/plugin/widget.dart b/flutter/lib/desktop/plugin/widget.dart new file mode 100644 index 000000000..3f7b413ea --- /dev/null +++ b/flutter/lib/desktop/plugin/widget.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +import './desc.dart'; +import './model.dart'; +import './common.dart'; + +class LocationItem extends StatelessWidget { + final String peerId; + final FFI ffi; + final String location; + final LocationModel locationModel; + + LocationItem({ + Key? key, + required this.peerId, + required this.ffi, + required this.location, + required this.locationModel, + }) : super(key: key); + + bool get isEmpty => locationModel.isEmpty; + + static LocationItem createLocationItem( + String peerId, FFI ffi, String location) { + final model = addLocation(location); + return LocationItem( + peerId: peerId, + ffi: ffi, + location: location, + locationModel: model, + ); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: locationModel, + child: Consumer(builder: (context, model, child) { + return Column( + children: model.pluginModels.entries + .map((entry) => _buildPluginItem(entry.key, entry.value)) + .toList(), + ); + }), + ); + } + + Widget _buildPluginItem(PluginId id, PluginModel model) => PluginItem( + pluginId: id, + peerId: peerId, + ffi: ffi, + location: location, + pluginModel: model, + ); +} + +class PluginItem extends StatelessWidget { + final PluginId pluginId; + final String peerId; + final FFI ffi; + final String location; + final PluginModel pluginModel; + + PluginItem({ + Key? key, + required this.pluginId, + required this.peerId, + required this.ffi, + required this.location, + required this.pluginModel, + }) : super(key: key); + + bool get isEmpty => pluginModel.isEmpty; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: pluginModel, + child: Consumer(builder: (context, model, child) { + return Column( + children: model.uiList.map((ui) => _buildItem(ui)).toList(), + ); + }), + ); + } + + // to-do: add plugin icon and tooltip + Widget _buildItem(UiType ui) { + switch (ui.runtimeType) { + case UiButton: + return _buildMenuButton(ui as UiButton); + case UiCheckbox: + return _buildCheckboxMenuButton(ui as UiCheckbox); + default: + return Container(); + } + } + + Uint8List _makeEvent( + String localPeerId, + String key, { + bool? v, + }) { + final event = MsgFromUi( + remotePeerId: peerId, + localPeerId: localPeerId, + id: pluginId, + name: getDesc(pluginId)?.name ?? '', + location: location, + key: key, + value: + v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '', + action: '', + ); + return Uint8List.fromList(event.toString().codeUnits); + } + + Widget _buildMenuButton(UiButton ui) { + return MenuButton( + onPressed: () { + () async { + final localPeerId = await bind.mainGetMyId(); + bind.pluginEvent( + id: pluginId, + event: _makeEvent(localPeerId, ui.key), + ); + }(); + }, + trailingIcon: Icon( + IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')), + // to-do: RustDesk translate or plugin translate ? + child: Text(ui.text), + ffi: ffi, + ); + } + + Widget _buildCheckboxMenuButton(UiCheckbox ui) { + final v = + bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: ui.key); + if (v == null) { + // session or plugin not found + return Container(); + } + return CkbMenuButton( + value: ConfigItem.isTrue(v), + onChanged: (v) { + if (v != null) { + () async { + final localPeerId = await bind.mainGetMyId(); + bind.pluginEvent( + id: pluginId, + event: _makeEvent(localPeerId, ui.key, v: v), + ); + }(); + } + }, + // to-do: rustdesk translate or plugin translate ? + child: Text(ui.text), + ffi: ffi, + ); + } +} + +void handleReloading(Map evt, String peer) { + if (evt['id'] == null || evt['location'] == null) { + return; + } + final ui = UiType.fromJson(evt); + addLocationUi(evt['location']!, evt['id']!, ui); +} diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index f8a38c830..f4895c785 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -9,6 +8,8 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/desktop/plugin/widget.dart'; +import 'package:flutter_hbb/desktop/plugin/common.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -641,7 +642,7 @@ class _ControlMenu extends StatelessWidget { if (e.divider) { return Divider(); } else { - return _MenuItemButton( + return MenuButton( child: e.child, onPressed: e.onPressed, ffi: ffi, @@ -656,13 +657,19 @@ class _DisplayMenu extends StatefulWidget { final FFI ffi; final MenubarState state; final Function(bool) setFullscreen; + final LocationItem pluginItem; _DisplayMenu( {Key? key, required this.id, required this.ffi, required this.state, required this.setFullscreen}) - : super(key: key); + : pluginItem = LocationItem.createLocationItem( + id, + ffi, + kLocationClientRemoteToolbarDisplay, + ), + super(key: key); @override State<_DisplayMenu> createState() => _DisplayMenuState(); @@ -700,6 +707,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { resolutions(), Divider(), toggles(), + widget.pluginItem, ]); } @@ -711,7 +719,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { if (!visible) return Offstage(); return Column( children: [ - _MenuItemButton( + MenuButton( child: Text(translate('Adjust Window')), onPressed: _doAdjustWindow, ffi: widget.ffi), @@ -828,7 +836,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { final v = data as List>; return Column(children: [ ...v - .map((e) => _RadioMenuButton( + .map((e) => RdoMenuButton( value: e.value, groupValue: e.groupValue, onChanged: e.onChanged, @@ -858,14 +866,14 @@ class _DisplayMenuState extends State<_DisplayMenu> { final enabled = widget.ffi.canvasModel.imageOverflow.value; return Column(children: [ - _RadioMenuButton( + RdoMenuButton( child: Text(translate('ScrollAuto')), value: kRemoteScrollStyleAuto, groupValue: groupValue, onChanged: enabled ? (value) => onChange(value) : null, ffi: widget.ffi, ), - _RadioMenuButton( + RdoMenuButton( child: Text(translate('Scrollbar')), value: kRemoteScrollStyleBar, groupValue: groupValue, @@ -886,7 +894,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, child: Text(translate('Image Quality')), menuChildren: v - .map((e) => _RadioMenuButton( + .map((e) => RdoMenuButton( value: e.value, groupValue: e.groupValue, onChanged: e.onChanged, @@ -908,7 +916,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, child: Text(translate('Codec')), menuChildren: v - .map((e) => _RadioMenuButton( + .map((e) => RdoMenuButton( value: e.value, groupValue: e.groupValue, onChanged: e.onChanged, @@ -948,7 +956,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { return _SubmenuButton( ffi: widget.ffi, menuChildren: resolutions - .map((e) => _RadioMenuButton( + .map((e) => RdoMenuButton( value: '${e.width}x${e.height}', groupValue: groupValue, onChanged: onChanged, @@ -966,7 +974,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { if (v.isEmpty) return Offstage(); return Column( children: v - .map((e) => _CheckboxMenuButton( + .map((e) => CkbMenuButton( value: e.value, onChanged: e.onChanged, child: e.child, @@ -1026,7 +1034,7 @@ class _KeyboardMenu extends StatelessWidget { KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), ]; - List<_RadioMenuButton> list = []; + List list = []; final enabled = !ffi.ffiModel.viewOnly; onChanged(String? value) async { if (value == null) return; @@ -1049,7 +1057,7 @@ class _KeyboardMenu extends StatelessWidget { if (mode.key == _kKeyTranslateMode) { text = '$text beta'; } - list.add(_RadioMenuButton( + list.add(RdoMenuButton( child: Text(text), value: mode.key, groupValue: groupValue, @@ -1069,7 +1077,7 @@ class _KeyboardMenu extends StatelessWidget { return Column( children: [ Divider(), - _MenuItemButton( + MenuButton( child: Text( '${translate('Local keyboard type')}: ${KBLayoutType.value}'), trailingIcon: const Icon(Icons.settings), @@ -1085,7 +1093,7 @@ class _KeyboardMenu extends StatelessWidget { view_mode() { final ffiModel = ffi.ffiModel; final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; - return _CheckboxMenuButton( + return CkbMenuButton( value: ffiModel.viewOnly, onChanged: enabled ? (value) async { @@ -1129,7 +1137,7 @@ class _ChatMenuState extends State<_ChatMenu> { } textChat() { - return _MenuItemButton( + return MenuButton( child: Text(translate('Text chat')), ffi: widget.ffi, onPressed: () { @@ -1148,7 +1156,7 @@ class _ChatMenuState extends State<_ChatMenu> { } voiceCall() { - return _MenuItemButton( + return MenuButton( child: Text(translate('Voice call')), ffi: widget.ffi, onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), @@ -1403,12 +1411,12 @@ class _SubmenuButton extends StatelessWidget { } } -class _MenuItemButton extends StatelessWidget { +class MenuButton extends StatelessWidget { final VoidCallback? onPressed; final Widget? trailingIcon; final Widget? child; final FFI ffi; - _MenuItemButton( + MenuButton( {Key? key, this.onPressed, this.trailingIcon, @@ -1431,12 +1439,12 @@ class _MenuItemButton extends StatelessWidget { } } -class _CheckboxMenuButton extends StatelessWidget { +class CkbMenuButton extends StatelessWidget { final bool? value; final ValueChanged? onChanged; final Widget? child; final FFI ffi; - const _CheckboxMenuButton( + const CkbMenuButton( {Key? key, required this.value, required this.onChanged, @@ -1460,13 +1468,13 @@ class _CheckboxMenuButton extends StatelessWidget { } } -class _RadioMenuButton extends StatelessWidget { +class RdoMenuButton extends StatelessWidget { final T value; final T? groupValue; final ValueChanged? onChanged; final Widget? child; final FFI ffi; - const _RadioMenuButton( + const RdoMenuButton( {Key? key, required this.value, required this.groupValue, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b9a2a7598..406783c2e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -16,8 +16,9 @@ import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; -import 'package:flutter_hbb/plugin/event.dart'; -import 'package:flutter_hbb/plugin/reloader.dart'; +import 'package:flutter_hbb/desktop/plugin/event.dart'; +import 'package:flutter_hbb/desktop/plugin/desc.dart'; +import 'package:flutter_hbb/desktop/plugin/widget.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; @@ -229,7 +230,7 @@ class FfiModel with ChangeNotifier { } else if (name == "fingerprint") { FingerprintState.find(peerId).value = evt['fingerprint'] ?? ''; } else if (name == "plugin_desc") { - handleReloading(evt, peerId); + updateDesc(evt); } else if (name == "plugin_event") { handlePluginEvent( evt, peerId, (Map e) => handleMsgBox(e, peerId)); diff --git a/flutter/lib/plugin/reloader.dart b/flutter/lib/plugin/reloader.dart deleted file mode 100644 index 1b1641f87..000000000 --- a/flutter/lib/plugin/reloader.dart +++ /dev/null @@ -1,29 +0,0 @@ -void handleReloading(Map evt, String peer) { - // location - // host|main|settings|display|others - // client|remote|toolbar|display - // - // ui - // { - // "t": "Button", - // "c": { - // "key": "key", - // "text": "text", - // "icon": "icon", - // "tooltip": "tooltip", - // "action": "action" - // } - // } - // - // { - // "t": "Checkbox", - // "c": { - // "key": "key", - // "text": "text", - // "tooltip": "tooltip", - // "action": "action" - // } - // } - // - -} diff --git a/flutter/lib/plugin/widget.dart b/flutter/lib/plugin/widget.dart deleted file mode 100644 index dbdfbbadd..000000000 --- a/flutter/lib/plugin/widget.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -final Map pluginWidgets = {}; - -class PluginWidget { - final String id; - final String name; - final String location; - final Widget widget; - - PluginWidget({ - required this.id, - required this.name, - required this.location, - required this.widget, - }); -} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5f72431ef..b5cc669ab 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1405,6 +1405,58 @@ pub fn plugin_event(_id: String, _event: Vec) { } } +#[inline] +pub fn plugin_get_session_option(_id: String, _peer: String, _key: String) -> SyncReturn> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return SyncReturn(crate::plugin::PeerConfig::get(&_id, &_peer, &_key)); + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + return SyncReturn(None); + } +} + +#[inline] +pub fn plugin_set_session_option(_id: String, _peer: String, _key: String, _value: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::plugin::PeerConfig::set(&_id, &_peer, &_key, &_value); + } +} + +#[inline] +pub fn plugin_get_local_option(_id: String, _key: String) -> SyncReturn> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return SyncReturn(crate::plugin::LocalConfig::get(&_id, &_key)); + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + return SyncReturn(None); + } +} + +#[inline] +pub fn plugin_set_local_option(_id: String, _key: String, _value: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::plugin::LocalConfig::set(&_id, &_key, &_value); + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/plugin/config.rs b/src/plugin/config.rs index 7a9228425..d86053649 100644 --- a/src/plugin/config.rs +++ b/src/plugin/config.rs @@ -15,9 +15,9 @@ lazy_static::lazy_static! { } #[derive(Debug, Default, Serialize, Deserialize)] -struct LocalConfig(HashMap); +pub struct LocalConfig(HashMap); #[derive(Debug, Default, Serialize, Deserialize)] -struct PeerConfig(HashMap); +pub struct PeerConfig(HashMap); type PeersConfig = HashMap; #[inline] diff --git a/src/plugin/desc.rs b/src/plugin/desc.rs index bc094abf9..94a137570 100644 --- a/src/plugin/desc.rs +++ b/src/plugin/desc.rs @@ -8,7 +8,7 @@ use std::ffi::{c_char, CStr}; pub struct UiButton { key: String, text: String, - icon: String, + icon: String, // icon can be int in flutter, but string in other ui framework. And it is flexible to use string. tooltip: String, action: String, // The action to be triggered when the button is clicked. } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 2f16f0325..c7ff2c4af 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -12,6 +12,8 @@ pub use plugins::{ reload_plugin, unload_plugin, }; +pub use config::{LocalConfig, PeerConfig}; + #[inline] fn cstr_to_string(cstr: *const c_char) -> ResultType { Ok(String::from_utf8(unsafe {