APT/Docker manage

- view apt update
- view docker container
This commit is contained in:
Junyuan Feng
2022-03-08 14:47:57 +08:00
parent b800bd91fd
commit 34e6b99297
20 changed files with 519 additions and 42 deletions

124
lib/view/page/apt.dart Normal file
View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/apt.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
class AptManagePage extends StatefulWidget {
const AptManagePage(this.spi, {Key? key}) : super(key: key);
final ServerPrivateInfo spi;
@override
_AptManagePageState createState() => _AptManagePageState();
}
class _AptManagePageState extends State<AptManagePage>
with SingleTickerProviderStateMixin {
late MediaQueryData _media;
final greyStyle = const TextStyle(color: Colors.grey);
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
}
@override
void dispose() {
super.dispose();
locator<AptProvider>().clear();
}
@override
void initState() {
super.initState();
final si = locator<ServerProvider>()
.servers
.firstWhere((e) => e.info == widget.spi);
if (si.client == null) {
showSnackBar(context, const Text('Plz wait for ssh connection'));
Navigator.of(context).pop();
return;
}
locator<AptProvider>().init(si.client!, si.status.sysVer.dist);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Apt'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
locator<AptProvider>().refreshInstalled();
},
),
],
),
body: Consumer<AptProvider>(builder: (_, apt, __) {
if (apt.upgradeable == null) {
apt.refreshInstalled();
}
return ListView(
padding: const EdgeInsets.all(13),
children: [_buildUpdatePanel(apt)],
);
}),
);
}
Widget _buildUpdatePanel(AptProvider apt) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RoundRectCard(ExpansionTile(
title: Text(!apt.isReady
? 'Loading...'
: 'Found ${apt.upgradeable!.length} update'),
subtitle: !apt.isReady
? null
: Text(
apt.upgradeable!.map((e) => e.package).join(', '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: greyStyle,
),
children: [
// TextButton(
// child: Text('Update all'),
// onPressed: () {
// apt.upgrade();
// }),
!apt.isReady
? const SizedBox()
: SizedBox(
height: _media.size.height * 0.23,
child: ListView(
children: apt.upgradeable!
.map((e) => _buildUpdateItem(e, apt))
.toList()),
)
],
))
],
);
}
Widget _buildUpdateItem(AptUpgradePkgInfo info, AptProvider apt) {
return ListTile(
title: Text(info.package),
subtitle: Text(
'${info.nowVersion} -> ${info.newVersion}',
style: greyStyle,
),
);
}
}

109
lib/view/page/docker.dart Normal file
View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/docker/ps.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi;
const DockerManagePage(this.spi, {Key? key}) : super(key: key);
@override
State<DockerManagePage> createState() => _DockerManagePageState();
}
class _DockerManagePageState extends State<DockerManagePage> {
final _docker = locator<DockerProvider>();
final greyTextStyle = const TextStyle(color: Colors.grey);
@override
void dispose() {
super.dispose();
_docker.clear();
}
@override
void initState() {
super.initState();
final client = locator<ServerProvider>()
.servers
.firstWhere((element) => element.info == widget.spi)
.client;
if (client == null) {
showSnackBar(context, const Text('No client found'));
Navigator.of(context).pop();
return;
}
_docker.init(client);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Docker'),
),
body: _buildMain(),
);
}
Widget _buildMain() {
return Consumer<DockerProvider>(builder: (_, docker, __) {
final running = docker.running;
if (docker.error != null && running == null) {
return Center(
child: Text(docker.error!),
);
}
if (running == null) {
_docker.refresh();
return centerLoading;
}
return ListView(
padding: const EdgeInsets.all(7),
children:
[_buildVersion(docker.edition ?? 'Unknown', docker.version ?? 'Unknown'), _buildPsItems(running)].map((e) => RoundRectCard(e)).toList(),
);
});
}
Widget _buildVersion(String edition, String version) {
return Padding(padding: EdgeInsets.all(13), child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(edition),
Text(version)
],
),);
}
Widget _buildPsItems(List<DockerPsItem> running) {
return ExpansionTile(
title: const Text('Container Status'),
subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
children: running.map((item) {
return ListTile(
title: Text(item.image),
subtitle: Text(item.status),
trailing: IconButton(
onPressed: () {},
icon: Icon(item.running ? Icons.stop : Icons.play_arrow)),
);
}).toList(),
);
}
String _buildSubtitle(List<DockerPsItem> running) {
final runningCount = running.where((element) => element.running).length;
final stoped = running.length - runningCount;
if (stoped == 0) {
return '$runningCount container running.';
}
return '$runningCount running, $stoped stoped.';
}
}

View File

