From 7e8e0e2efcbe138fff2d9a6ceea392a88294a64b Mon Sep 17 00:00:00 2001 From: Junyuan Feng Date: Tue, 10 May 2022 21:49:17 +0800 Subject: [PATCH] optimized apt experience --- lib/data/model/app/update.dart | 6 +- lib/data/provider/apt.dart | 105 +++++++++++++++++++++------- lib/data/provider/server.dart | 3 - lib/generated/intl/messages_en.dart | 29 ++++---- lib/generated/intl/messages_zh.dart | 29 ++++---- lib/generated/l10n.dart | 20 ++++++ lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_zh.arb | 4 +- lib/view/page/apt.dart | 96 ++++++++++++++++++++++--- lib/view/page/server/edit.dart | 2 +- lib/view/page/sftp/downloaded.dart | 3 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 13 files changed, 232 insertions(+), 73 deletions(-) diff --git a/lib/data/model/app/update.dart b/lib/data/model/app/update.dart index 548bd97b..580d50be 100644 --- a/lib/data/model/app/update.dart +++ b/lib/data/model/app/update.dart @@ -31,9 +31,9 @@ class AppUpdate { }); AppUpdate.fromJson(Map json) { newest = json["newest"]?.toInt(); - newest = json["macbuild"]?.toInt(); - newest = json["iosbuild"]?.toInt(); - newest = json["androidbuild"]?.toInt(); + macbuild = json["macbuild"]?.toInt(); + iosbuild = json["iosbuild"]?.toInt(); + androidbuild = json["androidbuild"]?.toInt(); android = json["android"].toString(); ios = json["ios"].toString(); min = json["min"].toInt(); diff --git a/lib/data/provider/apt.dart b/lib/data/provider/apt.dart index 5e4f1775..ca44e040 100644 --- a/lib/data/provider/apt.dart +++ b/lib/data/provider/apt.dart @@ -1,25 +1,41 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:dartssh2/dartssh2.dart'; +import 'package:logging/logging.dart'; import 'package:toolbox/core/extension/uint8list.dart'; import 'package:toolbox/core/provider_base.dart'; import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart'; import 'package:toolbox/data/model/distribution.dart'; +typedef PwdRequestFunc = Future Function(String? userName); +final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):'); + class AptProvider extends BusyProvider { + final logger = Logger('AptProvider'); + SSHClient? client; Distribution? dist; + Function()? onUpgrade; + Function()? onUpdate; + PwdRequestFunc? onPasswordRequest; + String? whoami; List? upgradeable; String? error; + String? upgradeLog; String? updateLog; - Function()? onUpgrade; + String? savedPwd; AptProvider(); - Future init( - SSHClient client, Distribution dist, Function() onUpgrade) async { + Future init(SSHClient client, Distribution dist, Function() onUpgrade, + Function() onUpdate, PwdRequestFunc onPasswordRequest) async { this.client = client; this.dist = dist; this.onUpgrade = onUpgrade; + this.onPasswordRequest = onPasswordRequest; whoami = (await client.run('whoami').string).trim(); } @@ -30,8 +46,12 @@ class AptProvider extends BusyProvider { dist = null; upgradeable = null; error = null; - updateLog = null; - whoami = null; + upgradeLog = null; + updateLog = whoami = null; + savedPwd = null; + onUpgrade = null; + onUpdate = null; + onPasswordRequest = null; } Future refreshInstalled() async { @@ -44,38 +64,43 @@ class AptProvider extends BusyProvider { try { getUpgradeableList(result); } catch (e) { - error = e.toString(); + error = '[Server Raw]:\n$result\n[App Error]:\n$e'; } finally { notifyListeners(); } } - void getUpgradeableList(String raw) { + void getUpgradeableList(String? raw) { + if (raw == null) return; + var list = raw.split('\n'); switch (dist) { case Distribution.rehl: - var list = raw.split('\n').sublist(2); + list = list.sublist(2); list.removeWhere((element) => element.isEmpty); final endLine = list.lastIndexWhere( (element) => element.contains('Obsoleting Packages')); list = list.sublist(0, endLine); - upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList(); break; - case Distribution.debian: - case Distribution.unknown: default: - final list = raw.split('\n').sublist(4); + list = list.sublist(4); list.removeWhere((element) => element.isEmpty); - upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList(); } + upgradeable = list.map((e) => UpgradePkgInfo(e, dist!)).toList(); } Future _update() async { switch (dist) { case Distribution.rehl: - return await client!.run('yum check-update').string; - case Distribution.debian: + return await client!.run(_wrap('yum check-update')).string; default: - await client!.run('apt update'); + final session = await client!.execute(_wrap('apt update')); + session.stderr.listen((event) => _onPwd(event, session.stdin)); + session.stdout.listen((event) { + updateLog = (updateLog ?? '') + event.string; + notifyListeners(); + onUpdate!(); + }); + await session.done; return await client!.run('apt list --upgradeable').string; } } @@ -85,24 +110,50 @@ class AptProvider extends BusyProvider { error = 'No client'; return; } - updateLog = null; - final session = await client!.execute(upgradeCmd); - session.stdout.listen((data) { - updateLog = (updateLog ?? '') + data.string; + final upgradeCmd = () { + switch (dist) { + case Distribution.rehl: + return 'yum upgrade -y'; + default: + return 'apt upgrade -y'; + } + }(); + + final session = await client!.execute(_wrap(upgradeCmd)); + session.stderr.listen((e) => _onPwd(e, session.stdin)); + + session.stdout.listen((data) async { + upgradeLog = (upgradeLog ?? '') + data.string; notifyListeners(); onUpgrade!(); }); + + upgradeLog = null; + await session.done; refreshInstalled(); } - String get upgradeCmd { - switch (dist) { - case Distribution.rehl: - return 'yum upgrade -y'; - case Distribution.debian: - default: - return 'apt upgrade -y'; + Future _onPwd(Uint8List e, StreamSink stdin) async { + final event = e.string; + if (event.contains('[sudo] password for ')) { + final user = pwdRequestWithUserReg.firstMatch(event)?.group(1); + logger.info('sudo password request for $user'); + final pwd = await () async { + if (savedPwd != null) return savedPwd!; + final inputPwd = await (onPasswordRequest ?? (_) async => '')(user); + if (inputPwd.isNotEmpty) { + savedPwd = inputPwd; + } + return inputPwd; + }(); + if (pwd.isEmpty) { + logger.info('sudo password request cancelled'); + } + stdin.add(Uint8List.fromList(utf8.encode(pwd + '\n'))); } } + + String _wrap(String cmd) => + 'export LANG=en_US.utf-8 && ${isSU ? "" : "sudo -S "}$cmd'; } diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 6af96668..d690bd4d 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -327,9 +327,7 @@ class ServerProvider extends BusyProvider { void _getMem(ServerPrivateInfo spi, String raw) { final info = _servers.firstWhere((e) => e.info == spi); for (var item in raw.split('\n')) { - var found = false; if (item.contains(memPrefix)) { - found = true; final split = item.replaceFirst(memPrefix, '').split(' '); split.removeWhere((e) => e == ''); final memList = split.map((e) => int.parse(e)).toList(); @@ -342,7 +340,6 @@ class ServerProvider extends BusyProvider { avail: memList[5]); break; } - if (found) break; } } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index b9ec1f8c..29b9fe24 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -38,17 +38,19 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(myGithub) => "\nMade with ❤️ by ${myGithub}"; - static String m8(time) => "Spent time: ${time}"; + static String m8(user) => "Password for ${user}"; - static String m9(name) => "Are you sure to delete [${name}]?"; + static String m9(time) => "Spent time: ${time}"; - static String m10(server) => "Are you sure to delete server [${server}]?"; + static String m10(name) => "Are you sure to delete [${name}]?"; - static String m11(build) => "Found: v1.0.${build}, click to update"; + static String m11(server) => "Are you sure to delete server [${server}]?"; - static String m12(build) => "Current: v1.0.${build}"; + static String m12(build) => "Found: v1.0.${build}, click to update"; - static String m13(build) => "Current: v1.0.${build}, is up to date"; + static String m13(build) => "Current: v1.0.${build}"; + + static String m14(build) => "Current: v1.0.${build}, is up to date"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -140,6 +142,8 @@ class MessageLookup extends MessageLookupByLibrary { "pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"), "pingInputIP": MessageLookupByLibrary.simpleMessage( "Please input a target IP/domain."), + "platformNotSupportUpdate": MessageLookupByLibrary.simpleMessage( + "Current platform does not support in app update.\nPlease build from source and install it."), "plzEnterHost": MessageLookupByLibrary.simpleMessage("Please enter host."), "plzEnterPwd": @@ -149,6 +153,7 @@ class MessageLookup extends MessageLookupByLibrary { "port": MessageLookupByLibrary.simpleMessage("Port"), "privateKey": MessageLookupByLibrary.simpleMessage("Private Key"), "pwd": MessageLookupByLibrary.simpleMessage("Password"), + "pwdForUser": m8, "rename": MessageLookupByLibrary.simpleMessage("Rename"), "reportBugsOnGithubIssue": MessageLookupByLibrary.simpleMessage( "Please report bugs on https://github.com/LollipopKit/flutter_server_box/issues"), @@ -175,11 +180,11 @@ class MessageLookup extends MessageLookupByLibrary { "sftpSSHConnected": MessageLookupByLibrary.simpleMessage("SFTP Connected"), "snippet": MessageLookupByLibrary.simpleMessage("Snippet"), - "spentTime": m8, + "spentTime": m9, "start": MessageLookupByLibrary.simpleMessage("Start"), "stop": MessageLookupByLibrary.simpleMessage("Stop"), - "sureDelete": m9, - "sureToDeleteServer": m10, + "sureDelete": m10, + "sureToDeleteServer": m11, "ttl": MessageLookupByLibrary.simpleMessage("TTL"), "unknown": MessageLookupByLibrary.simpleMessage("unknown"), "unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"), @@ -193,9 +198,9 @@ class MessageLookup extends MessageLookupByLibrary { "upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"), "urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"), "user": MessageLookupByLibrary.simpleMessage("User"), - "versionHaveUpdate": m11, - "versionUnknownUpdate": m12, - "versionUpdated": m13, + "versionHaveUpdate": m12, + "versionUnknownUpdate": m13, + "versionUpdated": m14, "waitConnection": MessageLookupByLibrary.simpleMessage( "Please wait for the connection to be established."), "willTakEeffectImmediately": diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 986a8654..c16dd2c1 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -38,17 +38,19 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(myGithub) => "\n用❤️制作 by ${myGithub}"; - static String m8(time) => "耗时: ${time}"; + static String m8(user) => "用户${user}的密码"; - static String m9(name) => "确定删除[${name}]?"; + static String m9(time) => "耗时: ${time}"; - static String m10(server) => "你确定要删除服务器 [${server}] 吗?"; + static String m10(name) => "确定删除[${name}]?"; - static String m11(build) => "找到新版本:v1.0.${build}, 点击更新"; + static String m11(server) => "你确定要删除服务器 [${server}] 吗?"; - static String m12(build) => "当前:v1.0.${build}"; + static String m12(build) => "找到新版本:v1.0.${build}, 点击更新"; - static String m13(build) => "当前:v1.0.${build}, 已是最新版本"; + static String m13(build) => "当前:v1.0.${build}"; + + static String m14(build) => "当前:v1.0.${build}, 已是最新版本"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -122,12 +124,15 @@ class MessageLookup extends MessageLookupByLibrary { "ping": MessageLookupByLibrary.simpleMessage("Ping"), "pingAvg": MessageLookupByLibrary.simpleMessage("平均:"), "pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"), + "platformNotSupportUpdate": + MessageLookupByLibrary.simpleMessage("当前平台不支持更新,请编译最新源码后手动安装"), "plzEnterHost": MessageLookupByLibrary.simpleMessage("请输入主机"), "plzEnterPwd": MessageLookupByLibrary.simpleMessage("请输入密码"), "plzSelectKey": MessageLookupByLibrary.simpleMessage("请选择私钥"), "port": MessageLookupByLibrary.simpleMessage("端口"), "privateKey": MessageLookupByLibrary.simpleMessage("私钥"), "pwd": MessageLookupByLibrary.simpleMessage("密码"), + "pwdForUser": m8, "rename": MessageLookupByLibrary.simpleMessage("重命名"), "reportBugsOnGithubIssue": MessageLookupByLibrary.simpleMessage( "请到 https://github.com/LollipopKit/flutter_server_box/issues 提交问题"), @@ -149,11 +154,11 @@ class MessageLookup extends MessageLookupByLibrary { "sftpSSHConnected": MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."), "snippet": MessageLookupByLibrary.simpleMessage("代码片段"), - "spentTime": m8, + "spentTime": m9, "start": MessageLookupByLibrary.simpleMessage("开始"), "stop": MessageLookupByLibrary.simpleMessage("停止"), - "sureDelete": m9, - "sureToDeleteServer": m10, + "sureDelete": m10, + "sureToDeleteServer": m11, "ttl": MessageLookupByLibrary.simpleMessage("缓存时间"), "unknown": MessageLookupByLibrary.simpleMessage("未知"), "unknownError": MessageLookupByLibrary.simpleMessage("未知错误"), @@ -166,9 +171,9 @@ class MessageLookup extends MessageLookupByLibrary { "upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"), "urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"), "user": MessageLookupByLibrary.simpleMessage("用户"), - "versionHaveUpdate": m11, - "versionUnknownUpdate": m12, - "versionUpdated": m13, + "versionHaveUpdate": m12, + "versionUnknownUpdate": m13, + "versionUpdated": m14, "waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"), "willTakEeffectImmediately": MessageLookupByLibrary.simpleMessage("更改将会立即生效") diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index a9dc3482..b0c7ab59 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1180,6 +1180,26 @@ class S { args: [], ); } + + /// `Password for {user}` + String pwdForUser(Object user) { + return Intl.message( + 'Password for $user', + name: 'pwdForUser', + desc: '', + args: [user], + ); + } + + /// `Current platform does not support in app update.\nPlease build from source and install it.` + String get platformNotSupportUpdate { + return Intl.message( + 'Current platform does not support in app update.\nPlease build from source and install it.', + name: 'platformNotSupportUpdate', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fad1e815..1c1013aa 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -111,5 +111,7 @@ "reportBugsOnGithubIssue": "Please report bugs on https://github.com/LollipopKit/flutter_server_box/issues", "noUpdateAvailable": "No update available", "foundNUpdate": "Found {count} update", - "updateAll": "Update all" + "updateAll": "Update all", + "pwdForUser": "Password for {user}", + "platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it." } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 136b0f42..91956d5d 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -111,5 +111,7 @@ "reportBugsOnGithubIssue": "请到 https://github.com/LollipopKit/flutter_server_box/issues 提交问题", "noUpdateAvailable": "没有可用更新", "foundNUpdate": "找到 {count} 个更新", - "updateAll": "更新全部" + "updateAll": "更新全部", + "pwdForUser": "用户{user}的密码", + "platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装" } \ No newline at end of file diff --git a/lib/view/page/apt.dart b/lib/view/page/apt.dart index 01cb01e0..c83f1970 100644 --- a/lib/view/page/apt.dart +++ b/lib/view/page/apt.dart @@ -27,6 +27,8 @@ class _AptManagePageState extends State late MediaQueryData _media; final greyStyle = const TextStyle(color: Colors.grey); final scrollController = ScrollController(); + final scrollControllerUpdate = ScrollController(); + final _aptProvider = locator(); late S s; @override @@ -34,6 +36,7 @@ class _AptManagePageState extends State super.didChangeDependencies(); _media = MediaQuery.of(context); s = S.of(context); + _aptProvider.refreshInstalled(); } @override @@ -53,11 +56,52 @@ class _AptManagePageState extends State Navigator.of(context).pop(); return; } - locator().init( + + // ignore: prefer_function_declarations_over_variables + PwdRequestFunc onPwdRequest = (user) async { + final textController = TextEditingController(); + await showRoundDialog( + context, + s.pwdForUser(user ?? s.unknown), + TextField( + controller: textController, + decoration: InputDecoration( + labelText: s.pwd, + ), + ), + [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(s.cancel)), + TextButton( + onPressed: () { + if (textController.text == '') { + showRoundDialog( + context, s.attention, Text(s.fieldMustNotEmpty), [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(s.ok)), + ]); + return; + } + Navigator.of(context).pop(); + }, + child: Text( + s.ok, + style: const TextStyle(color: Colors.red), + )), + ]); + return textController.text.trim(); + }; + + _aptProvider.init( si.client!, si.status.sysVer.dist, () => - scrollController.jumpTo(scrollController.position.maxScrollExtent)); + scrollController.jumpTo(scrollController.position.maxScrollExtent), + () => scrollControllerUpdate + .jumpTo(scrollControllerUpdate.position.maxScrollExtent), + onPwdRequest); } @override @@ -68,10 +112,40 @@ class _AptManagePageState extends State title: TwoLineText(up: 'Apt', down: widget.spi.name), ), body: Consumer(builder: (_, apt, __) { - if (apt.upgradeable == null) { - apt.refreshInstalled(); + if (apt.error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.redAccent, + size: 37, + ), + const SizedBox( + height: 37, + ), + Text( + apt.error!, + textAlign: TextAlign.center, + ), + ], + ); + } + if (apt.updateLog == null && apt.upgradeable == null) { return centerLoading; } + if (apt.updateLog != null && apt.upgradeable == null) { + return SizedBox( + height: _media.size.height * 0.7, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: SingleChildScrollView( + padding: const EdgeInsets.all(18), + controller: scrollControllerUpdate, + child: Text(apt.updateLog!), + ), + )); + } return ListView( padding: const EdgeInsets.all(13), children: [ @@ -111,7 +185,7 @@ class _AptManagePageState extends State overflow: TextOverflow.ellipsis, style: greyStyle, ), - children: apt.updateLog == null + children: apt.upgradeLog == null ? [ TextButton( child: Text(s.updateAll), @@ -121,6 +195,7 @@ class _AptManagePageState extends State SizedBox( height: _media.size.height * 0.73, child: ListView( + controller: scrollController, children: apt.upgradeable! .map((e) => _buildUpdateItem(e, apt)) .toList()), @@ -129,10 +204,13 @@ class _AptManagePageState extends State : [ SizedBox( height: _media.size.height * 0.7, - child: ListView( - padding: const EdgeInsets.all(18), - controller: scrollController, - children: [Text(apt.updateLog!)], + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: SingleChildScrollView( + padding: const EdgeInsets.all(18), + controller: scrollController, + child: Text(apt.upgradeLog!), + ), )) ], ) diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index c4cb4104..30a2069a 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -60,7 +60,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin { widget.spi != null ? IconButton( onPressed: () { - showRoundDialog(context, 'Attention', + showRoundDialog(context, s.attention, Text(s.sureToDeleteServer(widget.spi!.name)), [ TextButton( onPressed: () { diff --git a/lib/view/page/sftp/downloaded.dart b/lib/view/page/sftp/downloaded.dart index c8ae2bf2..c95fa0f7 100644 --- a/lib/view/page/sftp/downloaded.dart +++ b/lib/view/page/sftp/downloaded.dart @@ -90,8 +90,7 @@ class _SFTPDownloadedPageState extends State { const Divider(), (_path?.path ?? s.loadingFiles).omitStartStr( style: TextStyle( - color: - color.isBrightColor ? Colors.black : Colors.white), + color: color.isBrightColor ? Colors.black : Colors.white), ) ], ), diff --git a/pubspec.lock b/pubspec.lock index 37f18bba..2b891c69 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -689,5 +689,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.15.1 <3.0.0" + dart: ">=2.16.0 <3.0.0" flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index f5499902..cb14e8de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.16.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions