From f07d33a1d6b70e1d8c7b2179fcb7a250aea720b5 Mon Sep 17 00:00:00 2001 From: Junyuan Feng Date: Fri, 18 Feb 2022 13:32:50 +0800 Subject: [PATCH] SFTP init. --- .gitignore | 1 + lib/data/model/app/menu_item.dart | 2 +- lib/data/model/sftp/absolute_path.dart | 29 ++++ lib/data/model/sftp/sftp_side_status.dart | 39 +++++ lib/data/res/build_data.dart | 8 +- lib/view/page/home.dart | 2 +- lib/view/page/sftp.dart | 188 ++++++++++++---------- 7 files changed, 176 insertions(+), 93 deletions(-) create mode 100644 lib/data/model/sftp/absolute_path.dart create mode 100644 lib/data/model/sftp/sftp_side_status.dart diff --git a/.gitignore b/.gitignore index a4794805..c8d0d7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ app.*.map.json /android/app/fjy.androidstudio.key /release +test.dart diff --git a/lib/data/model/app/menu_item.dart b/lib/data/model/app/menu_item.dart index 52c7c840..f0185b32 100644 --- a/lib/data/model/app/menu_item.dart +++ b/lib/data/model/app/menu_item.dart @@ -15,7 +15,7 @@ class MenuItems { static const List secondItems = [edit]; static const ssh = MenuItem(text: 'SSH', icon: Icons.link); - static const sftp = MenuItem(text: 'SFTP', icon: Icons.file_present); + 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 edit = MenuItem(text: 'Edit', icon: Icons.edit); diff --git a/lib/data/model/sftp/absolute_path.dart b/lib/data/model/sftp/absolute_path.dart new file mode 100644 index 00000000..7d306205 --- /dev/null +++ b/lib/data/model/sftp/absolute_path.dart @@ -0,0 +1,29 @@ +class AbsolutePath { + String path; + String? _prePath; + AbsolutePath(this.path); + + void update(String newPath) { + _prePath = path; + if (newPath == '..') { + path = path.substring(0, path.lastIndexOf('/')); + if (path == '') { + path = '/'; + } + return; + } + if (newPath == '/') { + path = '/'; + return; + } + path = path + (path.endsWith('/') ? '' : '/') + newPath; + } + + bool undo() { + if (_prePath == null) { + return false; + } + path = _prePath!; + return true; + } +} diff --git a/lib/data/model/sftp/sftp_side_status.dart b/lib/data/model/sftp/sftp_side_status.dart new file mode 100644 index 00000000..28adebe0 --- /dev/null +++ b/lib/data/model/sftp/sftp_side_status.dart @@ -0,0 +1,39 @@ +import 'package:dartssh2/dartssh2.dart'; +import 'package:toolbox/data/model/server/server_private_info.dart'; +import 'package:toolbox/data/model/sftp/absolute_path.dart'; + +class SFTPSideViewStatus { + bool leftSelected = false; + bool rightSelected = false; + ServerPrivateInfo? leftSpi; + ServerPrivateInfo? rightSpi; + List? leftFiles; + List? rightFiles; + AbsolutePath? leftPath; + AbsolutePath? rightPath; + SftpClient? leftClient; + SftpClient? rightClient; + + SFTPSideViewStatus(); + + ServerPrivateInfo? spi(bool left) => left ? leftSpi : rightSpi; + void setSpi(bool left, ServerPrivateInfo nSpi) => + left ? leftSpi = nSpi : rightSpi = nSpi; + + /// Whether the Left/Right Destination is selected. + bool selected(bool left) => left ? leftSelected : rightSelected; + void setSelect(bool left, bool nSelect) => + left ? leftSelected = nSelect : rightSelected = nSelect; + + List? files(bool left) => left ? leftFiles : rightFiles; + void setFiles(bool left, List? nFiles) => + left ? leftFiles = nFiles : rightFiles = nFiles; + + AbsolutePath? path(bool left) => left ? leftPath : rightPath; + void setPath(bool left, AbsolutePath? nPath) => + left ? leftPath = nPath : rightPath = nPath; + + SftpClient? client(bool left) => left ? leftClient : rightClient; + void setClient(bool left, SftpClient? nClient) => + left ? leftClient = nClient : rightClient = nClient; +} diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 0f08b2e4..ddf3d04f 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 = 97; + static const int build = 98; static const String engine = - "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (8 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n"; - static const String buildAt = "2022-02-10 19:30:23.388434"; - static const int modifications = 9; + "╔════════════════════════════════════════════════════════════════════════════╗\n║ A new version of Flutter is available! ║\n║ ║\n║ To update to the latest version, run \"flutter upgrade\". ║\n╚════════════════════════════════════════════════════════════════════════════╝\n\nFlutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (9 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n"; + static const String buildAt = "2022-02-18 13:28:18.254386"; + static const int modifications = 5; } diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 7faf4957..7a9740e4 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -97,7 +97,7 @@ class _MyHomePageState extends State Widget _buildMain(BuildContext context) { return AdvancedDrawer( controller: _advancedDrawerController, - animationCurve: Curves.easeInOutCirc, + animationCurve: Curves.easeInOut, animationDuration: const Duration(milliseconds: 300), animateChildDecoration: true, rtlOpening: false, diff --git a/lib/view/page/sftp.dart b/lib/view/page/sftp.dart index f8f90efe..623fcbdf 100644 --- a/lib/view/page/sftp.dart +++ b/lib/view/page/sftp.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:toolbox/core/utils.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; +import 'package:toolbox/data/model/sftp/absolute_path.dart'; +import 'package:toolbox/data/model/sftp/sftp_side_status.dart'; import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/locator.dart'; import 'package:toolbox/view/widget/fade_in.dart'; @@ -16,13 +18,7 @@ class SFTPPage extends StatefulWidget { } class _SFTPPageState extends State { - /// Whether the Left/Right Destination is selected. - final List _selectedDest = List.filled(2, false); - final List _destSpi = - List.filled(2, null); - final List?> _files = List?>.filled(2, null); - final List _paths = List.filled(2, ''); - final List _clients = List.filled(2, null); + final SFTPSideViewStatus _status = SFTPSideViewStatus(); final ScrollController _leftScrollController = ScrollController(); final ScrollController _rightScrollController = ScrollController(); @@ -39,8 +35,8 @@ class _SFTPPageState extends State { void initState() { super.initState(); if (widget.spi != null) { - _destSpi[0] = widget.spi; - _selectedDest[0] = true; + _status.setSpi(true, widget.spi!); + _status.setSelect(true, true); } } @@ -49,7 +45,7 @@ class _SFTPPageState extends State { return Scaffold( appBar: AppBar( centerTitle: true, - title: Text(_titleText), + title: const Text('SFTP'), ), body: Row( children: [ @@ -63,31 +59,10 @@ class _SFTPPageState extends State { ); } - String get _titleText { - List titles = [ - '', - '', - ]; - if (_selectedDest[0]) { - titles[0] = _destSpi[0]?.name ?? ''; - } - if (_selectedDest[1]) { - titles[1] = _destSpi[1]?.name ?? ''; - } - return titles[0] == '' || titles[1] == '' ? 'SFTP' : titles.join(' - '); - } - Widget _buildSingleColumn(bool left) { - Widget child; - if (!_selectedDest[left ? 0 : 1]) { - child = _buildDestSelector(left); - } else { - child = _buildFileView(left); - } - return SizedBox( width: (_media.size.width - 2) / 2, - child: child, + child: _buildFileView(left), ); } @@ -103,7 +78,14 @@ class _SFTPPageState extends State { ); Widget _buildFileView(bool left) { - final spi = _destSpi[left ? 0 : 1]; + if (!_status.selected(left)) { + return ListView( + children: [ + _buildDestSelector(left), + ], + ); + } + final spi = _status.spi(left); final si = locator().servers.firstWhere((s) => s.info == spi); final client = si.client; @@ -112,44 +94,45 @@ class _SFTPPageState extends State { return centerCircleLoading; } - if (_files[left ? 0 : 1] == null) { - updatePath('/', left); - listDir(client, '/', left); + if (_status.files(left) == null) { + _status.setPath(left, AbsolutePath('/')); + listDir('/', left, client: client); return centerCircleLoading; } else { return RefreshIndicator( child: FadeIn( child: ListView.builder( - itemCount: _files[left ? 0 : 1]!.length, + itemCount: _status.files(left)!.length + 1, controller: left ? _leftScrollController : _rightScrollController, itemBuilder: (context, index) { - final file = _files[left ? 0 : 1]![index]; + if (index == 0) { + return _buildDestSelector(left); + } + final file = _status.files(left)![index - 1]; final isDir = file.attr.mode?.isDirectory ?? true; return ListTile( - leading: - Icon(isDir ? Icons.folder : Icons.insert_drive_file), - title: Text(file.filename), - subtitle: isDir - ? null - : Text((convertBytes(file.attr.size ?? 0))), - onTap: () { - if (isDir) { - updatePath(file.filename, left); - listDir(client, _paths[left ? 0 : 1], left); - } else { - // downloadFile(client, file.name); - } - }, - onLongPress: () => onItemLongPress(context, left, file)); + leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), + title: Text(file.filename), + subtitle: + isDir ? null : Text((convertBytes(file.attr.size ?? 0))), + onTap: () { + if (isDir) { + _status.path(left)?.update(file.filename); + listDir(_status.path(left)?.path ?? '/', left); + } else { + onItemPress(context, left, file); + } + }, + ); }, ), - key: Key(_paths[left ? 0 : 1]), + key: Key(_status.spi(left)!.name + _status.path(left)!.path), ), - onRefresh: () => listDir(client, _paths[left ? 0 : 1], left)); + onRefresh: () => listDir(_status.path(left)?.path ?? '/', left)); } } - void onItemLongPress(BuildContext context, bool left, SftpName file) { + void onItemPress(BuildContext context, bool left, SftpName file) { showRoundDialog( context, 'Action', @@ -226,8 +209,8 @@ class _SFTPPageState extends State { ]); return; } - _clients[left ? 0 : 1]! - .mkdir(_paths[left ? 0 : 1] + '/' + textController.text); + _status.client(left)!.mkdir( + _status.path(left)!.path + '/' + textController.text); }, child: const Text( 'Create', @@ -262,7 +245,8 @@ class _SFTPPageState extends State { ]); return; } - await _clients[left ? 0 : 1]! + await _status + .client(left)! .rename(file.filename, textController.text); }, child: const Text( @@ -286,40 +270,66 @@ class _SFTPPageState extends State { return '$finalValue ${suffix[squareTimes]}'; } - void updatePath(String filename, bool left) { - if (filename == '..') { - _paths[left ? 0 : 1] = _paths[left ? 0 : 1] - .substring(0, _paths[left ? 0 : 1].lastIndexOf('/')); - if (_paths[left ? 0 : 1] == '') { - _paths[left ? 0 : 1] = '/'; - } - return; + Future listDir(String path, bool left, {SSHClient? client}) async { + if (client != null) { + final sftpc = await client.sftp(); + _status.setClient(left, sftpc); } - _paths[left ? 0 : 1] = _paths[left ? 0 : 1] + - (_paths[left ? 0 : 1].endsWith('/') ? '' : '/') + - filename; - } - - Future listDir(SSHClient client, String path, bool left) async { - final sftpc = await client.sftp(); - _clients[left ? 0 : 1] = sftpc; - final fs = await sftpc.listdir(path); + final fs = await _status.client(left)!.listdir(path); fs.sort((a, b) => a.filename.compareTo(b.filename)); fs.removeAt(0); if (mounted) { setState(() { - _files[left ? 0 : 1] = fs; + _status.setFiles(left, fs); }); } } Widget _buildDestSelector(bool left) { - return Column( - children: locator() - .servers - .map((e) => _buildDestSelectorItem(e.info, left)) - .toList(), - ); + final str = _status.path(left)?.path; + return ExpansionTile( + title: Text(_status.spi(left)?.name ?? 'Choose target'), + subtitle: _status.selected(left) + ? LayoutBuilder(builder: (context, size) { + bool exceeded = false; + int len = 0; + for (; !exceeded && len < str!.length; len++) { + // Build the textspan + var span = TextSpan( + text: '...' + str.substring(str.length - len), + style: TextStyle( + fontSize: + Theme.of(context).textTheme.bodyText1?.fontSize ?? + 14), + ); + + // Use a textpainter to determine if it will exceed max lines + var tp = TextPainter( + maxLines: 1, + textAlign: TextAlign.left, + textDirection: TextDirection.ltr, + text: span, + ); + + // trigger it to layout + tp.layout(maxWidth: size.maxWidth); + + // whether the text overflowed or not + exceeded = tp.didExceedMaxLines; + } + + return Text( + (exceeded ? '...' : '') + str!.substring(str.length - len), + overflow: TextOverflow.clip, + maxLines: 1, + style: const TextStyle(color: Colors.grey), + ); + }) + : null, + children: locator() + .servers + .map((e) => _buildDestSelectorItem(e.info, left)) + .toList()); } Widget _buildDestSelectorItem(ServerPrivateInfo spi, bool left) { @@ -327,10 +337,14 @@ class _SFTPPageState extends State { title: Text(spi.name), subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'), onTap: () { - setState(() { - _destSpi[left ? 0 : 1] = spi; - _selectedDest[left ? 0 : 1] = true; - }); + _status.setSpi(left, spi); + _status.setSelect(left, true); + _status.setPath(left, AbsolutePath('/')); + listDir('/', left, + client: locator() + .servers + .firstWhere((s) => s.info == spi) + .client); }, ); }