Refact. Flutter web desktop (#7539)

* Refact. Flutter web desktop

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Flutter web, prevent default context menu

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou 2024-03-28 11:38:11 +08:00 committed by GitHub
parent 810b980e6b
commit 6e44a91d0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 362 additions and 153 deletions

View File

@ -29,6 +29,8 @@ import '../consts.dart';
import 'common/widgets/overlay.dart'; import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart'; import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/input_model.dart'; import 'models/input_model.dart';
import 'models/model.dart'; import 'models/model.dart';
import 'models/platform_model.dart'; import 'models/platform_model.dart';
@ -48,7 +50,7 @@ final isMacOS = isMacOS_;
final isLinux = isLinux_; final isLinux = isLinux_;
final isDesktop = isDesktop_; final isDesktop = isDesktop_;
final isWeb = isWeb_; final isWeb = isWeb_;
var isWebDesktop = false; final isWebDesktop = isWebDesktop_;
var isMobile = isAndroid || isIOS; var isMobile = isAndroid || isIOS;
var version = ''; var version = '';
int androidVersion = 0; int androidVersion = 0;
@ -60,6 +62,8 @@ DesktopType? desktopType;
bool get isMainDesktopWindow => bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm; desktopType == DesktopType.main || desktopType == DesktopType.cm;
String get screenInfo => screenInfo_;
/// Check if the app is running with single view mode. /// Check if the app is running with single view mode.
bool isSingleViewApp() { bool isSingleViewApp() {
return desktopType == DesktopType.cm; return desktopType == DesktopType.cm;
@ -233,11 +237,13 @@ class MyTheme {
); );
static SwitchThemeData switchTheme() { static SwitchThemeData switchTheme() {
return SwitchThemeData(splashRadius: isDesktop ? 0 : kRadialReactionRadius); return SwitchThemeData(
splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
} }
static RadioThemeData radioTheme() { static RadioThemeData radioTheme() {
return RadioThemeData(splashRadius: isDesktop ? 0 : kRadialReactionRadius); return RadioThemeData(
splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
} }
// Checkbox // Checkbox
@ -286,7 +292,7 @@ class MyTheme {
static EdgeInsets dialogContentPadding({bool actions = true}) { static EdgeInsets dialogContentPadding({bool actions = true}) {
final double p = dialogPadding; final double p = dialogPadding;
return isDesktop return (isDesktop || isWebDesktop)
? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p) ? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
: EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p); : EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
} }
@ -294,12 +300,12 @@ class MyTheme {
static EdgeInsets dialogActionsPadding() { static EdgeInsets dialogActionsPadding() {
final double p = dialogPadding; final double p = dialogPadding;
return isDesktop return (isDesktop || isWebDesktop)
? EdgeInsets.fromLTRB(p, 0, p, (p - 4)) ? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
: EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2)); : EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
} }
static EdgeInsets dialogButtonPadding = isDesktop static EdgeInsets dialogButtonPadding = (isDesktop || isWebDesktop)
? EdgeInsets.only(left: dialogPadding) ? EdgeInsets.only(left: dialogPadding)
: EdgeInsets.only(left: dialogPadding / 3); : EdgeInsets.only(left: dialogPadding / 3);
@ -371,10 +377,10 @@ class MyTheme {
labelColor: Colors.black87, labelColor: Colors.black87,
), ),
tooltipTheme: tooltipTheme(), tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null, splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null, highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
splashFactory: isDesktop ? NoSplash.splashFactory : null, splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop textButtonTheme: (isDesktop || isWebDesktop)
? TextButtonThemeData( ? TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
@ -414,7 +420,9 @@ class MyTheme {
color: Colors.white, color: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide( side: BorderSide(
color: isDesktop ? Color(0xFFECECEC) : Colors.transparent), color: (isDesktop || isWebDesktop)
? Color(0xFFECECEC)
: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(8.0)), borderRadius: BorderRadius.all(Radius.circular(8.0)),
)), )),
).copyWith( ).copyWith(
@ -440,7 +448,7 @@ class MyTheme {
), ),
), ),
scrollbarTheme: scrollbarThemeDark, scrollbarTheme: scrollbarThemeDark,
inputDecorationTheme: isDesktop inputDecorationTheme: (isDesktop || isWebDesktop)
? InputDecorationTheme( ? InputDecorationTheme(
fillColor: Color(0xFF24252B), fillColor: Color(0xFF24252B),
filled: true, filled: true,
@ -467,10 +475,10 @@ class MyTheme {
labelColor: Colors.white70, labelColor: Colors.white70,
), ),
tooltipTheme: tooltipTheme(), tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null, splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null, highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
splashFactory: isDesktop ? NoSplash.splashFactory : null, splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop textButtonTheme: (isDesktop || isWebDesktop)
? TextButtonThemeData( ? TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
@ -818,7 +826,7 @@ class OverlayDialogManager {
Offstage( Offstage(
offstage: !showCancel, offstage: !showCancel,
child: Center( child: Center(
child: isDesktop child: (isDesktop || isWebDesktop)
? dialogButton('Cancel', onPressed: cancel) ? dialogButton('Cancel', onPressed: cancel)
: TextButton( : TextButton(
style: flatButtonStyle, style: flatButtonStyle,
@ -1293,7 +1301,7 @@ class AndroidPermissionManager {
} }
static Future<bool> check(String type) { static Future<bool> check(String type) {
if (isDesktop) { if (isDesktop || isWeb) {
return Future.value(true); return Future.value(true);
} }
return gFFI.invokeMethod("check_permission", type); return gFFI.invokeMethod("check_permission", type);
@ -1307,7 +1315,7 @@ class AndroidPermissionManager {
/// We use XXPermissions to request permissions, /// We use XXPermissions to request permissions,
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
static Future<bool> request(String type) { static Future<bool> request(String type) {
if (isDesktop) { if (isDesktop || isWeb) {
return Future.value(true); return Future.value(true);
} }
@ -2197,13 +2205,29 @@ connect(BuildContext context, String id,
), ),
); );
} else { } else {
Navigator.push( if (isWebDesktop) {
context, Navigator.push(
MaterialPageRoute( context,
builder: (BuildContext context) => RemotePage( MaterialPageRoute(
id: id, password: password, isSharedPassword: isSharedPassword), builder: (BuildContext context) => desktop_remote.RemotePage(
), key: ValueKey(id),
); id: id,
toolbarState: ToolbarState(),
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => RemotePage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
} }
} }
@ -2398,7 +2422,7 @@ Widget dialogButton(String text,
Widget? icon, Widget? icon,
TextStyle? style, TextStyle? style,
ButtonStyle? buttonStyle}) { ButtonStyle? buttonStyle}) {
if (isDesktop) { if (isDesktop || isWebDesktop) {
if (isOutline) { if (isOutline) {
return icon == null return icon == null
? OutlinedButton( ? OutlinedButton(

View File

@ -63,7 +63,7 @@ class _AddressBookState extends State<AddressBook> {
retry: null, // remove retry retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''), close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded( Expanded(
child: isDesktop child: (isDesktop || isWebDesktop)
? _buildAddressBookDesktop() ? _buildAddressBookDesktop()
: _buildAddressBookMobile()) : _buildAddressBookMobile())
], ],
@ -311,7 +311,7 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e); return tagBuilder(e);
}); });
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop return (isDesktop || isWebDesktop)
? gridView ? gridView
: LimitedBox(maxHeight: maxHeight, child: gridView); : LimitedBox(maxHeight: maxHeight, child: gridView);
}); });

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -81,7 +80,7 @@ void changeIdDialog() {
final Iterable violations = rules.where((r) => !r.validate(newId)); final Iterable violations = rules.where((r) => !r.validate(newId));
if (violations.isNotEmpty) { if (violations.isNotEmpty) {
setState(() { setState(() {
msg = isDesktop msg = (isDesktop || isWebDesktop)
? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}' ? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'
: violations.map((r) => r.name).join(', '); : violations.map((r) => r.name).join(', ');
}); });
@ -106,7 +105,7 @@ void changeIdDialog() {
} }
setState(() { setState(() {
isInProgress = false; isInProgress = false;
msg = isDesktop msg = (isDesktop || isWebDesktop)
? '${translate('Prompt')}: ${translate(status)}' ? '${translate('Prompt')}: ${translate(status)}'
: translate(status); : translate(status);
}); });
@ -143,7 +142,7 @@ void changeIdDialog() {
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
isDesktop (isDesktop || isWebDesktop)
? Obx(() => Wrap( ? Obx(() => Wrap(
runSpacing: 8, runSpacing: 8,
spacing: 4, spacing: 4,
@ -1109,7 +1108,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value, errorText: errPwd.isEmpty ? null : errPwd.value,
), ),
], ],
).marginOnly(left: isDesktop ? 35 : 0), ).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
).marginOnly(top: 10), ).marginOnly(top: 10),
], ],
), ),

