mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-05-11 18:36:11 +02:00
feat, update, win, macos (#11618)
Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
parent
62276b4f4f
commit
ca00706a38
72 changed files with 2128 additions and 69 deletions
|
@ -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<Widget> 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<String, dynamic> 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,
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<DesktopHomePage>
|
|||
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, "", () {});
|
||||
|
|
|
@ -470,6 +470,8 @@ class _GeneralState extends State<_General> {
|
|||
}
|
||||
|
||||
Widget other() {
|
||||
final showAutoUpdate =
|
||||
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
|
||||
final children = <Widget>[
|
||||
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)) {
|
||||
|
|
234
flutter/lib/desktop/widgets/update_progress.dart
Normal file
234
flutter/lib/desktop/widgets/update_progress.dart
Normal file
|
@ -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<VoidCallback> 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<UpdateProgress> createState() => UpdateProgressState();
|
||||
}
|
||||
|
||||
class UpdateProgressState extends State<UpdateProgress> {
|
||||
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<void> handleDownloadNewVersion(Map<String, dynamic> 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<Widget> 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<Color>(Colors.blue),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue