optimized apt experience

This commit is contained in:
Junyuan Feng
2022-05-10 21:49:17 +08:00
parent 49f70fe41a
commit 7e8e0e2efc
13 changed files with 232 additions and 73 deletions

View File

@@ -31,9 +31,9 @@ class AppUpdate {
});
AppUpdate.fromJson(Map<String, dynamic> 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();

View File

@@ -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<String> 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<UpgradePkgInfo>? upgradeable;
String? error;
String? upgradeLog;
String? updateLog;
Function()? onUpgrade;
String? savedPwd;
AptProvider();
Future<void> init(
SSHClient client, Distribution dist, Function() onUpgrade) async {
Future<void> 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<void> 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<String> _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;
notifyListeners();
onUpgrade!();
});
refreshInstalled();
}
String get upgradeCmd {
final upgradeCmd = () {
switch (dist) {
case Distribution.rehl:
return 'yum upgrade -y';
case Distribution.debian:
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();
}
Future<void> _onPwd(Uint8List e, StreamSink<Uint8List> 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';
}

View File

@@ -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;
}
}

View File

@@ -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<String, Function> _notInlinedMessages(_) => <String, Function>{
@@ -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":

View File

@@ -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<String, Function> _notInlinedMessages(_) => <String, Function>{
@@ -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("更改将会立即生效")

View File

@@ -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<S> {

View File

@@ -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."
}

View File

@@ -111,5 +111,7 @@
"reportBugsOnGithubIssue": "请到 https://github.com/LollipopKit/flutter_server_box/issues 提交问题",
"noUpdateAvailable": "没有可用更新",
"foundNUpdate": "找到 {count} 个更新",
"updateAll": "更新全部"
"updateAll": "更新全部",
"pwdForUser": "用户{user}的密码",
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装"
}

View File

@@ -27,6 +27,8 @@ class _AptManagePageState extends State<AptManagePage>
late MediaQueryData _media;
final greyStyle = const TextStyle(color: Colors.grey);
final scrollController = ScrollController();
final scrollControllerUpdate = ScrollController();
final _aptProvider = locator<AptProvider>();
late S s;
@override
@@ -34,6 +36,7 @@ class _AptManagePageState extends State<AptManagePage>
super.didChangeDependencies();
_media = MediaQuery.of(context);
s = S.of(context);
_aptProvider.refreshInstalled();
}
@override
@@ -53,11 +56,52 @@ class _AptManagePageState extends State<AptManagePage>
Navigator.of(context).pop();
return;
}
locator<AptProvider>().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<AptManagePage>
title: TwoLineText(up: 'Apt', down: widget.spi.name),
),
body: Consumer<AptProvider>(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<AptManagePage>
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<AptManagePage>
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<AptManagePage>
: [
SizedBox(
height: _media.size.height * 0.7,
child: ListView(
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(18),
controller: scrollController,
children: [Text(apt.updateLog!)],
child: Text(apt.upgradeLog!),
),
))
],
)

View File

@@ -60,7 +60,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
widget.spi != null
? IconButton(
onPressed: () {
showRoundDialog(context, 'Attention',
showRoundDialog(context, s.attention,
Text(s.sureToDeleteServer(widget.spi!.name)), [
TextButton(
onPressed: () {

View File

@@ -90,8 +90,7 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
const Divider(),
(_path?.path ?? s.loadingFiles).omitStartStr(
style: TextStyle(
color:
color.isBrightColor ? Colors.black : Colors.white),
color: color.isBrightColor ? Colors.black : Colors.white),
)
],
),

View File

@@ -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"

View File

@@ -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