diff --git a/README.md b/README.md index 21e89d51..7ff3a15a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ Support, but not tested|Windows/Linux - [ ] SFTP - [ ] Snippet market - [ ] Docker manager -- [ ] SSH terminal ## Build Please use `make.dart` to build. diff --git a/lib/data/model/app/menu_item.dart b/lib/data/model/app/menu_item.dart index 68cb4b7a..ff555fec 100644 --- a/lib/data/model/app/menu_item.dart +++ b/lib/data/model/app/menu_item.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:toolbox/data/res/color.dart'; class MenuItem { final String text; @@ -8,9 +9,21 @@ class MenuItem { required this.text, 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 firstItems = [sftp, snippet, apt, docker]; static const List secondItems = [edit]; @@ -19,18 +32,10 @@ class MenuItems { static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update); static const docker = MenuItem(text: 'Docker', icon: Icons.view_agenda); static const edit = MenuItem(text: 'Edit', icon: Icons.edit); - - static Widget buildItem(MenuItem item) { - return Row( - children: [ - Icon(item.icon), - const SizedBox( - width: 10, - ), - Text( - item.text, - ), - ], - ); - } +} + +class DockerMenuItems { + static const rm = MenuItem(text: 'Remove', icon: Icons.delete); + static const start = MenuItem(text: 'Start', icon: Icons.play_arrow); + static const stop = MenuItem(text: 'Stop', icon: Icons.stop); } diff --git a/lib/data/model/docker/ps.dart b/lib/data/model/docker/ps.dart index 1a246a26..88277bdd 100644 --- a/lib/data/model/docker/ps.dart +++ b/lib/data/model/docker/ps.dart @@ -11,17 +11,21 @@ class DockerPsItem { this.status, this.ports, this.name); DockerPsItem.fromRawString(String rawString) { - final List parts = rawString.split(' '); + List parts = rawString.split(' '); + parts.removeWhere((element) => element.isEmpty); + parts = parts.map((e) => e.trim()).toList(); + containerId = parts[0]; image = parts[1]; - command = parts[2]; + command = parts[2].trim(); created = parts[3]; status = parts[4]; - ports = parts[5]; - if (running && parts.length == 9) { - name = parts[8]; - } else { + if (running) { + ports = parts[5]; name = parts[6]; + } else { + ports = ''; + name = parts[5]; } } diff --git a/lib/data/provider/docker.dart b/lib/data/provider/docker.dart index ceef45ff..2df355b6 100644 --- a/lib/data/provider/docker.dart +++ b/lib/data/provider/docker.dart @@ -36,7 +36,7 @@ class DockerProvider extends BusyProvider { version = verSplit[1].split(' ').last; edition = verSplit[0].split(': ')[1]; } - + final raw = await client!.run('docker ps -a').string; if (raw.contains('command not found')) { error = 'docker not found'; @@ -50,14 +50,42 @@ class DockerProvider extends BusyProvider { notifyListeners(); } - Future stop(String id) async { + Future stop(String id) async { + setBusyState(); if (client == null) { error = 'no client'; - notifyListeners(); - return; + setBusyState(false); + return false; } final result = await client!.run('docker stop $id').string; await refresh(); - notifyListeners(); + setBusyState(false); + return result.contains(id); + } + + Future 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 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); } } diff --git a/lib/view/page/apt.dart b/lib/view/page/apt.dart index 5b21fbb7..e877ffa4 100644 --- a/lib/view/page/apt.dart +++ b/lib/view/page/apt.dart @@ -8,6 +8,7 @@ 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'; +import 'package:toolbox/view/widget/two_line_text.dart'; class AptManagePage extends StatefulWidget { const AptManagePage(this.spi, {Key? key}) : super(key: key); @@ -53,7 +54,7 @@ class _AptManagePageState extends State Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Apt'), + title: TwoLineText(up: 'Apt', down: widget.spi.ip), actions: [ IconButton( icon: const Icon(Icons.refresh), diff --git a/lib/view/page/docker.dart b/lib/view/page/docker.dart index aeba1a37..4cd15773 100644 --- a/lib/view/page/docker.dart +++ b/lib/view/page/docker.dart @@ -1,13 +1,17 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.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/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/two_line_text.dart'; import 'package:toolbox/view/widget/round_rect_card.dart'; +import 'package:toolbox/view/widget/url_text.dart'; class DockerManagePage extends StatefulWidget { final ServerPrivateInfo spi; @@ -20,6 +24,7 @@ class DockerManagePage extends StatefulWidget { class _DockerManagePageState extends State { final _docker = locator(); final greyTextStyle = const TextStyle(color: Colors.grey); + late MediaQueryData _media; @override void dispose() { @@ -27,6 +32,12 @@ class _DockerManagePageState extends State { _docker.clear(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _media = MediaQuery.of(context); + } + @override void initState() { super.initState(); @@ -46,7 +57,7 @@ class _DockerManagePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Docker'), + title: TwoLineText(up: 'Docker', down: widget.spi.ip), ), body: _buildMain(), ); @@ -57,8 +68,15 @@ class _DockerManagePageState extends State { final running = docker.running; if (docker.error != null && running == null) { 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) { _docker.refresh(); @@ -66,23 +84,40 @@ class _DockerManagePageState extends State { } return ListView( padding: const EdgeInsets.all(7), - children: - [_buildVersion(docker.edition ?? 'Unknown', docker.version ?? 'Unknown'), _buildPsItems(running)].map((e) => RoundRectCard(e)).toList(), + children: [ + _buildVersion( + docker.edition ?? 'Unknown', docker.version ?? 'Unknown'), + _buildPsItems(running, docker) + ].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 _buildSolution(String err) { + switch (err) { + case 'docker not found': + return const UrlText( + text: 'Please https://docs.docker.com/engine/install docker first.', + 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 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 running, DockerProvider docker) { return ExpansionTile( title: const Text('Container Status'), subtitle: Text(_buildSubtitle(running), style: greyTextStyle), @@ -90,14 +125,60 @@ class _DockerManagePageState extends State { return ListTile( title: Text(item.image), subtitle: Text(item.status), - trailing: IconButton( - onPressed: () {}, - icon: Icon(item.running ? Icons.stop : Icons.play_arrow)), + trailing: docker.isBusy ? const CircularProgressIndicator() : _buildMoreBtn(item.running, item.containerId), ); }).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( + value: item, + child: item.build, + ), + DropdownMenuItem( + 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 running) { final runningCount = running.where((element) => element.running).length; final stoped = running.length - runningCount; diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index 6d44ef98..07142f65 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -7,7 +7,6 @@ import 'package:marquee/marquee.dart'; import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.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/server/server.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart'; @@ -222,30 +221,30 @@ class _ServerPageState extends State size: 17, ), ), - customItemsIndexes: [MenuItems.firstItems.length], + customItemsIndexes: [ServerTabMenuItems.firstItems.length], customItemsHeight: 8, items: [ - ...MenuItems.firstItems.map( + ...ServerTabMenuItems.firstItems.map( (item) => DropdownMenuItem( value: item, - child: MenuItems.buildItem(item), + child: item.build, ), ), const DropdownMenuItem(enabled: false, child: Divider()), - ...MenuItems.secondItems.map( + ...ServerTabMenuItems.secondItems.map( (item) => DropdownMenuItem( value: item, - child: MenuItems.buildItem(item), + child: item.build, ), ), ], onChanged: (value) { final item = value as MenuItem; switch (item) { - case MenuItems.apt: + case ServerTabMenuItems.apt: AppRoute(AptManagePage(spi), 'apt manage page').go(context); break; - case MenuItems.sftp: + case ServerTabMenuItems.sftp: AppRoute( SFTPPage( spi: spi, @@ -253,7 +252,7 @@ class _ServerPageState extends State 'SFTP') .go(context); break; - case MenuItems.snippet: + case ServerTabMenuItems.snippet: AppRoute( SnippetListPage( spi: spi, @@ -261,7 +260,7 @@ class _ServerPageState extends State 'snippet list') .go(context); break; - case MenuItems.edit: + case ServerTabMenuItems.edit: AppRoute( ServerEditPage( spi: spi, @@ -269,7 +268,7 @@ class _ServerPageState extends State 'Edit server info page') .go(context); break; - case MenuItems.docker: + case ServerTabMenuItems.docker: AppRoute(DockerManagePage(spi), 'Docker manage page').go(context); break; } diff --git a/lib/view/widget/two_line_text.dart b/lib/view/widget/two_line_text.dart new file mode 100644 index 00000000..be76c30a --- /dev/null +++ b/lib/view/widget/two_line_text.dart @@ -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),) + ], + ); + } +}