From 34e6b9929747b236d86972ee18947407c93a6401 Mon Sep 17 00:00:00 2001 From: Junyuan Feng Date: Tue, 8 Mar 2022 14:47:57 +0800 Subject: [PATCH] APT/Docker manage - view apt update - view docker container --- ios/Runner.xcodeproj/project.pbxproj | 12 +-- lib/core/extension/stringx.dart | 17 +++ lib/core/extension/uint8list.dart | 6 ++ lib/data/model/app/menu_item.dart | 4 +- lib/data/model/apt/upgrade_pkg_info.dart | 33 ++++++ lib/data/model/distribution.dart | 21 ++++ lib/data/model/docker/ps.dart | 29 +++++ lib/data/model/sftp/sftp_side_status.dart | 6 ++ lib/data/provider/apt.dart | 48 +++++++++ lib/data/provider/docker.dart | 63 +++++++++++ lib/data/res/build_data.dart | 8 +- lib/locator.dart | 4 + lib/main.dart | 4 + lib/view/page/apt.dart | 124 ++++++++++++++++++++++ lib/view/page/docker.dart | 109 +++++++++++++++++++ lib/view/page/server/edit.dart | 2 +- lib/view/page/server/tab.dart | 8 +- lib/view/page/sftp.dart | 58 +++++----- lib/view/widget/center_loading.dart | 3 + pubspec.lock | 2 +- 20 files changed, 519 insertions(+), 42 deletions(-) create mode 100644 lib/core/extension/uint8list.dart create mode 100644 lib/data/model/apt/upgrade_pkg_info.dart create mode 100644 lib/data/model/distribution.dart create mode 100644 lib/data/model/docker/ps.dart create mode 100644 lib/data/provider/apt.dart create mode 100644 lib/data/provider/docker.dart create mode 100644 lib/view/page/apt.dart create mode 100644 lib/view/page/docker.dart create mode 100644 lib/view/widget/center_loading.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8981e9e4..21b7220b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -354,7 +354,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -362,7 +362,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.97; + MARKETING_VERSION = 1.0.101; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -484,7 +484,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -492,7 +492,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.97; + MARKETING_VERSION = 1.0.101; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -508,7 +508,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -516,7 +516,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.97; + MARKETING_VERSION = 1.0.101; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/core/extension/stringx.dart b/lib/core/extension/stringx.dart index e7d42008..fac54185 100644 --- a/lib/core/extension/stringx.dart +++ b/lib/core/extension/stringx.dart @@ -1,3 +1,20 @@ +import 'package:toolbox/data/model/distribution.dart'; + extension StringX on String { int get i => int.parse(this); + + Distribution get dist { + final lower = toLowerCase(); + for (var dist in debianDistList) { + if (lower.contains(dist)) { + return Distribution.debian; + } + } + for (var dist in rehlDistList) { + if (lower.contains(dist)) { + return Distribution.rehl; + } + } + return Distribution.unknown; + } } diff --git a/lib/core/extension/uint8list.dart b/lib/core/extension/uint8list.dart new file mode 100644 index 00000000..f20f57b1 --- /dev/null +++ b/lib/core/extension/uint8list.dart @@ -0,0 +1,6 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +extension Uint8ListX on Future { + Future get string async => utf8.decode(await this); +} \ No newline at end of file diff --git a/lib/data/model/app/menu_item.dart b/lib/data/model/app/menu_item.dart index f0185b32..68cb4b7a 100644 --- a/lib/data/model/app/menu_item.dart +++ b/lib/data/model/app/menu_item.dart @@ -11,13 +11,13 @@ class MenuItem { } class MenuItems { - static const List firstItems = [ssh, sftp, snippet, apt]; + static const List firstItems = [sftp, snippet, apt, docker]; static const List secondItems = [edit]; - static const ssh = MenuItem(text: 'SSH', icon: Icons.link); static const sftp = MenuItem(text: 'SFTP', icon: Icons.insert_drive_file); static const snippet = MenuItem(text: 'Snippet', icon: Icons.label); 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) { diff --git a/lib/data/model/apt/upgrade_pkg_info.dart b/lib/data/model/apt/upgrade_pkg_info.dart new file mode 100644 index 00000000..12c21fae --- /dev/null +++ b/lib/data/model/apt/upgrade_pkg_info.dart @@ -0,0 +1,33 @@ +import 'package:toolbox/data/model/distribution.dart'; + +class AptUpgradePkgInfo { + final String _raw; + final Distribution _dist; + + late String package; + late String nowVersion; + late String newVersion; + late String arch; + + AptUpgradePkgInfo(this._raw, this._dist) { + switch (_dist) { + case Distribution.debian: + case Distribution.unknown: + _parseApt(); + break; + case Distribution.rehl: + _parseYum(); + } + } + + void _parseApt() { + final split1 = _raw.split("/"); + package = split1[0]; + final split2 = split1[1].split(" "); + newVersion = split2[1]; + arch = split2[2]; + nowVersion = split2[5].replaceFirst(']', ''); + } + + void _parseYum() {} +} diff --git a/lib/data/model/distribution.dart b/lib/data/model/distribution.dart new file mode 100644 index 00000000..c86045d1 --- /dev/null +++ b/lib/data/model/distribution.dart @@ -0,0 +1,21 @@ +enum Distribution { + unknown, + debian, + rehl, +} + +const debianDistList = [ + 'debian', + 'ubuntu', + 'linuxmint', + 'elementary', + 'raspbian' +]; +const rehlDistList = [ + 'redhat', + 'fedora', + 'centos', + 'scientificlinux', + 'rhel', + 'oraclelinux' +]; diff --git a/lib/data/model/docker/ps.dart b/lib/data/model/docker/ps.dart new file mode 100644 index 00000000..1a246a26 --- /dev/null +++ b/lib/data/model/docker/ps.dart @@ -0,0 +1,29 @@ +class DockerPsItem { + late String containerId; + late String image; + late String command; + late String created; + late String status; + late String ports; + late String name; + + DockerPsItem(this.containerId, this.image, this.command, this.created, + this.status, this.ports, this.name); + + DockerPsItem.fromRawString(String rawString) { + final List parts = rawString.split(' '); + containerId = parts[0]; + image = parts[1]; + command = parts[2]; + created = parts[3]; + status = parts[4]; + ports = parts[5]; + if (running && parts.length == 9) { + name = parts[8]; + } else { + name = parts[6]; + } + } + + bool get running => status.contains('Up '); +} diff --git a/lib/data/model/sftp/sftp_side_status.dart b/lib/data/model/sftp/sftp_side_status.dart index 28adebe0..9b7b949b 100644 --- a/lib/data/model/sftp/sftp_side_status.dart +++ b/lib/data/model/sftp/sftp_side_status.dart @@ -13,6 +13,8 @@ class SFTPSideViewStatus { AbsolutePath? rightPath; SftpClient? leftClient; SftpClient? rightClient; + bool isBusyLeft = false; + bool isBusyRight = false; SFTPSideViewStatus(); @@ -36,4 +38,8 @@ class SFTPSideViewStatus { SftpClient? client(bool left) => left ? leftClient : rightClient; void setClient(bool left, SftpClient? nClient) => left ? leftClient = nClient : rightClient = nClient; + + bool isBusy(bool left) => left ? isBusyLeft : isBusyRight; + void setBusy(bool left, bool nBusy) => + left ? isBusyLeft = nBusy : isBusyRight = nBusy; } diff --git a/lib/data/provider/apt.dart b/lib/data/provider/apt.dart new file mode 100644 index 00000000..262186c6 --- /dev/null +++ b/lib/data/provider/apt.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:dartssh2/dartssh2.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'; + +class AptProvider extends BusyProvider { + SSHClient? client; + Distribution? dist; + List? upgradeable; + + AptProvider(); + + void init(SSHClient client, Distribution dist) { + this.client = client; + this.dist = dist; + } + + void clear() { + client = null; + dist = null; + upgradeable = null; + } + + bool get isReady { + return upgradeable != null && !isBusy; + } + + Future refreshInstalled() async { + await update(); + final result = utf8.decode(await client!.run('apt list --upgradeable')); + final list = result.split('\n').sublist(4); + list.removeWhere((element) => element.isEmpty); + upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList(); + notifyListeners(); + } + + Future update() async { + await client!.run('apt update'); + } + + // Future upgrade() async { + // setBusyState(); + // await client!.run('apt upgrade -y'); + // refreshInstalled(); + // } +} diff --git a/lib/data/provider/docker.dart b/lib/data/provider/docker.dart new file mode 100644 index 00000000..ceef45ff --- /dev/null +++ b/lib/data/provider/docker.dart @@ -0,0 +1,63 @@ +import 'package:dartssh2/dartssh2.dart'; +import 'package:toolbox/core/extension/uint8list.dart'; +import 'package:toolbox/core/provider_base.dart'; +import 'package:toolbox/data/model/docker/ps.dart'; + +class DockerProvider extends BusyProvider { + SSHClient? client; + List? running; + String? version; + String? edition; + String? error; + + void init(SSHClient client) => this.client = client; + + void clear() { + client = null; + error = null; + running = null; + version = null; + edition = null; + } + + Future refresh() async { + if (client == null) { + error = 'no client'; + notifyListeners(); + return; + } + + final verRaw = await client!.run('docker version').string; + final verSplit = verRaw.split('\n'); + if (verSplit.length < 3) { + error = 'invalid version'; + notifyListeners(); + } else { + 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'; + notifyListeners(); + return; + } + final lines = raw.split('\n'); + lines.removeAt(0); + lines.removeWhere((element) => element.isEmpty); + running = lines.map((e) => DockerPsItem.fromRawString(e)).toList(); + notifyListeners(); + } + + Future stop(String id) async { + if (client == null) { + error = 'no client'; + notifyListeners(); + return; + } + final result = await client!.run('docker stop $id').string; + await refresh(); + notifyListeners(); + } +} diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 7beff5f6..4b5ef7b1 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,9 +2,9 @@ class BuildData { static const String name = "ServerBox"; - static const int build = 101; + static const int build = 102; static const String engine = - "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (3 months ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n"; - static const String buildAt = "2022-03-02 11:12:07.958841"; - static const int modifications = 4; + "Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 7e9793dee1 (5 days ago) • 2022-03-02 11:23:12 -0600\nEngine • revision bd539267b4\nTools • Dart 2.16.1 • DevTools 2.9.2\n"; + static const String buildAt = "2022-03-07 19:19:07.115966"; + static const int modifications = 18; } diff --git a/lib/locator.dart b/lib/locator.dart index 0b3287da..33e1dd70 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,6 +1,8 @@ import 'package:get_it/get_it.dart'; import 'package:toolbox/data/provider/app.dart'; +import 'package:toolbox/data/provider/apt.dart'; import 'package:toolbox/data/provider/debug.dart'; +import 'package:toolbox/data/provider/docker.dart'; import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/snippet.dart'; @@ -18,7 +20,9 @@ void setupLocatorForServices() { void setupLocatorForProviders() { locator.registerSingleton(AppProvider()); + locator.registerSingleton(AptProvider()); locator.registerSingleton(DebugProvider()); + locator.registerSingleton(DockerProvider()); locator.registerSingleton(ServerProvider()); locator.registerSingleton(SnippetProvider()); locator.registerSingleton(PrivateKeyProvider()); diff --git a/lib/main.dart b/lib/main.dart index 16ed5da3..996fb057 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,9 @@ import 'package:provider/provider.dart'; import 'package:toolbox/app.dart'; import 'package:toolbox/core/analysis.dart'; import 'package:toolbox/data/provider/app.dart'; +import 'package:toolbox/data/provider/apt.dart'; import 'package:toolbox/data/provider/debug.dart'; +import 'package:toolbox/data/provider/docker.dart'; import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/snippet.dart'; @@ -62,7 +64,9 @@ Future main() async { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => locator()), + ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), + ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), diff --git a/lib/view/page/apt.dart b/lib/view/page/apt.dart new file mode 100644 index 00000000..5b21fbb7 --- /dev/null +++ b/lib/view/page/apt.dart @@ -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 + 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().clear(); + } + + @override + void initState() { + super.initState(); + final si = locator() + .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().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().refreshInstalled(); + }, + ), + ], + ), + body: Consumer(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, + ), + ); + } +} diff --git a/lib/view/page/docker.dart b/lib/view/page/docker.dart new file mode 100644 index 00000000..aeba1a37 --- /dev/null +++ b/lib/view/page/docker.dart @@ -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 createState() => _DockerManagePageState(); +} + +class _DockerManagePageState extends State { + final _docker = locator(); + final greyTextStyle = const TextStyle(color: Colors.grey); + + @override + void dispose() { + super.dispose(); + _docker.clear(); + } + + @override + void initState() { + super.initState(); + final client = locator() + .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(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 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 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.'; + } +} diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index fcb7e2e4..4162169c 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -111,7 +111,7 @@ class _ServerEditPageState extends State 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)), diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index 39e8d3ba..6d44ef98 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -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 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 'Edit server info page') .go(context); break; + case MenuItems.docker: + AppRoute(DockerManagePage(spi), 'Docker manage page').go(context); + break; } }, itemHeight: 37, diff --git a/lib/view/page/sftp.dart b/lib/view/page/sftp.dart index 5b82cb9e..54591215 100644 --- a/lib/view/page/sftp.dart +++ b/lib/view/page/sftp.dart @@ -96,7 +96,7 @@ class _SFTPPageState extends State { 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 { 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 { 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 { ]); } - 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 { } void mkdir(BuildContext context, bool left) { + Navigator.of(context).pop(); final textController = TextEditingController(); showRoundDialog( context, @@ -219,6 +212,8 @@ class _SFTPPageState extends State { } _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 { } 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 { 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 { return '$finalValue ${suffix[squareTimes]}'; } - Future listDir(String path, bool left, {SSHClient? client}) async { + Future 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 { 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 { _status.setSpi(left, spi); _status.setSelect(left, true); _status.setPath(left, AbsolutePath('/')); - listDir('/', left, + listDir(left, client: locator() .servers .firstWhere((s) => s.info == spi) - .client); + .client, + path: '/'); }, ); } diff --git a/lib/view/widget/center_loading.dart b/lib/view/widget/center_loading.dart new file mode 100644 index 00000000..d260d258 --- /dev/null +++ b/lib/view/widget/center_loading.dart @@ -0,0 +1,3 @@ +import 'package:flutter/material.dart'; + +const centerLoading = Center(child: CircularProgressIndicator()); diff --git a/pubspec.lock b/pubspec.lock index 7875b243..2716724a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -362,7 +362,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" path_provider_android: dependency: transitive description: