diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 79bf6426a..a2a1a02a3 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -32,7 +32,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 32 sourceSets { main.java.srcDirs += 'src/main/kotlin' } diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ddce41a22..dbd15d436 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -199,7 +199,7 @@ const G = M * K; String readableFileSize(double size) { if (size < K) { - return size.toString() + " B"; + return size.toStringAsFixed(2) + " B"; } else if (size < M) { return (size / K).toStringAsFixed(2) + " KB"; } else if (size < G) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index a81a047b4..9ca9039f0 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -39,7 +39,7 @@ class App extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: !isAndroid ? WebHomePage() : HomePage(), + home: !isAndroid ? WebHomePage() : HomePage(key: homeKey), navigatorObservers: [ FirebaseAnalyticsObserver(analytics: analytics), FlutterSmartDialog.observer diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index eaf8d2243..149b60a46 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:dash_chat/dash_chat.dart'; +import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; import '../widgets/overlay.dart'; @@ -11,8 +11,8 @@ class MessageBody { List chatMessages; MessageBody(this.chatUser, this.chatMessages); - void add(ChatMessage cm) { - this.chatMessages.add(cm); + void insert(ChatMessage cm) { + this.chatMessages.insert(0, cm); } void clear() { @@ -24,19 +24,15 @@ class ChatModel with ChangeNotifier { static final clientModeID = -1; final ChatUser me = ChatUser( - uid: "", - name: "Me", + id: "", + firstName: "Me", ); late final Map _messages = Map() ..[clientModeID] = MessageBody(me, []); - final _scroller = ScrollController(); - var _currentID = clientModeID; - ScrollController get scroller => _scroller; - Map get messages => _messages; int get currentID => _currentID; @@ -62,8 +58,8 @@ class ChatModel with ChangeNotifier { "Failed to changeCurrentID,remote user doesn't exist"); } final chatUser = ChatUser( - uid: client.peerId, - name: client.name, + id: client.peerId, + firstName: client.name, ); _messages[id] = MessageBody(chatUser, []); _currentID = id; @@ -80,48 +76,39 @@ class ChatModel with ChangeNotifier { late final chatUser; if (id == clientModeID) { chatUser = ChatUser( - name: FFI.ffiModel.pi.username, - uid: FFI.getId(), + firstName: FFI.ffiModel.pi.username, + id: FFI.getId(), ); } else { final client = FFI.serverModel.clients[id]; if (client == null) { return debugPrint("Failed to receive msg,user doesn't exist"); } - chatUser = ChatUser(uid: client.peerId, name: client.name); + chatUser = ChatUser(id: client.peerId, firstName: client.name); } if (!_messages.containsKey(id)) { _messages[id] = MessageBody(chatUser, []); } - _messages[id]!.add(ChatMessage(text: text, user: chatUser)); + _messages[id]!.insert( + ChatMessage(text: text, user: chatUser, createdAt: DateTime.now())); _currentID = id; notifyListeners(); - scrollToBottom(); - } - - scrollToBottom() { - Future.delayed(Duration(milliseconds: 500), () { - _scroller.animateTo(_scroller.position.maxScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.fastLinearToSlowEaseIn); - }); } send(ChatMessage message) { - if (message.text != null && message.text!.isNotEmpty) { - _messages[_currentID]?.add(message); + if (message.text.isNotEmpty) { + _messages[_currentID]?.insert(message); if (_currentID == clientModeID) { - FFI.setByName("chat_client_mode", message.text!); + FFI.setByName("chat_client_mode", message.text); } else { final msg = Map() ..["id"] = _currentID - ..["text"] = message.text!; + ..["text"] = message.text; FFI.setByName("chat_server_mode", jsonEncode(msg)); } } notifyListeners(); - scrollToBottom(); } close() { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index e4afc892f..4a96e6e73 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -199,6 +199,7 @@ class FileModel extends ChangeNotifier { onClose() { SmartDialog.dismiss(); + jobReset(); // save config Map msg = Map(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2f6007400..4e1c76e4a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -68,7 +68,7 @@ class FfiModel with ChangeNotifier { void updatePermission(Map evt) { evt.forEach((k, v) { - if (k == 'name') return; + if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; }); print('$_permissions'); @@ -162,6 +162,8 @@ class FfiModel with ChangeNotifier { FFI.serverModel.onClientAuthorized(evt); } else if (name == 'on_client_remove') { FFI.serverModel.onClientRemove(evt); + } else if (name == 'update_quality_status') { + FFI.qualityMonitorModel.updateQualityStatus(evt); } }; PlatformFFI.setEventCallback(cb); @@ -193,14 +195,17 @@ class FfiModel with ChangeNotifier { wrongPasswordDialog(id); } else if (type == 'input-password') { enterPasswordDialog(id); + } else if (type == 'restarting') { + showMsgBox(type, title, text, false, hasCancel: false); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(type, title, text, hasRetry); } } - void showMsgBox(String type, String title, String text, bool hasRetry) { - msgBox(type, title, text); + void showMsgBox(String type, String title, String text, bool hasRetry, + {bool? hasCancel}) { + msgBox(type, title, text, hasCancel: hasCancel); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { @@ -655,6 +660,44 @@ class CursorModel with ChangeNotifier { } } +class QualityMonitorData { + String? speed; + String? fps; + String? delay; + String? targetBitrate; + String? codecFormat; +} + +class QualityMonitorModel with ChangeNotifier { + var _show = FFI.getByName('toggle_option', 'show-quality-monitor') == 'true'; + final _data = QualityMonitorData(); + + bool get show => _show; + QualityMonitorData get data => _data; + + checkShowQualityMonitor() { + final show = + FFI.getByName('toggle_option', 'show-quality-monitor') == 'true'; + if (_show != show) { + _show = show; + notifyListeners(); + } + } + + updateQualityStatus(Map evt) { + try { + if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"]; + if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"]; + if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"]; + if ((evt["target_bitrate"] as String).isNotEmpty) + _data.targetBitrate = evt["target_bitrate"]; + if ((evt["codec_format"] as String).isNotEmpty) + _data.codecFormat = evt["codec_format"]; + notifyListeners(); + } catch (e) {} + } +} + enum MouseButtons { left, right, wheel } extension ToString on MouseButtons { @@ -684,6 +727,7 @@ class FFI { static final serverModel = ServerModel(); static final chatModel = ChatModel(); static final fileModel = FileModel(); + static final qualityMonitorModel = QualityMonitorModel(); static String getId() { return getByName('remote_id'); diff --git a/flutter/lib/pages/chat_page.dart b/flutter/lib/pages/chat_page.dart index af940a29e..ce139d062 100644 --- a/flutter/lib/pages/chat_page.dart +++ b/flutter/lib/pages/chat_page.dart @@ -1,4 +1,4 @@ -import 'package:dash_chat/dash_chat.dart'; +import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; @@ -6,8 +6,6 @@ import 'package:provider/provider.dart'; import '../models/model.dart'; import 'home_page.dart'; -ChatPage chatPage = ChatPage(); - class ChatPage extends StatelessWidget implements PageShape { @override final title = translate("Chat"); @@ -25,7 +23,7 @@ class ChatPage extends StatelessWidget implements PageShape { final id = entry.key; final user = entry.value.chatUser; return PopupMenuItem( - child: Text("${user.name} ${user.uid}"), + child: Text("${user.firstName} ${user.id}"), value: id, ); }).toList(); @@ -46,19 +44,24 @@ class ChatPage extends StatelessWidget implements PageShape { return Stack( children: [ DashChat( - inputContainerStyle: BoxDecoration(color: Colors.white70), - sendOnEnter: false, - // if true,reload keyboard everytime,need fix onSend: (chatMsg) { chatModel.send(chatMsg); }, - user: chatModel.me, + currentUser: chatModel.me, messages: chatModel.messages[chatModel.currentID]?.chatMessages ?? [], - // default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53 - scrollToBottom: false, - scrollController: chatModel.scroller, + messageOptions: MessageOptions( + showOtherUsersAvatar: false, + showTime: true, + messageDecorationBuilder: (_, __, ___) => + defaultMessageDecoration( + color: MyTheme.accent80, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: 8, + borderBottomLeft: 8, + )), ), chatModel.currentID == ChatModel.clientModeID ? SizedBox.shrink() @@ -70,7 +73,7 @@ class ChatPage extends StatelessWidget implements PageShape { color: MyTheme.accent80), SizedBox(width: 5), Text( - "${currentUser.name ?? ""} ${currentUser.uid ?? ""}", + "${currentUser.firstName} ${currentUser.id}", style: TextStyle(color: MyTheme.accent50), ), ], diff --git a/flutter/lib/pages/file_manager_page.dart b/flutter/lib/pages/file_manager_page.dart index 2cb980f44..7e9c39a4e 100644 --- a/flutter/lib/pages/file_manager_page.dart +++ b/flutter/lib/pages/file_manager_page.dart @@ -28,6 +28,7 @@ class _FileManagerPageState extends State { void initState() { super.initState(); FFI.connect(widget.id, isFileTransfer: true); + showLoading(translate('Connecting...')); FFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); } diff --git a/flutter/lib/pages/home_page.dart b/flutter/lib/pages/home_page.dart index 371aa3f64..fbf4fa0d2 100644 --- a/flutter/lib/pages/home_page.dart +++ b/flutter/lib/pages/home_page.dart @@ -12,6 +12,8 @@ abstract class PageShape extends Widget { final List appBarActions = []; } +final homeKey = GlobalKey<_HomePageState>(); + class HomePage extends StatefulWidget { HomePage({Key? key}) : super(key: key); @@ -23,12 +25,23 @@ class _HomePageState extends State { var _selectedIndex = 0; final List _pages = []; + void refreshPages() { + setState(() { + initPages(); + }); + } + @override void initState() { super.initState(); + initPages(); + } + + void initPages() { + _pages.clear(); _pages.add(ConnectionPage()); if (isAndroid) { - _pages.addAll([chatPage, ServerPage()]); + _pages.addAll([ChatPage(), ServerPage()]); } _pages.add(SettingsPage()); } diff --git a/flutter/lib/pages/remote_page.dart b/flutter/lib/pages/remote_page.dart index 265222837..a308496ea 100644 --- a/flutter/lib/pages/remote_page.dart +++ b/flutter/lib/pages/remote_page.dart @@ -592,6 +592,7 @@ class _RemotePageState extends State { child: Stack(children: [ ImagePaint(), CursorPaint(), + QualityMonitor(), getHelpTools(), SizedBox( width: 0, @@ -658,7 +659,7 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Row( children: ([ - Container(width: 100.0, child: Text(translate('OS Password'))), + Text(translate('OS Password')), TextButton( style: flatButtonStyle, onPressed: () { @@ -693,6 +694,13 @@ class _RemotePageState extends State { value: 'block-input')); } } + if (FFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + more.add(PopupMenuItem( + child: Text(translate('Restart Remote Device')), value: 'restart')); + } () async { var value = await showMenu( context: context, @@ -726,6 +734,8 @@ class _RemotePageState extends State { } } else if (value == 'reset_canvas') { FFI.cursorModel.reset(); + } else if (value == 'restart') { + showRestartRemoteDevice(pi, widget.id); } }(); } @@ -948,6 +958,47 @@ class ImagePainter extends CustomPainter { } } +class QualityMonitor extends StatelessWidget { + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: FFI.qualityMonitorModel, + child: Consumer( + builder: (context, qualityMonitorModel, child) => Positioned( + top: 10, + right: 10, + child: qualityMonitorModel.show + ? Container( + padding: EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Speed: ${qualityMonitorModel.data.speed}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "FPS: ${qualityMonitorModel.data.fps}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Delay: ${qualityMonitorModel.data.delay} ms", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Target Bitrate: ${qualityMonitorModel.data.targetBitrate}kb", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Codec: ${qualityMonitorModel.data.codecFormat}", + style: TextStyle(color: MyTheme.grayBg), + ), + ], + ), + ) + : SizedBox.shrink()))); +} + CheckboxListTile getToggle( void Function(void Function()) setState, option, name) { return CheckboxListTile( @@ -956,6 +1007,9 @@ CheckboxListTile getToggle( setState(() { FFI.setByName('toggle_option', option); }); + if (option == "show-quality-monitor") { + FFI.qualityMonitorModel.checkShowQualityMonitor(); + } }, dense: true, title: Text(translate(name))); @@ -1058,6 +1112,27 @@ void showOptions() { }, clickMaskDismiss: true, backDismiss: true); } +void showRestartRemoteDevice(PeerInfo pi, String id) async { + final res = + await DialogManager.show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + SizedBox(width: 10), + Text(translate("Restart Remote Device")), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + TextButton( + onPressed: () => close(), child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: () => close(true), child: Text(translate("OK"))), + ], + )); + if (res == true) FFI.setByName('restart_remote_device'); +} + void showSetOSPassword(bool login) { final controller = TextEditingController(); var password = FFI.getByName('peer_option', "os-password"); diff --git a/flutter/lib/pages/server_page.dart b/flutter/lib/pages/server_page.dart index bb80b8d51..8e79466d6 100644 --- a/flutter/lib/pages/server_page.dart +++ b/flutter/lib/pages/server_page.dart @@ -200,7 +200,8 @@ class ServerInfo extends StatelessWidget { Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 24), SizedBox(width: 10), - Text( + Expanded( + child: Text( translate("Service is not running"), style: TextStyle( fontFamily: 'WorkSans', @@ -208,7 +209,7 @@ class ServerInfo extends StatelessWidget { fontSize: 18, color: MyTheme.accent80, ), - ) + )) ], )), SizedBox(height: 5), @@ -316,30 +317,35 @@ class PermissionRow extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - SizedBox( - width: 140, + Expanded( + flex: 5, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, child: Text(name, - style: TextStyle(fontSize: 16.0, color: MyTheme.accent50))), - SizedBox( - width: 50, + style: + TextStyle(fontSize: 16.0, color: MyTheme.accent50)))), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, child: Text(isOk ? translate("ON") : translate("OFF"), style: TextStyle( fontSize: 16.0, - color: isOk ? Colors.green : Colors.grey)), - ) - ], + color: isOk ? Colors.green : Colors.grey))), ), - TextButton( - onPressed: onPressed, - child: Text( - translate(isOk ? "CLOSE" : "OPEN"), - style: TextStyle(fontWeight: FontWeight.bold), - )), - const Divider(height: 0) + Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: TextButton( + onPressed: onPressed, + child: Text( + translate(isOk ? "CLOSE" : "OPEN"), + style: TextStyle(fontWeight: FontWeight.bold), + )))), ], ); } diff --git a/flutter/lib/pages/settings_page.dart b/flutter/lib/pages/settings_page.dart index 30eb88b7b..bb1238494 100644 --- a/flutter/lib/pages/settings_page.dart +++ b/flutter/lib/pages/settings_page.dart @@ -26,11 +26,11 @@ class SettingsPage extends StatefulWidget implements PageShape { _SettingsState createState() => _SettingsState(); } -class _SettingsState extends State with WidgetsBindingObserver { - static const url = 'https://rustdesk.com/'; - final _hasIgnoreBattery = androidVersion >= 26; - var _ignoreBatteryOpt = false; +const url = 'https://rustdesk.com/'; +final _hasIgnoreBattery = androidVersion >= 26; +var _ignoreBatteryOpt = false; +class _SettingsState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); @@ -146,6 +146,12 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.cloud), onPressed: (context) { showServerSettings(); + }), + SettingsTile.navigation( + title: Text(translate('Language')), + leading: Icon(Icons.translate), + onPressed: (context) { + showLanguageSettings(); }) ]), SettingsSection( @@ -185,6 +191,42 @@ void showServerSettings() { showServerSettingsWithValue(id, relay, key, api); } +void showLanguageSettings() { + try { + final langs = json.decode(FFI.getByName('langs')) as List; + var lang = FFI.getByName('local_option', 'lang'); + DialogManager.show((setState, close) { + final setLang = (v) { + if (lang != v) { + setState(() { + lang = v; + }); + final msg = Map() + ..['name'] = 'lang' + ..['value'] = v; + FFI.setByName('local_option', json.encode(msg)); + homeKey.currentState?.refreshPages(); + Future.delayed(Duration(milliseconds: 200), close); + } + }; + return CustomAlertDialog( + title: SizedBox.shrink(), + content: Column( + children: [ + getRadio('Default', '', lang, setLang), + Divider(color: MyTheme.border), + ] + + langs.map((e) { + final key = e[0] as String; + final name = e[1] as String; + return getRadio(name, key, lang, setLang); + }).toList(), + ), + actions: []); + }, backDismiss: true, clickMaskDismiss: true); + } catch (_e) {} +} + void showAbout() { DialogManager.show((setState, close) { return CustomAlertDialog( diff --git a/flutter/lib/widgets/overlay.dart b/flutter/lib/widgets/overlay.dart index 276c175f9..b2ee54410 100644 --- a/flutter/lib/widgets/overlay.dart +++ b/flutter/lib/widgets/overlay.dart @@ -27,7 +27,7 @@ class DraggableChatWindow extends StatelessWidget { height: height, builder: (_, onPanUpdate) { return isIOS - ? chatPage + ? ChatPage() : Scaffold( resizeToAvoidBottomInset: false, appBar: CustomAppBar( @@ -68,7 +68,7 @@ class DraggableChatWindow extends StatelessWidget { ), ), ), - body: chatPage, + body: ChatPage(), ); }); } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a1db4ba6c..ec709b959 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -29,6 +29,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" characters: dependency: transitive description: @@ -71,6 +92,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.2" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -78,13 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" - dash_chat: + dash_chat_2: dependency: "direct main" description: - name: dash_chat + name: dash_chat_2 url: "https://pub.dartlang.org" source: hosted - version: "1.1.16" + version: "0.0.12" device_info: dependency: "direct main" description: @@ -195,6 +223,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: @@ -202,6 +237,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -247,6 +289,13 @@ packages: description: flutter source: sdk version: "0.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" http: dependency: "direct main" description: @@ -345,6 +394,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" package_info: dependency: "direct main" description: @@ -449,16 +505,14 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "6.0.3" qr_code_scanner: dependency: "direct main" description: - path: "." - ref: fix_break_changes_platform - resolved-ref: "0feca6f15042c279ff575c559a3430df917b623d" - url: "https://github.com/Heap-Hop/qr_code_scanner.git" - source: git - version: "0.7.0" + name: qr_code_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" quiver: dependency: transitive description: @@ -466,6 +520,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.27.5" settings_ui: dependency: "direct main" description: @@ -541,6 +602,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1+1" stack_trace: dependency: transitive description: @@ -562,6 +637,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -583,13 +665,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" - transparent_image: - dependency: transitive - description: - name: transparent_image - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" tuple: dependency: "direct main" description: @@ -674,6 +749,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + video_player: + dependency: transitive + description: + name: video_player + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.5" + video_player_android: + dependency: transitive + description: + name: video_player_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.8" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.5" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.3" + video_player_web: + dependency: transitive + description: + name: video_player_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" wakelock: dependency: "direct main" description: @@ -745,5 +855,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fc01b3c83..2ee7ec1ac 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: ffi: ^1.1.2 path_provider: ^2.0.2 external_path: ^1.0.1 - provider: ^5.0.0 + provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.5.2 device_info: ^2.0.2 @@ -41,15 +41,12 @@ dependencies: url_launcher: ^6.0.9 shared_preferences: ^2.0.6 toggle_switch: ^1.4.0 - dash_chat: ^1.1.16 + dash_chat_2: ^0.0.12 draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 http: ^0.13.4 - qr_code_scanner: - git: - url: https://github.com/Heap-Hop/qr_code_scanner.git - ref: fix_break_changes_platform + qr_code_scanner: ^1.0.0 zxing2: ^0.1.0 image_picker: ^0.8.5 image: ^3.1.3 diff --git a/src/client/helper.rs b/src/client/helper.rs index 26dc37ba4..5274a7c55 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -3,7 +3,10 @@ use std::{ time::Instant, }; -use hbb_common::{log, message_proto::{VideoFrame, video_frame}}; +use hbb_common::{ + log, + message_proto::{video_frame, VideoFrame}, +}; const MAX_LATENCY: i64 = 500; const MIN_LATENCY: i64 = 100; @@ -87,3 +90,12 @@ impl ToString for CodecFormat { } } } + +#[derive(Debug, Default)] +pub struct QualityStatus { + pub speed: Option, + pub fps: Option, + pub delay: Option, + pub target_bitrate: Option, + pub codec_format: Option, +} diff --git a/src/lang.rs b/src/lang.rs index ec0f3c187..073f47694 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -8,17 +8,17 @@ mod de; mod en; mod eo; mod es; -mod hu; mod fr; +mod hu; mod id; mod it; +mod pl; mod ptbr; mod ru; mod sk; mod tr; mod tw; mod vn; -mod pl; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -90,13 +90,15 @@ pub fn translate_locale(name: String, locale: &str) -> String { _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { - v.to_string() - } else { - if lang != "en" { - if let Some(v) = en::T.get(&name as &str) { - return v.to_string(); + if v.is_empty() { + if lang != "en" { + if let Some(v) = en::T.get(&name as &str) { + return v.to_string(); + } } + } else { + return v.to_string(); } - name } + name } diff --git a/src/mobile.rs b/src/mobile.rs index d19e7afb9..44161189c 100644 --- a/src/mobile.rs +++ b/src/mobile.rs @@ -1,12 +1,16 @@ use crate::client::*; -use crate::common::{make_fd_to_json}; +use crate::common::make_fd_to_json; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ allow_err, compress::decompress, config::{Config, LocalConfig}, - fs, log, - fs::{can_enable_overwrite_detection, new_send_confirm, DigestCheckResult, get_string, transform_windows_path}, + fs, + fs::{ + can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path, + DigestCheckResult, + }, + log, message_proto::*, protobuf::Message as _, rendezvous_proto::ConnType, @@ -17,6 +21,7 @@ use hbb_common::{ }, Stream, }; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::{ collections::{HashMap, VecDeque}, sync::{Arc, Mutex, RwLock}, @@ -83,6 +88,15 @@ impl Session { } } + pub fn restart_remote_device() { + if let Some(session) = SESSION.write().unwrap().as_ref() { + let mut lc = session.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); + session.send(Data::Message(msg)); + } + } + fn send(data: Data) { if let Some(session) = SESSION.read().unwrap().as_ref() { session.send(data); @@ -397,6 +411,26 @@ impl Session { log::debug!("{:?}", msg_out); self.send_msg(msg_out); } + + fn update_quality_status(&self, status: QualityStatus) { + const NULL: String = String::new(); + self.push_event( + "update_quality_status", + vec![ + ("speed", &status.speed.map_or(NULL, |it| it)), + ("fps", &status.fps.map_or(NULL, |it| it.to_string())), + ("delay", &status.delay.map_or(NULL, |it| it.to_string())), + ( + "target_bitrate", + &status.target_bitrate.map_or(NULL, |it| it.to_string()), + ), + ( + "codec_format", + &status.codec_format.map_or(NULL, |it| it.to_string()), + ), + ], + ); + } } impl FileManager for Session {} @@ -438,7 +472,11 @@ impl Interface for Session { if lc.is_file_transfer { if pi.username.is_empty() { - self.msgbox("error", "Error", "No active console user logged on, please connect and logon first."); + self.msgbox( + "error", + "Error", + "No active console user logged on, please connect and logon first.", + ); return; } } else { @@ -487,7 +525,14 @@ impl Interface for Session { } async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - handle_test_delay(t, peer).await; + if !t.from_client { + self.update_quality_status(QualityStatus { + delay: Some(t.last_delay as _), + target_bitrate: Some(t.target_bitrate as _), + ..Default::default() + }); + handle_test_delay(t, peer).await; + } } } @@ -502,6 +547,9 @@ struct Connection { write_jobs: Vec, timer: Interval, last_update_jobs_status: (Instant, HashMap), + data_count: Arc, + frame_count: Arc, + video_format: CodecFormat, } impl Connection { @@ -528,6 +576,9 @@ impl Connection { write_jobs: Vec::new(), timer: time::interval(SEC30), last_update_jobs_status: (Instant::now(), Default::default()), + data_count: Arc::new(AtomicUsize::new(0)), + frame_count: Arc::new(AtomicUsize::new(0)), + video_format: CodecFormat::Unknown, }; let key = Config::get_option("key"); let token = Config::get_option("access_token"); @@ -541,6 +592,9 @@ impl Connection { ("direct", &direct.to_string()), ], ); + + let mut status_timer = time::interval(Duration::new(1, 0)); + loop { tokio::select! { res = peer.next() => { @@ -553,14 +607,20 @@ impl Connection { } Ok(ref bytes) => { last_recv_time = Instant::now(); + conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !conn.handle_msg_from_peer(bytes, &mut peer).await { break } } } } else { - log::info!("Reset by the peer"); - session.msgbox("error", "Connection Error", "Reset by the peer"); + if session.lc.read().unwrap().restarting_remote_device { + log::info!("Restart remote device"); + session.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); + } else { + log::info!("Reset by the peer"); + session.msgbox("error", "Connection Error", "Reset by the peer"); + } break; } } @@ -586,6 +646,16 @@ impl Connection { conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); } } + _ = status_timer.tick() => { + let speed = conn.data_count.swap(0, Ordering::Relaxed); + let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); + let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _; + conn.session.update_quality_status(QualityStatus { + speed:Some(speed), + fps:Some(fps), + ..Default::default() + }); + } } } log::debug!("Exit io_loop of id={}", session.id); @@ -603,10 +673,19 @@ impl Connection { if !self.first_frame { self.first_frame = true; } + let incomming_format = CodecFormat::from(&vf); + if self.video_format != incomming_format { + self.video_format = incomming_format.clone(); + self.session.update_quality_status(QualityStatus { + codec_format: Some(incomming_format), + ..Default::default() + }) + }; if let (Ok(true), Some(s)) = ( self.video_handler.handle_frame(vf), RGBA_STREAM.read().unwrap().as_ref(), ) { + self.frame_count.fetch_add(1, Ordering::Relaxed); s.add(ZeroCopyBuffer(self.video_handler.rgb.clone())); } } @@ -664,113 +743,114 @@ impl Connection { vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], ); } - Some(message::Union::FileResponse(fr)) => match fr.union { - Some(file_response::Union::Dir(fd)) => { - let mut entries = fd.entries.to_vec(); - if self.session.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - let id = fd.id; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], - ); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.set_files(entries); - } - } - Some(file_response::Union::Block(block)) => { - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::Dir(fd)) => { + let mut entries = fd.entries.to_vec(); + if self.session.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + let id = fd.id; + self.session.push_event( + "file_dir", + vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], + ); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.set_files(entries); } - self.update_jobs_status(); } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); + Some(file_response::Union::Block(block)) => { + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block, None).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); + Some(file_response::Union::Done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::Error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let read_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handle_override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + ); + } } } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let write_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + match fs::is_write_need_confirmation(&write_path, &digest) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let msg= new_send_confirm(FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() }); - self.session.send_msg(msg); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); self.session.send_msg(msg); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - write_path.to_string(), - false, - ); } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }, + ); + self.session.send_msg(msg); + } else { + self.handle_override_file_confirm( + digest.id, + digest.file_num, + write_path.to_string(), + false, + ); + } + } + DigestCheckResult::NoSuchFile => { + let msg = new_send_confirm( FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, @@ -778,19 +858,20 @@ impl Connection { ..Default::default() }, ); - self.session.send_msg(msg); + self.session.send_msg(msg); + } + }, + Err(err) => { + println!("error recving digest: {}", err); } - }, - Err(err) => { - println!("error recving digest: {}", err); } } } } } + _ => {} } - _ => {} - }, + } Some(message::Union::Misc(misc)) => match misc.union { Some(misc::Union::AudioFormat(f)) => { self.audio_handler.handle_format(f); // @@ -809,6 +890,7 @@ impl Connection { Permission::Keyboard => "keyboard", Permission::Clipboard => "clipboard", Permission::Audio => "audio", + Permission::Restart => "restart", _ => "", }, &p.enabled.to_string(), diff --git a/src/mobile_ffi.rs b/src/mobile_ffi.rs index c3ed39764..b9ef571bb 100644 --- a/src/mobile_ffi.rs +++ b/src/mobile_ffi.rs @@ -113,6 +113,14 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co res = Session::get_option(arg); } } + "local_option" => { + if let Ok(arg) = arg.to_str() { + res = LocalConfig::get_option(arg); + } + } + "langs" => { + res = crate::lang::LANGS.to_string(); + } // File Action "get_home_dir" => { res = fs::get_home_as_string(); @@ -311,9 +319,21 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } } } + "local_option" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let Some(name) = m.get("name") { + if let Some(value) = m.get("value") { + LocalConfig::set_option(name.to_owned(), value.to_owned()); + } + } + } + } "input_os_password" => { Session::input_os_password(value.to_owned(), true); } + "restart_remote_device" => { + Session::restart_remote_device(); + } // File Action "read_remote_dir" => { if let Ok(m) = serde_json::from_str::>(value) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 48e059797..325b88b5f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -239,15 +239,6 @@ impl sciter::EventHandler for Handler { } } -#[derive(Debug, Default)] -struct QualityStatus { - speed: Option, - fps: Option, - delay: Option, - target_bitrate: Option, - codec_format: Option, -} - impl Handler { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let me = Self { @@ -638,8 +629,9 @@ impl Handler { } fn restart_remote_device(&mut self) { - self.lc.write().unwrap().restarting_remote_device = true; - let msg = self.lc.write().unwrap().restart_remote_device(); + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); self.send(Data::Message(msg)); }