From ca00706a38051e5e2c31c74d90d6685ab1860878 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 4 May 2025 07:32:47 +0800 Subject: [PATCH] feat, update, win, macos (#11618) Signed-off-by: fufesou --- flutter/lib/common.dart | 50 +- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 20 +- .../desktop/pages/desktop_setting_page.dart | 11 +- .../lib/desktop/widgets/update_progress.dart | 234 ++++++++ res/msi/Package/Components/RustDesk.wxs | 2 +- .../Package/Fragments/AddRemoveProperties.wxs | 2 + src/common.rs | 6 +- src/core_main.rs | 40 +- src/flutter.rs | 13 +- src/flutter_ffi.rs | 106 +++- src/hbbs_http.rs | 1 + src/hbbs_http/downloader.rs | 274 ++++++++++ src/ipc.rs | 23 + src/lang/ar.rs | 7 + src/lang/be.rs | 7 + src/lang/bg.rs | 7 + src/lang/ca.rs | 7 + src/lang/cn.rs | 7 + src/lang/cs.rs | 7 + src/lang/da.rs | 7 + src/lang/de.rs | 7 + src/lang/el.rs | 7 + src/lang/en.rs | 3 + src/lang/eo.rs | 7 + src/lang/es.rs | 7 + src/lang/et.rs | 7 + src/lang/eu.rs | 7 + src/lang/fa.rs | 7 + src/lang/fr.rs | 7 + src/lang/ge.rs | 9 +- src/lang/he.rs | 7 + src/lang/hr.rs | 7 + src/lang/hu.rs | 23 +- src/lang/id.rs | 7 + src/lang/it.rs | 7 + src/lang/ja.rs | 7 + src/lang/ko.rs | 7 + src/lang/kz.rs | 7 + src/lang/lt.rs | 7 + src/lang/lv.rs | 7 + src/lang/nb.rs | 7 + src/lang/nl.rs | 7 + src/lang/pl.rs | 7 + src/lang/pt_PT.rs | 7 + src/lang/ptbr.rs | 7 + src/lang/ro.rs | 7 + src/lang/ru.rs | 7 + src/lang/sc.rs | 7 + src/lang/sk.rs | 7 + src/lang/sl.rs | 7 + src/lang/sq.rs | 7 + src/lang/sr.rs | 7 + src/lang/sv.rs | 7 + src/lang/ta.rs | 7 + src/lang/template.rs | 7 + src/lang/th.rs | 7 + src/lang/tr.rs | 7 + src/lang/tw.rs | 7 + src/lang/uk.rs | 7 + src/lang/vn.rs | 7 + src/lib.rs | 7 +- src/platform/macos.rs | 209 +++++++- src/platform/mod.rs | 71 ++- src/platform/privileges_scripts/install.scpt | 2 +- .../privileges_scripts/uninstall.scpt | 2 +- src/platform/privileges_scripts/update.scpt | 18 + src/platform/windows.cc | 7 +- src/platform/windows.rs | 501 +++++++++++++++++- src/rendezvous_mediator.rs | 4 + src/ui/index.tis | 1 + src/updater.rs | 249 +++++++++ 72 files changed, 2128 insertions(+), 69 deletions(-) create mode 100644 flutter/lib/desktop/widgets/update_progress.dart create mode 100644 src/hbbs_http/downloader.rs create mode 100644 src/platform/privileges_scripts/update.scpt create mode 100644 src/updater.rs diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f97276662..3088b3f2a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1152,15 +1152,23 @@ Widget createDialogContent(String text) { void msgBox(SessionID sessionId, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { + {bool? hasCancel, + ReconnectHandle? reconnect, + int? reconnectTimeout, + VoidCallback? onSubmit, + int? submitTimeout}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; submit() { dialogManager.dismissAll(); - // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom") && desktopType != DesktopType.portForward) { - closeConnection(); + if (onSubmit != null) { + onSubmit.call(); + } else { + // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom") && desktopType != DesktopType.portForward) { + closeConnection(); + } } } @@ -1176,7 +1184,18 @@ void msgBox(SessionID sessionId, String type, String title, String text, if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; - buttons.insert(0, dialogButton('OK', onPressed: submit)); + late final Widget btn; + if (submitTimeout != null) { + btn = _CountDownButton( + text: 'OK', + second: submitTimeout, + onPressed: submit, + submitOnTimeout: true, + ); + } else { + btn = dialogButton('OK', onPressed: submit); + } + buttons.insert(0, btn); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && @@ -1197,7 +1216,8 @@ void msgBox(SessionID sessionId, String type, String title, String text, reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = Obx(() => _ReconnectCountDownButton( + final button = Obx(() => _CountDownButton( + text: 'Reconnect', second: reconnectTimeout, onPressed: enabled.isTrue ? () { @@ -3183,21 +3203,24 @@ parseParamScreenRect(Map params) { get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2"; -class _ReconnectCountDownButton extends StatefulWidget { - _ReconnectCountDownButton({ +class _CountDownButton extends StatefulWidget { + _CountDownButton({ Key? key, + required this.text, required this.second, required this.onPressed, + this.submitOnTimeout = false, }) : super(key: key); + final String text; final VoidCallback? onPressed; final int second; + final bool submitOnTimeout; @override - State<_ReconnectCountDownButton> createState() => - _ReconnectCountDownButtonState(); + State<_CountDownButton> createState() => _CountDownButtonState(); } -class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { +class _CountDownButtonState extends State<_CountDownButton> { late int _countdownSeconds = widget.second; Timer? _timer; @@ -3218,6 +3241,9 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { _timer = Timer.periodic(Duration(seconds: 1), (timer) { if (_countdownSeconds <= 0) { timer.cancel(); + if (widget.submitOnTimeout) { + widget.onPressed?.call(); + } } else { setState(() { _countdownSeconds--; @@ -3229,7 +3255,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { @override Widget build(BuildContext context) { return dialogButton( - '${translate('Reconnect')} (${_countdownSeconds}s)', + '${translate(widget.text)} (${_countdownSeconds}s)', onPressed: widget.onPressed, isOutline: true, ); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a4d60b0a6..5ae15e52e 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -139,6 +139,7 @@ const String kOptionCurrentAbName = "current-ab-name"; const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs"; const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render"; const String kOptionEnableCheckUpdate = "enable-check-update"; +const String kOptionAllowAutoUpdate = "allow-auto-update"; const String kOptionAllowLinuxHeadless = "allow-linux-headless"; const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper"; const String kOptionStopService = "stop-service"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 7f30a5a63..07421d14f 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/update_progress.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -22,7 +23,6 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; import 'package:window_size/window_size.dart' as window_size; - import '../widgets/button.dart'; class DesktopHomePage extends StatefulWidget { @@ -433,13 +433,23 @@ class _DesktopHomePageState extends State updateUrl.isNotEmpty && !isCardClosed && bind.mainUriPrefixSync().contains('rustdesk')) { + final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); + String btnText = isToUpdate ? 'Click to update' : 'Click to download'; + GestureTapCallback onPressed = () async { + final Uri url = Uri.parse('https://rustdesk.com/download'); + await launchUrl(url); + }; + if (isToUpdate) { + onPressed = () { + handleUpdate(updateUrl); + }; + } return buildInstallCard( "Status", "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", - "Click to download", () async { - final Uri url = Uri.parse('https://rustdesk.com/download'); - await launchUrl(url); - }, closeButton: true); + btnText, + onPressed, + closeButton: true); } if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5ac53775c..f81925c77 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -470,6 +470,8 @@ class _GeneralState extends State<_General> { } Widget other() { + final showAutoUpdate = + isWindows && bind.mainIsInstalled() && !bind.isCustomClient(); final children = [ if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', @@ -523,12 +525,19 @@ class _GeneralState extends State<_General> { kOptionEnableCheckUpdate, isServer: false, ), + if (showAutoUpdate) + _OptionCheckBox( + context, + 'Auto update', + kOptionAllowAutoUpdate, + isServer: true, + ), if (isWindows && !bind.isOutgoingOnly()) _OptionCheckBox( context, 'Capture screen using DirectX', kOptionDirectxCapture, - ) + ), ], ]; if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart new file mode 100644 index 000000000..ac425fa2b --- /dev/null +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -0,0 +1,234 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void handleUpdate(String releasePageUrl) { + String downloadUrl = releasePageUrl.replaceAll('tag', 'download'); + String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); + final String downloadFile = + bind.mainGetCommonSync(key: 'download-file-$version'); + if (downloadFile.startsWith('error:')) { + final error = downloadFile.replaceFirst('error:', ''); + msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error, + releasePageUrl, gFFI.dialogManager); + return; + } + downloadUrl = '$downloadUrl/$downloadFile'; + + SimpleWrapper downloadId = SimpleWrapper(''); + SimpleWrapper onCanceled = SimpleWrapper(() {}); + gFFI.dialogManager.dismissAll(); + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate('Downloading {$appName}')), + content: + UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled) + .marginSymmetric(horizontal: 8) + .paddingOnly(top: 12), + actions: [ + dialogButton(translate('Cancel'), onPressed: () async { + onCanceled.value(); + await bind.mainSetCommon( + key: 'cancel-downloader', value: downloadId.value); + // Wait for the downloader to be removed. + for (int i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 300)); + final isCanceled = 'error:Downloader not found' == + await bind.mainGetCommon( + key: 'download-data-${downloadId.value}'); + if (isCanceled) { + break; + } + } + close(); + }, isOutline: true), + ]); + }); +} + +class UpdateProgress extends StatefulWidget { + final String releasePageUrl; + final String downloadUrl; + final SimpleWrapper downloadId; + final SimpleWrapper onCanceled; + UpdateProgress( + this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled, + {Key? key}) + : super(key: key); + + @override + State createState() => UpdateProgressState(); +} + +class UpdateProgressState extends State { + Timer? _timer; + int? _totalSize; + int _downloadedSize = 0; + int _getDataFailedCount = 0; + final String _eventKeyDownloadNewVersion = 'download-new-version'; + + @override + void initState() { + super.initState(); + widget.onCanceled.value = () { + cancelQueryTimer(); + }; + platformFFI.registerEventHandler(_eventKeyDownloadNewVersion, + _eventKeyDownloadNewVersion, handleDownloadNewVersion, + replace: true); + bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl); + } + + @override + void dispose() { + cancelQueryTimer(); + platformFFI.unregisterEventHandler( + _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion); + super.dispose(); + } + + void cancelQueryTimer() { + _timer?.cancel(); + _timer = null; + } + + Future handleDownloadNewVersion(Map evt) async { + if (evt.containsKey('id')) { + widget.downloadId.value = evt['id'] as String; + _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + _updateDownloadData(); + }); + } else { + if (evt.containsKey('error')) { + _onError(evt['error'] as String); + } else { + // unreachable + _onError('$evt'); + } + } + } + + void _onError(String error) { + cancelQueryTimer(); + + debugPrint('Download new version error: $error'); + final msgBoxType = 'custom-nocancel-nook-hasclose'; + final msgBoxTitle = 'Error'; + final msgBoxText = 'download-new-version-failed-tip'; + final dialogManager = gFFI.dialogManager; + + close() { + dialogManager.dismissAll(); + } + + jumplink() { + launchUrl(Uri.parse(widget.releasePageUrl)); + dialogManager.dismissAll(); + } + + retry() { + dialogManager.dismissAll(); + handleUpdate(widget.releasePageUrl); + } + + final List buttons = [ + dialogButton('Download', onPressed: jumplink), + dialogButton('Retry', onPressed: retry), + dialogButton('Close', onPressed: close), + ]; + dialogManager.dismissAll(); + dialogManager.show( + (setState, close, context) => CustomAlertDialog( + title: null, + content: SelectionArea( + child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)), + actions: buttons, + ), + tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle', + ); + } + + void _updateDownloadData() { + String err = ''; + String downloadData = + bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}'); + if (downloadData.startsWith('error:')) { + err = downloadData.substring('error:'.length); + } else { + try { + jsonDecode(downloadData).forEach((key, value) { + if (key == 'total_size') { + if (value != null && value is int) { + _totalSize = value; + } + } else if (key == 'downloaded_size') { + _downloadedSize = value as int; + } else if (key == 'error') { + if (value != null) { + err = value.toString(); + } + } + }); + } catch (e) { + _getDataFailedCount += 1; + debugPrint( + 'Failed to get download data ${widget.downloadUrl}, error $e'); + if (_getDataFailedCount > 3) { + err = e.toString(); + } + } + } + if (err != '') { + _onError(err); + } else { + if (_totalSize != null && _downloadedSize >= _totalSize!) { + cancelQueryTimer(); + bind.mainSetCommon( + key: 'remove-downloader', value: widget.downloadId.value); + if (_totalSize == 0) { + _onError('The download file size is 0.'); + } else { + setState(() {}); + msgBox( + gFFI.sessionId, + 'custom-nocancel', + '{$appName} Update', + '{$appName}-to-update-tip', + '', + gFFI.dialogManager, + onSubmit: () { + debugPrint('Downloaded, update to new version now'); + bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl); + }, + submitTimeout: 5, + ); + } + } else { + setState(() {}); + } + } + } + + @override + Widget build(BuildContext context) { + return onDownloading(context); + } + + Widget onDownloading(BuildContext context) { + final value = _totalSize == null + ? 0.0 + : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!); + return LinearProgressIndicator( + value: value, + minHeight: 20, + borderRadius: BorderRadius.circular(5), + backgroundColor: Colors.grey[300], + valueColor: const AlwaysStoppedAnimation(Colors.blue), + ); + } +} diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index ffe4f0ffb..337e84ec3 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -56,7 +56,7 @@ - + + +