update_progress.dart 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_hbb/common.dart';
  5. import 'package:flutter_hbb/models/platform_model.dart';
  6. import 'package:get/get.dart';
  7. import 'package:url_launcher/url_launcher.dart';
  8. void handleUpdate(String releasePageUrl) {
  9. String downloadUrl = releasePageUrl.replaceAll('tag', 'download');
  10. String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
  11. final String downloadFile =
  12. bind.mainGetCommonSync(key: 'download-file-$version');
  13. if (downloadFile.startsWith('error:')) {
  14. final error = downloadFile.replaceFirst('error:', '');
  15. msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error,
  16. releasePageUrl, gFFI.dialogManager);
  17. return;
  18. }
  19. downloadUrl = '$downloadUrl/$downloadFile';
  20. SimpleWrapper downloadId = SimpleWrapper('');
  21. SimpleWrapper<VoidCallback> onCanceled = SimpleWrapper(() {});
  22. gFFI.dialogManager.dismissAll();
  23. gFFI.dialogManager.show((setState, close, context) {
  24. return CustomAlertDialog(
  25. title: Text(translate('Downloading {$appName}')),
  26. content:
  27. UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled)
  28. .marginSymmetric(horizontal: 8)
  29. .paddingOnly(top: 12),
  30. actions: [
  31. dialogButton(translate('Cancel'), onPressed: () async {
  32. onCanceled.value();
  33. await bind.mainSetCommon(
  34. key: 'cancel-downloader', value: downloadId.value);
  35. // Wait for the downloader to be removed.
  36. for (int i = 0; i < 10; i++) {
  37. await Future.delayed(const Duration(milliseconds: 300));
  38. final isCanceled = 'error:Downloader not found' ==
  39. await bind.mainGetCommon(
  40. key: 'download-data-${downloadId.value}');
  41. if (isCanceled) {
  42. break;
  43. }
  44. }
  45. close();
  46. }, isOutline: true),
  47. ]);
  48. });
  49. }
  50. class UpdateProgress extends StatefulWidget {
  51. final String releasePageUrl;
  52. final String downloadUrl;
  53. final SimpleWrapper downloadId;
  54. final SimpleWrapper onCanceled;
  55. UpdateProgress(
  56. this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled,
  57. {Key? key})
  58. : super(key: key);
  59. @override
  60. State<UpdateProgress> createState() => UpdateProgressState();
  61. }
  62. class UpdateProgressState extends State<UpdateProgress> {
  63. Timer? _timer;
  64. int? _totalSize;
  65. int _downloadedSize = 0;
  66. int _getDataFailedCount = 0;
  67. final String _eventKeyDownloadNewVersion = 'download-new-version';
  68. @override
  69. void initState() {
  70. super.initState();
  71. widget.onCanceled.value = () {
  72. cancelQueryTimer();
  73. };
  74. platformFFI.registerEventHandler(_eventKeyDownloadNewVersion,
  75. _eventKeyDownloadNewVersion, handleDownloadNewVersion,
  76. replace: true);
  77. bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl);
  78. }
  79. @override
  80. void dispose() {
  81. cancelQueryTimer();
  82. platformFFI.unregisterEventHandler(
  83. _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
  84. super.dispose();
  85. }
  86. void cancelQueryTimer() {
  87. _timer?.cancel();
  88. _timer = null;
  89. }
  90. Future<void> handleDownloadNewVersion(Map<String, dynamic> evt) async {
  91. if (evt.containsKey('id')) {
  92. widget.downloadId.value = evt['id'] as String;
  93. _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) {
  94. _updateDownloadData();
  95. });
  96. } else {
  97. if (evt.containsKey('error')) {
  98. _onError(evt['error'] as String);
  99. } else {
  100. // unreachable
  101. _onError('$evt');
  102. }
  103. }
  104. }
  105. void _onError(String error) {
  106. cancelQueryTimer();
  107. debugPrint('Download new version error: $error');
  108. final msgBoxType = 'custom-nocancel-nook-hasclose';
  109. final msgBoxTitle = 'Error';
  110. final msgBoxText = 'download-new-version-failed-tip';
  111. final dialogManager = gFFI.dialogManager;
  112. close() {
  113. dialogManager.dismissAll();
  114. }
  115. jumplink() {
  116. launchUrl(Uri.parse(widget.releasePageUrl));
  117. dialogManager.dismissAll();
  118. }
  119. retry() {
  120. dialogManager.dismissAll();
  121. handleUpdate(widget.releasePageUrl);
  122. }
  123. final List<Widget> buttons = [
  124. dialogButton('Download', onPressed: jumplink),
  125. dialogButton('Retry', onPressed: retry),
  126. dialogButton('Close', onPressed: close),
  127. ];
  128. dialogManager.dismissAll();
  129. dialogManager.show(
  130. (setState, close, context) => CustomAlertDialog(
  131. title: null,
  132. content: SelectionArea(
  133. child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
  134. actions: buttons,
  135. ),
  136. tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
  137. );
  138. }
  139. void _updateDownloadData() {
  140. String err = '';
  141. String downloadData =
  142. bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}');
  143. if (downloadData.startsWith('error:')) {
  144. err = downloadData.substring('error:'.length);
  145. } else {
  146. try {
  147. jsonDecode(downloadData).forEach((key, value) {
  148. if (key == 'total_size') {
  149. if (value != null && value is int) {
  150. _totalSize = value;
  151. }
  152. } else if (key == 'downloaded_size') {
  153. _downloadedSize = value as int;
  154. } else if (key == 'error') {
  155. if (value != null) {
  156. err = value.toString();
  157. }
  158. }
  159. });
  160. } catch (e) {
  161. _getDataFailedCount += 1;
  162. debugPrint(
  163. 'Failed to get download data ${widget.downloadUrl}, error $e');
  164. if (_getDataFailedCount > 3) {
  165. err = e.toString();
  166. }
  167. }
  168. }
  169. if (err != '') {
  170. _onError(err);
  171. } else {
  172. if (_totalSize != null && _downloadedSize >= _totalSize!) {
  173. cancelQueryTimer();
  174. bind.mainSetCommon(
  175. key: 'remove-downloader', value: widget.downloadId.value);
  176. if (_totalSize == 0) {
  177. _onError('The download file size is 0.');
  178. } else {
  179. setState(() {});
  180. msgBox(
  181. gFFI.sessionId,
  182. 'custom-nocancel',
  183. '{$appName} Update',
  184. '{$appName}-to-update-tip',
  185. '',
  186. gFFI.dialogManager,
  187. onSubmit: () {
  188. debugPrint('Downloaded, update to new version now');
  189. bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
  190. },
  191. submitTimeout: 5,
  192. );
  193. }
  194. } else {
  195. setState(() {});
  196. }
  197. }
  198. }
  199. @override
  200. Widget build(BuildContext context) {
  201. return onDownloading(context);
  202. }
  203. Widget onDownloading(BuildContext context) {
  204. final value = _totalSize == null
  205. ? 0.0
  206. : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!);
  207. return LinearProgressIndicator(
  208. value: value,
  209. minHeight: 20,
  210. borderRadius: BorderRadius.circular(5),
  211. backgroundColor: Colors.grey[300],
  212. valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
  213. );
  214. }
  215. }