View File

@ -47,7 +47,10 @@ class _MyGroupState extends State<MyGroup> {
err: gFFI.groupModel.groupLoadError, err: gFFI.groupModel.groupLoadError,
retry: null, retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''), close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded(child: isDesktop ? _buildDesktop() : _buildMobile()) Expanded(
child: (isDesktop || isWebDesktop)
? _buildDesktop()
: _buildMobile())
], ],
); );
}); });
@ -164,7 +167,7 @@ class _MyGroupState extends State<MyGroup> {
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return isDesktop return (isDesktop || isWebDesktop)
? listView ? listView
: LimitedBox(maxHeight: maxHeight, child: listView); : LimitedBox(maxHeight: maxHeight, child: listView);
}); });

View File

@ -54,7 +54,7 @@ class DraggableChatWindow extends StatelessWidget {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: CustomAppBar( appBar: CustomAppBar(
onPanUpdate: onPanUpdate, onPanUpdate: onPanUpdate,
appBar: isDesktop appBar: (isDesktop || isWebDesktop)
? _buildDesktopAppBar(context) ? _buildDesktopAppBar(context)
: _buildMobileAppBar(context), : _buildMobileAppBar(context),
), ),

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -52,7 +51,7 @@ class _PeerCardState extends State<_PeerCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (isDesktop) { if (isDesktop || isWebDesktop) {
return _buildDesktop(); return _buildDesktop();
} else { } else {
return _buildMobile(); return _buildMobile();
@ -883,8 +882,7 @@ class RecentPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
if (!isWeb) { if (isDesktop || isWebDesktop) {
// TODO: support web version
menuItems.add(_renameAction(peer.id)); menuItems.add(_renameAction(peer.id));
} }
if (await bind.mainPeerHasPassword(id: peer.id)) { if (await bind.mainPeerHasPassword(id: peer.id)) {
@ -940,8 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
if (!isWeb) { if (isDesktop || isWebDesktop) {
// TODO: support web version
menuItems.add(_renameAction(peer.id)); menuItems.add(_renameAction(peer.id));
} }
if (await bind.mainPeerHasPassword(id: peer.id)) { if (await bind.mainPeerHasPassword(id: peer.id)) {
@ -1046,8 +1043,7 @@ class AddressBookPeerCard extends BasePeerCard {
} }
if (gFFI.abModel.current.canWrite()) { if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
if (!isWeb) { if (isDesktop || isWebDesktop) {
// TODO: support web version
menuItems.add(_renameAction(peer.id)); menuItems.add(_renameAction(peer.id));
} }
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) { if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
@ -1249,7 +1245,7 @@ void _rdpDialog(String id) async {
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: isDesktop ? 8 : 0),
Row( Row(
children: [ children: [
isDesktop (isDesktop || isWebDesktop)
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
@ -1260,15 +1256,17 @@ void _rdpDialog(String id) async {
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: isDesktop ? null : translate('Username')), labelText: (isDesktop || isWebDesktop)
? null
: translate('Username')),
controller: userController, controller: userController,
), ),
), ),
], ],
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
Row( Row(
children: [ children: [
isDesktop (isDesktop || isWebDesktop)
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
@ -1280,7 +1278,9 @@ void _rdpDialog(String id) async {
child: Obx(() => TextField( child: Obx(() => TextField(
obscureText: secure.value, obscureText: secure.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: isDesktop ? null : translate('Password'), labelText: (isDesktop || isWebDesktop)
? null
: translate('Password'),
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () => secure.value = !secure.value, onPressed: () => secure.value = !secure.value,
icon: Icon(secure.value icon: Icon(secure.value

View File

@ -37,7 +37,7 @@ class _TabEntry {
} }
EdgeInsets? _menuPadding() { EdgeInsets? _menuPadding() {
return isDesktop ? kDesktopMenuPadding : null; return (isDesktop || isWebDesktop) ? kDesktopMenuPadding : null;
} }
class _PeerTabPageState extends State<PeerTabPage> class _PeerTabPageState extends State<PeerTabPage>
@ -113,7 +113,9 @@ class _PeerTabPageState extends State<PeerTabPage>
SizedBox( SizedBox(
height: 32, height: 32,
child: Container( child: Container(
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2), padding: (isDesktop || isWebDesktop)
? null
: EdgeInsets.symmetric(horizontal: 2),
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -127,7 +129,7 @@ class _PeerTabPageState extends State<PeerTabPage>
], ],
)), )),
), ),
).paddingOnly(right: isDesktop ? 12 : 0), ).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
_createPeersView(), _createPeersView(),
], ],
); );
@ -195,7 +197,8 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
} }
return Expanded( return Expanded(
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); child: child.marginSymmetric(
vertical: (isDesktop || isWebDesktop) ? 12.0 : 6.0));
} }
Widget _createRefresh( Widget _createRefresh(

View File

@ -78,7 +78,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
LoadEvent.lan: 'empty_lan_tip', LoadEvent.lan: 'empty_lan_tip',
LoadEvent.addressBook: 'empty_address_book_tip', LoadEvent.addressBook: 'empty_address_book_tip',
}); });
final space = isDesktop ? 12.0 : 8.0; final space = (isDesktop || isWebDesktop) ? 12.0 : 8.0;
final _curPeers = <String>{}; final _curPeers = <String>{};
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
@ -200,7 +200,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
Provider.of<PeerTabModel>(context, listen: false).currentTab; Provider.of<PeerTabModel>(context, listen: false).currentTab;
final hideAbTagsPanel = final hideAbTagsPanel =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return isDesktop return (isDesktop || isWebDesktop)
? Obx( ? Obx(
() => SizedBox( () => SizedBox(
width: peerCardUiType.value != PeerUiType.list width: peerCardUiType.value != PeerUiType.list

View File

@ -77,7 +77,7 @@ class _RawTouchGestureDetectorRegionState
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel; FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel; InputModel get inputModel => widget.inputModel;
bool get handleTouch => isDesktop || ffiModel.touchMode; bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
SessionID get sessionId => ffi.sessionId; SessionID get sessionId => ffi.sessionId;
@override @override
@ -183,7 +183,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop || !ffiModel.touchMode) { if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
inputModel.tap(MouseButtons.right); inputModel.tap(MouseButtons.right);
} }
} }
@ -262,7 +262,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt(); final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale; _scale = d.scale;
@ -286,7 +286,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if ((isDesktop || isWebDesktop)) {
bind.sessionSendPointer( bind.sessionSendPointer(
sessionId: sessionId, sessionId: sessionId,
msg: json.encode( msg: json.encode(
@ -409,7 +409,9 @@ class RawPointerMouseRegion extends StatelessWidget {
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
child: MouseRegion( child: MouseRegion(
cursor: cursor ?? MouseCursor.defer, cursor: inputModel.isViewOnly
? MouseCursor.defer
: (cursor ?? MouseCursor.defer),
onEnter: onEnter, onEnter: onEnter,
onExit: onExit, onExit: onExit,
child: child, child: child,

View File

@ -209,10 +209,10 @@ List<Widget> ServerConfigImportExportWidgets(
List<(String, String)> otherDefaultSettings() { List<(String, String)> otherDefaultSettings() {
List<(String, String)> v = [ List<(String, String)> v = [
('View Mode', 'view_only'), ('View Mode', 'view_only'),
if (isDesktop) ('show_monitors_tip', kKeyShowMonitorsToolbar), if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
if (isDesktop) ('Collapse toolbar', 'collapse_toolbar'), if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
('Show remote cursor', 'show_remote_cursor'), ('Show remote cursor', 'show_remote_cursor'),
if (isDesktop) ('Zoom cursor', 'zoom-cursor'), if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
('Show quality monitor', 'show_quality_monitor'), ('Show quality monitor', 'show_quality_monitor'),
('Mute', 'disable_audio'), ('Mute', 'disable_audio'),
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'), if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),

View File

@ -94,7 +94,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')), Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
]), ]),
trailingIcon: Transform.scale( trailingIcon: Transform.scale(
scale: isDesktop ? 0.8 : 1, scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
if (isMobile && Navigator.canPop(context)) { if (isMobile && Navigator.canPop(context)) {
@ -160,7 +160,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// divider // divider
if (isDesktop) { if (isDesktop || isWebDesktop) {
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
} }
// ctrlAltDel // ctrlAltDel
@ -229,7 +229,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
)); ));
} }
// record // record
if (!isDesktop && if (!(isDesktop || isWeb) &&
(ffi.recordingModel.start || (perms["recording"] != false))) { (ffi.recordingModel.start || (perms["recording"] != false))) {
v.add(TTextMenu( v.add(TTextMenu(
child: Row( child: Row(
@ -250,7 +250,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
onPressed: () => ffi.recordingModel.toggle())); onPressed: () => ffi.recordingModel.toggle()));
} }
// fingerprint // fingerprint
if (!isDesktop) { if (!(isDesktop || isWebDesktop)) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Copy Fingerprint')), child: Text(translate('Copy Fingerprint')),
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
@ -511,8 +511,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Show displays as individual windows')))); child: Text(translate('Show displays as individual windows'))));
} }
final screenList = await getScreenRectList(); final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) { if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession( final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) == sessionId: ffi.sessionId) ==
'Y'; 'Y';

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';

View File

@ -114,7 +114,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
if (!bind.isIncomingOnly()) if (!bind.isIncomingOnly())
_TabInfo( _TabInfo(
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows), 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows),
if (!bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_TabInfo('Plugin', Icons.extension_outlined, Icons.extension), _TabInfo('Plugin', Icons.extension_outlined, Icons.extension),
if (!bind.isDisableAccount()) if (!bind.isDisableAccount())
_TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('Account', Icons.person_outline, Icons.person),
@ -129,7 +129,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(), if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
if (!bind.isDisableSettings()) _Network(), if (!bind.isDisableSettings()) _Network(),
if (!bind.isIncomingOnly()) _Display(), if (!bind.isIncomingOnly()) _Display(),
if (!bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) _Plugin(), if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_Plugin(),
if (!bind.isDisableAccount()) _Account(), if (!bind.isDisableAccount()) _Account(),
_About(), _About(),
]; ];

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';

View File

@ -35,13 +35,13 @@ class RemotePage extends StatefulWidget {
RemotePage({ RemotePage({
Key? key, Key? key,
required this.id, required this.id,
required this.sessionId,
required this.tabWindowId,
required this.display,
required this.displays,
required this.password,
required this.toolbarState, required this.toolbarState,
required this.tabController, this.sessionId,
this.tabWindowId,
this.password,
this.display,
this.displays,
this.tabController,
this.switchUuid, this.switchUuid,
this.forceRelay, this.forceRelay,
this.isSharedPassword, this.isSharedPassword,
@ -58,7 +58,7 @@ class RemotePage extends StatefulWidget {
final bool? forceRelay; final bool? forceRelay;
final bool? isSharedPassword; final bool? isSharedPassword;
final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null); final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
final DesktopTabController tabController; final DesktopTabController? tabController;
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
@ -129,7 +129,7 @@ class _RemotePageState extends State<RemotePage>
} }
_ffi.ffiModel.updateEventListener(sessionId, widget.id); _ffi.ffiModel.updateEventListener(sessionId, widget.id);
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
// Session option should be set after models.dart/FFI.start // Session option should be set after models.dart/FFI.start
_showRemoteCursor.value = bind.sessionGetToggleOptionSync( _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
@ -150,7 +150,7 @@ class _RemotePageState extends State<RemotePage>
// } // }
_blockableOverlayState.applyFfi(_ffi); _blockableOverlayState.applyFfi(_ffi);
widget.tabController.onSelected?.call(widget.id); widget.tabController?.onSelected?.call(widget.id);
} }
@override @override
@ -431,9 +431,9 @@ class _RemotePageState extends State<RemotePage>
Widget getBodyForDesktop(BuildContext context) { Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[ var paints = <Widget>[
MouseRegion(onEnter: (evt) { MouseRegion(onEnter: (evt) {
bind.hostStopSystemKeyPropagate(stopped: false); if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
}, onExit: (evt) { }, onExit: (evt) {
bind.hostStopSystemKeyPropagate(stopped: true); if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
}, child: LayoutBuilder(builder: (context, constraints) { }, child: LayoutBuilder(builder: (context, constraints) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
Provider.of<CanvasModel>(context, listen: false).updateViewStyle(); Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
@ -669,6 +669,11 @@ class _ImagePaintState extends State<ImagePaint> {
MouseCursor _buildCursorOfCache( MouseCursor _buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) { CursorModel cursor, double scale, CursorData? cache) {
// TODO: web cursor
if (isWeb) {
return MouseCursor.defer;
}
if (cache == null) { if (cache == null) {
return MouseCursor.defer; return MouseCursor.defer;
} else { } else {

View File

@ -491,7 +491,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
} }
toolbarItems.add(_RecordMenu()); if (!isWeb) toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0)); final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
return Column( return Column(
@ -940,13 +940,12 @@ class ScreenAdjustor {
} }
updateScreen() async { updateScreen() async {
final v = await rustDeskWinManager.call( final String info =
WindowType.Main, kWindowGetWindowInfo, ''); isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
final String valueStr = v.result; if (info.isEmpty) {
if (valueStr.isEmpty) {
_screen = null; _screen = null;
} else { } else {
final screenMap = jsonDecode(valueStr); final screenMap = jsonDecode(info);
_screen = window_size.Screen( _screen = window_size.Screen(
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'], Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
screenMap['frame']['r'], screenMap['frame']['b']), screenMap['frame']['r'], screenMap['frame']['b']),
@ -959,15 +958,23 @@ class ScreenAdjustor {
} }
} }
_getScreenInfoDesktop() async {
final v = await rustDeskWinManager.call(
WindowType.Main, kWindowGetWindowInfo, '');
return v.result;
}
Future<bool> isWindowCanBeAdjusted() async { Future<bool> isWindowCanBeAdjusted() async {
final viewStyle = final viewStyle =
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? ''; await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
if (viewStyle != kRemoteViewStyleOriginal) { if (viewStyle != kRemoteViewStyleOriginal) {
return false; return false;
} }
final remoteCount = RemoteCountState.find().value; if (!isWeb) {
if (remoteCount != 1) { final remoteCount = RemoteCountState.find().value;
return false; if (remoteCount != 1) {
return false;
}
} }
if (_screen == null) { if (_screen == null) {
return false; return false;
@ -1325,6 +1332,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
final display = json.decode(mainDisplay); final display = json.decode(mainDisplay);
if (display['w'] != null && display['h'] != null) { if (display['w'] != null && display['h'] != null) {
_localResolution = Resolution(display['w'], display['h']); _localResolution = Resolution(display['w'], display['h']);
if (isWeb) {
if (display['scaleFactor'] != null) {
_localResolution = Resolution(
(display['w'] / display['scaleFactor']).toInt(),
(display['h'] / display['scaleFactor']).toInt(),
);
}
}
} }
} catch (e) { } catch (e) {
debugPrint('Failed to decode $mainDisplay, $e'); debugPrint('Failed to decode $mainDisplay, $e');

View File

@ -125,10 +125,7 @@ void runMainApp(bool startService) async {
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
if (isWeb) {
// Web does not support window manager.
return;
}
// Set window option. // Set window option.
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.waitUntilReadyToShow(windowOptions, () async {

View File

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:toggle_switch/toggle_switch.dart'; import 'package:toggle_switch/toggle_switch.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';

View File

@ -193,6 +193,7 @@ class InputModel {
bool get keyboardPerm => parent.target!.ffiModel.keyboard; bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? ''; String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform; String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
InputModel(this.parent) { InputModel(this.parent) {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
@ -207,7 +208,7 @@ class InputModel {
updateKeyboardMode() async { updateKeyboardMode() async {
// * Currently mobile does not enable map mode // * Currently mobile does not enable map mode
if (isDesktop) { if (isDesktop || isWebDesktop) {
if (keyboardMode.isEmpty) { if (keyboardMode.isEmpty) {
keyboardMode = keyboardMode =
await bind.sessionGetKeyboardMode(sessionId: sessionId) ?? await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
@ -217,7 +218,8 @@ class InputModel {
} }
KeyEventResult handleRawKeyEvent(RawKeyEvent e) { KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isDesktop && !isInputSourceFlutter) { if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -256,7 +258,7 @@ class InputModel {
} }
// * Currently mobile does not enable map mode // * Currently mobile does not enable map mode
if (isDesktop && keyboardMode == 'map') { if ((isDesktop || isWebDesktop) && keyboardMode == 'map') {
mapKeyboardMode(e); mapKeyboardMode(e);
} else { } else {
legacyKeyboardMode(e); legacyKeyboardMode(e);
@ -467,6 +469,7 @@ class InputModel {
void onPointHoverImage(PointerHoverEvent e) { void onPointHoverImage(PointerHoverEvent e) {
_stopFling = true; _stopFling = true;
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (!isPhysicalMouse.value) { if (!isPhysicalMouse.value) {
isPhysicalMouse.value = true; isPhysicalMouse.value = true;
@ -479,7 +482,7 @@ class InputModel {
void onPointerPanZoomStart(PointerPanZoomStartEvent e) { void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
_lastScale = 1.0; _lastScale = 1.0;
_stopFling = true; _stopFling = true;
if (isViewOnly) return;
if (peerPlatform == kPeerPlatformAndroid) { if (peerPlatform == kPeerPlatformAndroid) {
handlePointerEvent('touch', 'pan_start', e.position); handlePointerEvent('touch', 'pan_start', e.position);
} }
@ -487,6 +490,7 @@ class InputModel {
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
if (isViewOnly) return;
if (peerPlatform != kPeerPlatformAndroid) { if (peerPlatform != kPeerPlatformAndroid) {
final scale = ((e.scale - _lastScale) * 1000).toInt(); final scale = ((e.scale - _lastScale) * 1000).toInt();
_lastScale = e.scale; _lastScale = e.scale;
@ -612,6 +616,7 @@ class InputModel {
void onPointDownImage(PointerDownEvent e) { void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}"); debugPrint("onPointDownImage ${e.kind}");
_stopFling = true; _stopFling = true;
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
isPhysicalMouse.value = false; isPhysicalMouse.value = false;
@ -623,6 +628,7 @@ class InputModel {
} }
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
@ -630,6 +636,7 @@ class InputModel {
} }
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
@ -637,6 +644,7 @@ class InputModel {
} }
void onPointerSignalImage(PointerSignalEvent e) { void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return;
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt(); var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt(); var dy = e.scrollDelta.dy.toInt();
@ -902,9 +910,11 @@ class InputModel {
int minX = rect.left.toInt(); int minX = rect.left.toInt();
// https://github.com/rustdesk/rustdesk/issues/6678 // https://github.com/rustdesk/rustdesk/issues/6678
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping. // For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
int maxX = (rect.left + rect.width).toInt() - (peerPlatform == kPeerPlatformWindows ? 0 : 1); int maxX = (rect.left + rect.width).toInt() -
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
int minY = rect.top.toInt(); int minY = rect.top.toInt();
int maxY = (rect.top + rect.height).toInt() - (peerPlatform == kPeerPlatformWindows ? 0 : 1); int maxY = (rect.top + rect.height).toInt() -
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
evtX = trySetNearestRange(evtX, minX, maxX, 5); evtX = trySetNearestRange(evtX, minX, maxX, 5);
evtY = trySetNearestRange(evtY, minY, maxY, 5); evtY = trySetNearestRange(evtY, minY, maxY, 5);
if (kind == kPointerEventKindMouse) { if (kind == kPointerEventKindMouse) {

View File

@ -429,7 +429,7 @@ class FfiModel with ChangeNotifier {
} }
handleAliasChanged(Map<String, dynamic> evt) { handleAliasChanged(Map<String, dynamic> evt) {
if (!isDesktop) return; if (!(isDesktop || isWebDesktop)) return;
final String peerId = evt['id']; final String peerId = evt['id'];
final String alias = evt['alias']; final String alias = evt['alias'];
String label = getDesktopTabLabel(peerId, alias); String label = getDesktopTabLabel(peerId, alias);
@ -767,7 +767,7 @@ class FfiModel with ChangeNotifier {
_pi.isSet.value = true; _pi.isSet.value = true;
stateGlobal.resetLastResolutionGroupValues(peerId); stateGlobal.resetLastResolutionGroupValues(peerId);
if (isDesktop) { if (isDesktop || isWebDesktop) {
checkDesktopKeyboardMode(); checkDesktopKeyboardMode();
} }
@ -1114,7 +1114,7 @@ class ImageModel with ChangeNotifier {
update(ui.Image? image) async { update(ui.Image? image) async {
if (_image == null && image != null) { if (_image == null && image != null) {
if (isWebDesktop || isDesktop) { if (isDesktop || isWebDesktop) {
await parent.target?.canvasModel.updateViewStyle(); await parent.target?.canvasModel.updateViewStyle();
await parent.target?.canvasModel.updateScrollStyle(); await parent.target?.canvasModel.updateScrollStyle();
} else { } else {
@ -1288,18 +1288,15 @@ class CanvasModel with ChangeNotifier {
double get scrollX => _scrollX; double get scrollX => _scrollX;
double get scrollY => _scrollY; double get scrollY => _scrollY;
static double get leftToEdge => (isDesktop || isWebDesktop) static double get leftToEdge =>
? windowBorderWidth + kDragToResizeAreaPadding.left isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.left : 0;
: 0; static double get rightToEdge =>
static double get rightToEdge => (isDesktop || isWebDesktop) isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.right : 0;
? windowBorderWidth + kDragToResizeAreaPadding.right static double get topToEdge => isDesktop
: 0;
static double get topToEdge => (isDesktop || isWebDesktop)
? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top ? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top
: 0; : 0;
static double get bottomToEdge => (isDesktop || isWebDesktop) static double get bottomToEdge =>
? windowBorderWidth + kDragToResizeAreaPadding.bottom isDesktop ? windowBorderWidth + kDragToResizeAreaPadding.bottom : 0;
: 0;
updateViewStyle({refreshMousePos = true}) async { updateViewStyle({refreshMousePos = true}) async {
Size getSize() { Size getSize() {
@ -1422,7 +1419,7 @@ class CanvasModel with ChangeNotifier {
// If keyboard is not permitted, do not move cursor when mouse is moving. // If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null && parent.target!.ffiModel.keyboard) { if (parent.target != null && parent.target!.ffiModel.keyboard) {
// Draw cursor if is not desktop. // Draw cursor if is not desktop.
if (!isDesktop) { if (!(isDesktop || isWebDesktop)) {
parent.target!.cursorModel.moveLocal(x, y); parent.target!.cursorModel.moveLocal(x, y);
} else { } else {
try { try {
@ -2495,7 +2492,8 @@ class PeerInfo with ChangeNotifier {
List<int> get virtualDisplays => List<int>.from( List<int> get virtualDisplays => List<int>.from(
platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []); platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []);
bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession; bool get isSupportMultiDisplay =>
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false; bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;

View File

@ -98,9 +98,11 @@ class PlatformFFI {
sessionId: sessionId, display: display, ptr: ptr); sessionId: sessionId, display: display, ptr: ptr);
Future<void> init(String appType) async { Future<void> init(String appType) async {
isWebDesktop = !context.callMethod('isMobile');
context.callMethod('init'); context.callMethod('init');
version = getByName('version'); version = getByName('version');
window.onContextMenu.listen((event) {
event.preventDefault();
});
context['onRegisteredEvent'] = (String message) { context['onRegisteredEvent'] = (String message) {
try { try {

View File

@ -6,5 +6,8 @@ final isWindows_ = Platform.isWindows;
final isMacOS_ = Platform.isMacOS; final isMacOS_ = Platform.isMacOS;
final isLinux_ = Platform.isLinux; final isLinux_ = Platform.isLinux;
final isWeb_ = false; final isWeb_ = false;
final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux; final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => '';

View File

@ -3,6 +3,8 @@ import 'dart:ui' as ui;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
Future<ui.Image> decodeImageFromPixels( Future<ui.Image> decodeImageFromPixels(
Uint8List pixels, Uint8List pixels,
int width, int width,
@ -79,6 +81,11 @@ class ImagePainter extends CustomPainter {
paint.filterQuality = FilterQuality.high; paint.filterQuality = FilterQuality.high;
} }
} }
// It's strange that if (scale < 0.5 && paint.filterQuality == FilterQuality.medium)
// The canvas.drawImage will not work on web
if (isWeb) {
paint.filterQuality = FilterQuality.high;
}
canvas.drawImage( canvas.drawImage(
image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
} }

View File

@ -5,6 +5,8 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:flutter_hbb/consts.dart';
final _privateConstructorUsedError = UnsupportedError( final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
@ -237,7 +239,6 @@ class RustdeskImpl {
Future<String?> sessionGetViewStyle( Future<String?> sessionGetViewStyle(
{required UuidValue sessionId, dynamic hint}) { {required UuidValue sessionId, dynamic hint}) {
// TODO: default values
return Future(() => return Future(() =>
js.context.callMethod('getByName', ['option:session', 'view_style'])); js.context.callMethod('getByName', ['option:session', 'view_style']));
} }
@ -252,7 +253,6 @@ class RustdeskImpl {
Future<String?> sessionGetScrollStyle( Future<String?> sessionGetScrollStyle(
{required UuidValue sessionId, dynamic hint}) { {required UuidValue sessionId, dynamic hint}) {
// TODO: default values
return Future(() => return Future(() =>
js.context.callMethod('getByName', ['option:session', 'scroll_style'])); js.context.callMethod('getByName', ['option:session', 'scroll_style']));
} }
@ -266,9 +266,7 @@ class RustdeskImpl {
} }
Future<String?> sessionGetImageQuality( Future<String?> sessionGetImageQuality(
// TODO: default values {required UuidValue sessionId, dynamic hint}) {
{required UuidValue sessionId,
dynamic hint}) {
return Future(() => js.context return Future(() => js.context
.callMethod('getByName', ['option:session', 'image_quality'])); .callMethod('getByName', ['option:session', 'image_quality']));
} }
@ -283,9 +281,9 @@ class RustdeskImpl {
Future<String?> sessionGetKeyboardMode( Future<String?> sessionGetKeyboardMode(
{required UuidValue sessionId, dynamic hint}) { {required UuidValue sessionId, dynamic hint}) {
// TODO: default values final mode =
return Future(() => js.context js.context.callMethod('getByName', ['option:session', 'keyboard_mode']);
.callMethod('getByName', ['option:session', 'keyboard_mode'])); return Future(() => mode == '' ? null : mode);
} }
Future<void> sessionSetKeyboardMode( Future<void> sessionSetKeyboardMode(
@ -345,7 +343,7 @@ class RustdeskImpl {
bool sessionIsKeyboardModeSupported( bool sessionIsKeyboardModeSupported(
{required UuidValue sessionId, required String mode, dynamic hint}) { {required UuidValue sessionId, required String mode, dynamic hint}) {
throw UnimplementedError(); return mode == kKeyLegacyMode;
} }
Future<void> sessionSetCustomImageQuality( Future<void> sessionSetCustomImageQuality(
@ -748,8 +746,7 @@ class RustdeskImpl {
} }
Future<void> mainCheckConnectStatus({dynamic hint}) { Future<void> mainCheckConnectStatus({dynamic hint}) {
return Future( throw UnimplementedError();
() => js.context.callMethod('setByName', ["check_conn_status"]));
} }
Future<bool> mainIsUsingPublicServer({dynamic hint}) { Future<bool> mainIsUsingPublicServer({dynamic hint}) {
@ -929,12 +926,14 @@ class RustdeskImpl {
Future<void> mainSetUserDefaultOption( Future<void> mainSetUserDefaultOption(
{required String key, required String value, dynamic hint}) { {required String key, required String value, dynamic hint}) {
// TODO: do we need the default option? return js.context.callMethod('getByName', [
throw UnimplementedError(); 'option:user:default',
jsonEncode({'name': key, 'value': value})
]);
} }
String mainGetUserDefaultOption({required String key, dynamic hint}) { String mainGetUserDefaultOption({required String key, dynamic hint}) {
throw UnimplementedError(); return js.context.callMethod('getByName', ['option:user:default', key]);
} }
Future<String> mainHandleRelayId({required String id, dynamic hint}) { Future<String> mainHandleRelayId({required String id, dynamic hint}) {
@ -946,7 +945,7 @@ class RustdeskImpl {
} }
String mainGetMainDisplay({dynamic hint}) { String mainGetMainDisplay({dynamic hint}) {
throw UnimplementedError(); return js.context.callMethod('getByName', ['main_display']);
} }
String mainGetDisplays({dynamic hint}) { String mainGetDisplays({dynamic hint}) {
@ -1399,7 +1398,7 @@ class RustdeskImpl {
} }
bool mainHasPixelbufferTextureRender({dynamic hint}) { bool mainHasPixelbufferTextureRender({dynamic hint}) {
throw UnimplementedError(); return false;
} }
bool mainHasFileClipboard({dynamic hint}) { bool mainHasFileClipboard({dynamic hint}) {
@ -1553,7 +1552,9 @@ class RustdeskImpl {
} }
String mainSupportedInputSource({dynamic hint}) { String mainSupportedInputSource({dynamic hint}) {
return jsonEncode(['Input source 2', 'input_source_2_tip']); return jsonEncode([
['Input source 2', 'input_source_2_tip']
]);
} }
Future<String> mainGenerate2Fa({dynamic hint}) { Future<String> mainGenerate2Fa({dynamic hint}) {

View File

@ -1,3 +1,4 @@
import 'dart:js' as js;
final isAndroid_ = false; final isAndroid_ = false;
final isIOS_ = false; final isIOS_ = false;
@ -5,5 +6,8 @@ final isWindows_ = false;
final isMacOS_ = false; final isMacOS_ = false;
final isLinux_ = false; final isLinux_ = false;
final isWeb_ = true; final isWeb_ = true;
final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false; final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);

View File

@ -652,7 +652,7 @@ export default class Connection {
} }
getOption(name: string): any { getOption(name: string): any {
return this._options[name]; return this._options[name] ?? globals.getUserDefaultOption(name);
} }
getToggleOption(name: string): Boolean { getToggleOption(name: string): Boolean {
@ -839,6 +839,52 @@ export default class Connection {
} }
toggleOption(name: string) { toggleOption(name: string) {
// } else if name == "block-input" {
// option.block_input = BoolOption::Yes.into();
// } else if name == "unblock-input" {
// option.block_input = BoolOption::No.into();
// } else if name == "show-quality-monitor" {
// config.show_quality_monitor.v = !config.show_quality_monitor.v;
// } else if name == "allow_swap_key" {
// config.allow_swap_key.v = !config.allow_swap_key.v;
// } else if name == "view-only" {
// config.view_only.v = !config.view_only.v;
// let f = |b: bool| {
// if b {
// BoolOption::Yes.into()
// } else {
// BoolOption::No.into()
// }
// };
// if config.view_only.v {
// option.disable_keyboard = f(true);
// option.disable_clipboard = f(true);
// option.show_remote_cursor = f(true);
// option.enable_file_transfer = f(false);
// option.lock_after_session_end = f(false);
// } else {
// option.disable_keyboard = f(false);
// option.disable_clipboard = f(self.get_toggle_option("disable-clipboard"));
// option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor"));
// option.enable_file_transfer = f(self.config.enable_file_transfer.v);
// option.lock_after_session_end = f(self.config.lock_after_session_end.v);
// }
// } else {
// let is_set = self
// .options
// .get(&name)
// .map(|o| !o.is_empty())
// .unwrap_or(false);
// if is_set {
// self.config.options.remove(&name);
// } else {
// self.config.options.insert(name, "Y".to_owned());
// }
// self.config.store(&self.id);
// return None;
// }
const v = !this._options[name]; const v = !this._options[name];
const option = message.OptionMessage.fromPartial({}); const option = message.OptionMessage.fromPartial({});
const v2 = v const v2 = v
@ -860,13 +906,43 @@ export default class Connection {
case "privacy-mode": case "privacy-mode":
option.privacy_mode = v2; option.privacy_mode = v2;
break; break;
case "enable-file-transfer":
option.enable_file_transfer = v2;
break;
case "block-input": case "block-input":
option.block_input = message.OptionMessage_BoolOption.Yes; option.block_input = message.OptionMessage_BoolOption.Yes;
break; break;
case "unblock-input": case "unblock-input":
option.block_input = message.OptionMessage_BoolOption.No; option.block_input = message.OptionMessage_BoolOption.No;
break; break;
case "show-quality-monitor":
case "allow-swap-key":
break;
case "view-only":
if (v) {
option.disable_keyboard = message.OptionMessage_BoolOption.Yes;
option.disable_clipboard = message.OptionMessage_BoolOption.Yes;
option.show_remote_cursor = message.OptionMessage_BoolOption.Yes;
option.enable_file_transfer = message.OptionMessage_BoolOption.No;
option.lock_after_session_end = message.OptionMessage_BoolOption.No;
} else {
option.disable_keyboard = message.OptionMessage_BoolOption.No;
option.disable_clipboard = this.getToggleOption("disable-clipboard")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
option.enable_file_transfer = this.getToggleOption("enable-file-transfer")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
? message.OptionMessage_BoolOption.Yes
: message.OptionMessage_BoolOption.No;
}
break;
default: default:
this.setOption(name, this._options[name] ? undefined : "Y");
return; return;
} }
if (name.indexOf("block-input") < 0) this.setOption(name, v); if (name.indexOf("block-input") < 0) this.setOption(name, v);

View File

@ -211,7 +211,7 @@ window.setByName = (name, value) => {
curConn.refresh(); curConn.refresh();
break; break;
case 'reconnect': case 'reconnect':
curConn.reconnect(); curConn?.reconnect();
break; break;
case 'toggle_option': case 'toggle_option':
curConn.toggleOption(value); curConn.toggleOption(value);
@ -244,6 +244,7 @@ window.setByName = (name, value) => {
curConn.inputString(value); curConn.inputString(value);
break; break;
case 'send_mouse': case 'send_mouse':
if (!curConn) return;
let mask = 0; let mask = 0;
value = JSON.parse(value); value = JSON.parse(value);
switch (value.type) { switch (value.type) {
@ -288,6 +289,9 @@ window.setByName = (name, value) => {
value = JSON.parse(value); value = JSON.parse(value);
localStorage.setItem(name + ':' + value.name, value.value); localStorage.setItem(name + ':' + value.name, value.value);
break; break;
case 'option:user:default':
setUserDefaultOption(value);
break;
case 'option:session': case 'option:session':
value = JSON.parse(value); value = JSON.parse(value);
curConn.setOption(value.name, value.value); curConn.setOption(value.name, value.value);
@ -295,12 +299,11 @@ window.setByName = (name, value) => {
case 'option:peer': case 'option:peer':
setPeerOption(value); setPeerOption(value);
break; break;
case 'option:toggle':
return curConn.toggleOption(value);
case 'input_os_password': case 'input_os_password':
curConn.inputOsPassword(value); curConn.inputOsPassword(value);
break; break;
case 'check_conn_status':
curConn.checkConnStatus();
break;
case 'session_add_sync': case 'session_add_sync':
return sessionAdd(value); return sessionAdd(value);
case 'session_start': case 'session_start':
@ -374,8 +377,14 @@ function _getByName(name, arg) {
case 'translate': case 'translate':
arg = JSON.parse(arg); arg = JSON.parse(arg);
return translate(arg.locale, arg.text); return translate(arg.locale, arg.text);
case 'option:user:default':
return getUserDefaultOption(arg);
case 'option:session': case 'option:session':
return curConn.getOption(arg); if (curConn) {
return curConn.getOption(arg);
} else {
return getUserDefaultOption(arg);
}
case 'option:peer': case 'option:peer':
return getPeerOption(arg); return getPeerOption(arg);
case 'option:toggle': case 'option:toggle':
@ -412,6 +421,28 @@ function _getByName(name, arg) {
return getAuditServer(arg); return getAuditServer(arg);
case 'alternative_codecs': case 'alternative_codecs':
return getAlternativeCodecs(); return getAlternativeCodecs();
case 'screen_info':
return JSON.stringify({
frame: {
l: window.screenX,
t: window.screenY,
r: window.screenX + window.innerWidth,
b: window.screenY + window.innerHeight,
},
visibleFrame: {
l: window.screen.availLeft,
t: window.screen.availTop,
r: window.screen.availLeft + window.screen.availWidth,
b: window.screen.availTop + window.screen.availHeight,
},
scaleFactor: window.devicePixelRatio,
});
case 'main_display':
return JSON.stringify({
w: window.screen.availWidth,
h: window.screen.availHeight,
scaleFactor: window.devicePixelRatio,
});
} }
return ''; return '';
} }
@ -521,20 +552,52 @@ export function getVersionNumber(v) {
} }
} }
// ========================== options begin ==========================
function setUserDefaultOption(value) {
try {
const ojb = JSON.parse(value);
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
userDefaultOptions[ojb.name] = ojb.value;
localStorage.setItem('user-default-options', JSON.stringify(userDefaultOptions));
}
catch (e) {
console.error('Failed to set user default options: ' + e.message);
}
}
export function getUserDefaultOption(value) {
const defaultOptions = {
'view_style': 'original',
'scroll_style': 'scrollauto',
'image_quality': 'balanced',
'codec-preference': 'auto',
'custom_image_quality': '50',
'custom-fps': '30',
};
try {
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
return userDefaultOptions[value] || defaultOptions[value] || '';
}
catch (e) {
console.error('Failed to get user default options: ' + e.message);
return defaultOptions[value] || '';
}
}
function getPeerOption(value) { function getPeerOption(value) {
try { try {
const obj = JSON.parse(value); const obj = JSON.parse(value);
const options = getPeers()[obj.id] || {}; const options = getPeers()[obj.id] || {};
return options[obj.name] || ''; return options[obj.name] ?? getUserDefaultOption(obj.name);
} }
catch (e) { catch (e) {
console.error('Failed to get peer option: "' + value + '", ' + e.message); console.error('Failed to get peer option: "' + value + '", ' + e.message);
} }
} }
function setPeerOption(value) { function setPeerOption(param) {
try { try {
const obj = JSON.parse(value); const obj = JSON.parse(param);
const id = obj.id; const id = obj.id;
const name = obj.name; const name = obj.name;
const value = obj.value; const value = obj.value;
@ -554,6 +617,7 @@ function setPeerOption(value) {
console.error('Failed to set peer option: "' + value + '", ' + e.message); console.error('Failed to set peer option: "' + value + '", ' + e.message);
} }
} }
// ========================= options end ===========================
// ========================== peers begin ========================== // ========================== peers begin ==========================
function getRecentPeers() { function getRecentPeers() {
@ -668,10 +732,10 @@ function increasePort(host, offset) {
function getAlternativeCodecs() { function getAlternativeCodecs() {
return JSON.stringify({ return JSON.stringify({
vp8: 1, vp8: true,
av1: 0, av1: false,
h264: 1, h264: false,
h265: 1, h265: false,
}); });
} }
// ========================== settings end =========================== // ========================== settings end ===========================