mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 15:24:35 +01:00
Support Docker start/stop/remove
This commit is contained in:
@@ -56,7 +56,6 @@ Support, but not tested|Windows/Linux
|
|||||||
- [ ] SFTP
|
- [ ] SFTP
|
||||||
- [ ] Snippet market
|
- [ ] Snippet market
|
||||||
- [ ] Docker manager
|
- [ ] Docker manager
|
||||||
- [ ] SSH terminal
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
Please use `make.dart` to build.
|
Please use `make.dart` to build.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:toolbox/data/res/color.dart';
|
||||||
|
|
||||||
class MenuItem {
|
class MenuItem {
|
||||||
final String text;
|
final String text;
|
||||||
@@ -8,9 +9,21 @@ class MenuItem {
|
|||||||
required this.text,
|
required this.text,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Widget get build => Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: primaryColor),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuItems {
|
class ServerTabMenuItems {
|
||||||
static const List<MenuItem> firstItems = [sftp, snippet, apt, docker];
|
static const List<MenuItem> firstItems = [sftp, snippet, apt, docker];
|
||||||
static const List<MenuItem> secondItems = [edit];
|
static const List<MenuItem> secondItems = [edit];
|
||||||
|
|
||||||
@@ -19,18 +32,10 @@ class MenuItems {
|
|||||||
static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update);
|
static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update);
|
||||||
static const docker = MenuItem(text: 'Docker', icon: Icons.view_agenda);
|
static const docker = MenuItem(text: 'Docker', icon: Icons.view_agenda);
|
||||||
static const edit = MenuItem(text: 'Edit', icon: Icons.edit);
|
static const edit = MenuItem(text: 'Edit', icon: Icons.edit);
|
||||||
|
}
|
||||||
|
|
||||||
static Widget buildItem(MenuItem item) {
|
class DockerMenuItems {
|
||||||
return Row(
|
static const rm = MenuItem(text: 'Remove', icon: Icons.delete);
|
||||||
children: [
|
static const start = MenuItem(text: 'Start', icon: Icons.play_arrow);
|
||||||
Icon(item.icon),
|
static const stop = MenuItem(text: 'Stop', icon: Icons.stop);
|
||||||
const SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item.text,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,21 @@ class DockerPsItem {
|
|||||||
this.status, this.ports, this.name);
|
this.status, this.ports, this.name);
|
||||||
|
|
||||||
DockerPsItem.fromRawString(String rawString) {
|
DockerPsItem.fromRawString(String rawString) {
|
||||||
final List<String> parts = rawString.split(' ');
|
List<String> parts = rawString.split(' ');
|
||||||
|
parts.removeWhere((element) => element.isEmpty);
|
||||||
|
parts = parts.map((e) => e.trim()).toList();
|
||||||
|
|
||||||
containerId = parts[0];
|
containerId = parts[0];
|
||||||
image = parts[1];
|
image = parts[1];
|
||||||
command = parts[2];
|
command = parts[2].trim();
|
||||||
created = parts[3];
|
created = parts[3];
|
||||||
status = parts[4];
|
status = parts[4];
|
||||||
|
if (running) {
|
||||||
ports = parts[5];
|
ports = parts[5];
|
||||||
if (running && parts.length == 9) {
|
|
||||||
name = parts[8];
|
|
||||||
} else {
|
|
||||||
name = parts[6];
|
name = parts[6];
|
||||||
|
} else {
|
||||||
|
ports = '';
|
||||||
|
name = parts[5];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,42 @@ class DockerProvider extends BusyProvider {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop(String id) async {
|
Future<bool> stop(String id) async {
|
||||||
|
setBusyState();
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
error = 'no client';
|
error = 'no client';
|
||||||
notifyListeners();
|
setBusyState(false);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
final result = await client!.run('docker stop $id').string;
|
final result = await client!.run('docker stop $id').string;
|
||||||
await refresh();
|
await refresh();
|
||||||
notifyListeners();
|
setBusyState(false);
|
||||||
|
return result.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> start(String id) async {
|
||||||
|
setBusyState();
|
||||||
|
if (client == null) {
|
||||||
|
error = 'no client';
|
||||||
|
setBusyState(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final result = await client!.run('docker start $id').string;
|
||||||
|
await refresh();
|
||||||
|
setBusyState(false);
|
||||||
|
return result.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> delete(String id) async {
|
||||||
|
setBusyState();
|
||||||
|
if (client == null) {
|
||||||
|
error = 'no client';
|
||||||
|
setBusyState(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final result = await client!.run('docker rm $id').string;
|
||||||
|
await refresh();
|
||||||
|
setBusyState(false);
|
||||||
|
return result.contains(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:toolbox/data/provider/apt.dart';
|
|||||||
import 'package:toolbox/data/provider/server.dart';
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
import 'package:toolbox/locator.dart';
|
import 'package:toolbox/locator.dart';
|
||||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||||
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
class AptManagePage extends StatefulWidget {
|
class AptManagePage extends StatefulWidget {
|
||||||
const AptManagePage(this.spi, {Key? key}) : super(key: key);
|
const AptManagePage(this.spi, {Key? key}) : super(key: key);
|
||||||
@@ -53,7 +54,7 @@ class _AptManagePageState extends State<AptManagePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Apt'),
|
title: TwoLineText(up: 'Apt', down: widget.spi.ip),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:toolbox/core/utils.dart';
|
import 'package:toolbox/core/utils.dart';
|
||||||
|
import 'package:toolbox/data/model/app/menu_item.dart';
|
||||||
import 'package:toolbox/data/model/docker/ps.dart';
|
import 'package:toolbox/data/model/docker/ps.dart';
|
||||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
import 'package:toolbox/data/provider/docker.dart';
|
import 'package:toolbox/data/provider/docker.dart';
|
||||||
import 'package:toolbox/data/provider/server.dart';
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
import 'package:toolbox/locator.dart';
|
import 'package:toolbox/locator.dart';
|
||||||
import 'package:toolbox/view/widget/center_loading.dart';
|
import 'package:toolbox/view/widget/center_loading.dart';
|
||||||
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||||
|
import 'package:toolbox/view/widget/url_text.dart';
|
||||||
|
|
||||||
class DockerManagePage extends StatefulWidget {
|
class DockerManagePage extends StatefulWidget {
|
||||||
final ServerPrivateInfo spi;
|
final ServerPrivateInfo spi;
|
||||||
@@ -20,6 +24,7 @@ class DockerManagePage extends StatefulWidget {
|
|||||||
class _DockerManagePageState extends State<DockerManagePage> {
|
class _DockerManagePageState extends State<DockerManagePage> {
|
||||||
final _docker = locator<DockerProvider>();
|
final _docker = locator<DockerProvider>();
|
||||||
final greyTextStyle = const TextStyle(color: Colors.grey);
|
final greyTextStyle = const TextStyle(color: Colors.grey);
|
||||||
|
late MediaQueryData _media;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -27,6 +32,12 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
_docker.clear();
|
_docker.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_media = MediaQuery.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -46,7 +57,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Docker'),
|
title: TwoLineText(up: 'Docker', down: widget.spi.ip),
|
||||||
),
|
),
|
||||||
body: _buildMain(),
|
body: _buildMain(),
|
||||||
);
|
);
|
||||||
@@ -57,8 +68,15 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
final running = docker.running;
|
final running = docker.running;
|
||||||
if (docker.error != null && running == null) {
|
if (docker.error != null && running == null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(docker.error!),
|
child: Column(
|
||||||
);
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: _media.size.height * 0.43,
|
||||||
|
),
|
||||||
|
Text(docker.error!),
|
||||||
|
_buildSolution(docker.error!)
|
||||||
|
],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (running == null) {
|
if (running == null) {
|
||||||
_docker.refresh();
|
_docker.refresh();
|
||||||
@@ -66,23 +84,40 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
}
|
}
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(7),
|
padding: const EdgeInsets.all(7),
|
||||||
children:
|
children: [
|
||||||
[_buildVersion(docker.edition ?? 'Unknown', docker.version ?? 'Unknown'), _buildPsItems(running)].map((e) => RoundRectCard(e)).toList(),
|
_buildVersion(
|
||||||
|
docker.edition ?? 'Unknown', docker.version ?? 'Unknown'),
|
||||||
|
_buildPsItems(running, docker)
|
||||||
|
].map((e) => RoundRectCard(e)).toList(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildVersion(String edition, String version) {
|
Widget _buildSolution(String err) {
|
||||||
return Padding(padding: EdgeInsets.all(13), child: Row(
|
switch (err) {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
case 'docker not found':
|
||||||
children: [
|
return const UrlText(
|
||||||
Text(edition),
|
text: 'Please https://docs.docker.com/engine/install docker first.',
|
||||||
Text(version)
|
replace: 'install',
|
||||||
],
|
);
|
||||||
),);
|
case 'no client':
|
||||||
|
return const Text('Plz wait for the connection to be established.');
|
||||||
|
default:
|
||||||
|
return const Text('Unknown error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPsItems(List<DockerPsItem> running) {
|
Widget _buildVersion(String edition, String version) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(13),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [Text(edition), Text(version)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
title: const Text('Container Status'),
|
title: const Text('Container Status'),
|
||||||
subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
|
subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
|
||||||
@@ -90,14 +125,60 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.image),
|
title: Text(item.image),
|
||||||
subtitle: Text(item.status),
|
subtitle: Text(item.status),
|
||||||
trailing: IconButton(
|
trailing: docker.isBusy ? const CircularProgressIndicator() : _buildMoreBtn(item.running, item.containerId),
|
||||||
onPressed: () {},
|
|
||||||
icon: Icon(item.running ? Icons.stop : Icons.play_arrow)),
|
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMoreBtn(bool running, String containerId) {
|
||||||
|
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2(
|
||||||
|
customButton: const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 7),
|
||||||
|
child: Icon(
|
||||||
|
Icons.more_vert,
|
||||||
|
size: 17,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
customItemsHeight: 8,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem<MenuItem>(
|
||||||
|
value: item,
|
||||||
|
child: item.build,
|
||||||
|
),
|
||||||
|
DropdownMenuItem<MenuItem>(
|
||||||
|
value: DockerMenuItems.rm,
|
||||||
|
child: DockerMenuItems.rm.build,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
final item = value as MenuItem;
|
||||||
|
switch (item) {
|
||||||
|
case DockerMenuItems.rm:
|
||||||
|
_docker.delete(containerId);
|
||||||
|
break;
|
||||||
|
case DockerMenuItems.start:
|
||||||
|
_docker.start(containerId);
|
||||||
|
break;
|
||||||
|
case DockerMenuItems.stop:
|
||||||
|
_docker.stop(containerId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemHeight: 37,
|
||||||
|
itemPadding: const EdgeInsets.only(left: 17, right: 17),
|
||||||
|
dropdownWidth: 160,
|
||||||
|
dropdownDecoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
dropdownElevation: 8,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _buildSubtitle(List<DockerPsItem> running) {
|
String _buildSubtitle(List<DockerPsItem> running) {
|
||||||
final runningCount = running.where((element) => element.running).length;
|
final runningCount = running.where((element) => element.running).length;
|
||||||
final stoped = running.length - runningCount;
|
final stoped = running.length - runningCount;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:marquee/marquee.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
import 'package:toolbox/core/route.dart';
|
import 'package:toolbox/core/route.dart';
|
||||||
import 'package:toolbox/core/utils.dart';
|
|
||||||
import 'package:toolbox/data/model/app/menu_item.dart';
|
import 'package:toolbox/data/model/app/menu_item.dart';
|
||||||
import 'package:toolbox/data/model/server/server.dart';
|
import 'package:toolbox/data/model/server/server.dart';
|
||||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
||||||
@@ -222,30 +221,30 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
size: 17,
|
size: 17,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
customItemsIndexes: [MenuItems.firstItems.length],
|
customItemsIndexes: [ServerTabMenuItems.firstItems.length],
|
||||||
customItemsHeight: 8,
|
customItemsHeight: 8,
|
||||||
items: [
|
items: [
|
||||||
...MenuItems.firstItems.map(
|
...ServerTabMenuItems.firstItems.map(
|
||||||
(item) => DropdownMenuItem<MenuItem>(
|
(item) => DropdownMenuItem<MenuItem>(
|
||||||
value: item,
|
value: item,
|
||||||
child: MenuItems.buildItem(item),
|
child: item.build,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
|
const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
|
||||||
...MenuItems.secondItems.map(
|
...ServerTabMenuItems.secondItems.map(
|
||||||
(item) => DropdownMenuItem<MenuItem>(
|
(item) => DropdownMenuItem<MenuItem>(
|
||||||
value: item,
|
value: item,
|
||||||
child: MenuItems.buildItem(item),
|
child: item.build,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final item = value as MenuItem;
|
final item = value as MenuItem;
|
||||||
switch (item) {
|
switch (item) {
|
||||||
case MenuItems.apt:
|
case ServerTabMenuItems.apt:
|
||||||
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
|
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
|
||||||
break;
|
break;
|
||||||
case MenuItems.sftp:
|
case ServerTabMenuItems.sftp:
|
||||||
AppRoute(
|
AppRoute(
|
||||||
SFTPPage(
|
SFTPPage(
|
||||||
spi: spi,
|
spi: spi,
|
||||||
@@ -253,7 +252,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
'SFTP')
|
'SFTP')
|
||||||
.go(context);
|
.go(context);
|
||||||
break;
|
break;
|
||||||
case MenuItems.snippet:
|
case ServerTabMenuItems.snippet:
|
||||||
AppRoute(
|
AppRoute(
|
||||||
SnippetListPage(
|
SnippetListPage(
|
||||||
spi: spi,
|
spi: spi,
|
||||||
@@ -261,7 +260,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
'snippet list')
|
'snippet list')
|
||||||
.go(context);
|
.go(context);
|
||||||
break;
|
break;
|
||||||
case MenuItems.edit:
|
case ServerTabMenuItems.edit:
|
||||||
AppRoute(
|
AppRoute(
|
||||||
ServerEditPage(
|
ServerEditPage(
|
||||||
spi: spi,
|
spi: spi,
|
||||||
@@ -269,7 +268,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
'Edit server info page')
|
'Edit server info page')
|
||||||
.go(context);
|
.go(context);
|
||||||
break;
|
break;
|
||||||
case MenuItems.docker:
|
case ServerTabMenuItems.docker:
|
||||||
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
|
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
17
lib/view/widget/two_line_text.dart
Normal file
17
lib/view/widget/two_line_text.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TwoLineText extends StatelessWidget {
|
||||||
|
const TwoLineText({Key? key, required this.up, required this.down}) : super(key: key);
|
||||||
|
final String up;
|
||||||
|
final String down;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(up, style: const TextStyle(fontSize: 15),),
|
||||||
|
Text(down, style: const TextStyle(fontSize: 11),)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user