@@ -111,7 +111,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
const SizedBox(height: 7),
Row(
children: [
const Text('Public Key Auth'),
const Text('Key Auth'),
Switch(
value: usePublicKey,
onChanged: (val) => setState(() => usePublicKey = val)),

View File

@@ -17,6 +17,8 @@ import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/apt.dart';
import 'package:toolbox/view/page/docker.dart';
import 'package:toolbox/view/page/server/detail.dart';
import 'package:toolbox/view/page/server/edit.dart';
import 'package:toolbox/view/page/sftp.dart';
@@ -240,9 +242,8 @@ class _ServerPageState extends State<ServerPage>
onChanged: (value) {
final item = value as MenuItem;
switch (item) {
case MenuItems.ssh:
case MenuItems.apt:
showSnackBar(context, const Text('Now is not supported'));
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
break;
case MenuItems.sftp:
AppRoute(
@@ -268,6 +269,9 @@ class _ServerPageState extends State<ServerPage>
'Edit server info page')
.go(context);
break;
case MenuItems.docker:
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
break;
}
},
itemHeight: 37,

View File

@@ -96,7 +96,7 @@ class _SFTPPageState extends State<SFTPPage> {
if (_status.files(left) == null) {
_status.setPath(left, AbsolutePath('/'));
listDir('/', left, client: client);
listDir(left, path: '/', client: client);
return centerCircleLoading;
} else {
return RefreshIndicator(
@@ -114,21 +114,22 @@ class _SFTPPageState extends State<SFTPPage> {
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename),
subtitle:
isDir ? null : Text((convertBytes(file.attr.size ?? 0))),
isDir ? null : Text(convertBytes(file.attr.size ?? 0)),
onTap: () {
if (isDir) {
_status.path(left)?.update(file.filename);
listDir(_status.path(left)?.path ?? '/', left);
listDir(left, path: _status.path(left)?.path);
} else {
onItemPress(context, left, file);
}
},
onLongPress: () => onItemPress(context, left, file),
);
},
),
key: Key(_status.spi(left)!.name + _status.path(left)!.path),
),
onRefresh: () => listDir(_status.path(left)?.path ?? '/', left));
onRefresh: () => listDir(left, path: _status.path(left)?.path));
}
}
@@ -148,21 +149,11 @@ class _SFTPPageState extends State<SFTPPage> {
leading: const Icon(Icons.folder),
title: const Text('Create Folder'),
onTap: () => mkdir(context, left)),
ListTile(
leading: Icon(left ? Icons.arrow_forward : Icons.arrow_back),
title: const Text('Copy'),
onTap: () => copy(context, left, file),
),
ListTile(
leading: const Icon(Icons.edit),
title: const Text('Rename'),
onTap: () => rename(context, left, file),
),
ListTile(
leading: const Icon(Icons.file_download),
title: const Text('Download'),
onTap: () => download(context, left, file),
),
],
),
[
@@ -172,18 +163,19 @@ class _SFTPPageState extends State<SFTPPage> {
]);
}
void download(BuildContext context, bool left, SftpName file) {}
void copy(BuildContext context, bool left, SftpName file) {}
void delete(BuildContext context, bool left, SftpName file) {
Navigator.of(context).pop();
showRoundDialog(
context, 'Confirm', Text('Are you sure to delete ${file.filename}?'), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
TextButton(
onPressed: () {},
onPressed: () {
_status.client(left)!.remove(file.filename);
Navigator.of(context).pop();
listDir(left);
},
child: const Text(
'Delete',
style: TextStyle(color: Colors.red),
@@ -192,6 +184,7 @@ class _SFTPPageState extends State<SFTPPage> {
}
void mkdir(BuildContext context, bool left) {
Navigator.of(context).pop();
final textController = TextEditingController();
showRoundDialog(
context,
@@ -219,6 +212,8 @@ class _SFTPPageState extends State<SFTPPage> {
}
_status.client(left)!.mkdir(
_status.path(left)!.path + '/' + textController.text);
Navigator.of(context).pop();
listDir(left);
},
child: const Text(
'Create',
@@ -228,10 +223,11 @@ class _SFTPPageState extends State<SFTPPage> {
}
void rename(BuildContext context, bool left, SftpName file) {
Navigator.of(context).pop();
final textController = TextEditingController();
showRoundDialog(
context,
'Create Folder',
'Rename',
TextField(
controller: textController,
decoration: const InputDecoration(
@@ -256,9 +252,11 @@ class _SFTPPageState extends State<SFTPPage> {
await _status
.client(left)!
.rename(file.filename, textController.text);
Navigator.of(context).pop();
listDir(left);
},
child: const Text(
'Create',
'Rename',
style: TextStyle(color: Colors.red),
)),
]);
@@ -278,17 +276,24 @@ class _SFTPPageState extends State<SFTPPage> {
return '$finalValue ${suffix[squareTimes]}';
}
Future<void> listDir(String path, bool left, {SSHClient? client}) async {
Future<void> listDir(bool left, {String? path, SSHClient? client}) async {
if (_status.isBusy(left)) {
return;
}
_status.setBusy(left, true);
if (client != null) {
final sftpc = await client.sftp();
_status.setClient(left, sftpc);
}
final fs = await _status.client(left)!.listdir(path);
final fs = await _status
.client(left)!
.listdir(path ?? (_status.leftPath?.path ?? '/'));
fs.sort((a, b) => a.filename.compareTo(b.filename));
fs.removeAt(0);
if (mounted) {
setState(() {
_status.setFiles(left, fs);
_status.setBusy(left, false);
});
}
}
@@ -328,7 +333,7 @@ class _SFTPPageState extends State<SFTPPage> {
return Text(
(exceeded ? '...' : '') + str!.substring(str.length - len),
overflow: TextOverflow.ellipsis,
overflow: TextOverflow.clip,
maxLines: 1,
style: const TextStyle(color: Colors.grey),
);
@@ -348,11 +353,12 @@ class _SFTPPageState extends State<SFTPPage> {
_status.setSpi(left, spi);
_status.setSelect(left, true);
_status.setPath(left, AbsolutePath('/'));
listDir('/', left,
listDir(left,
client: locator<ServerProvider>()
.servers
.firstWhere((s) => s.info == spi)
.client);
.client,
path: '/');
},
);
}

View File

@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';
const centerLoading = Center(child: CircularProgressIndicator());