diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f93f3b09..fd95fc9b 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 = 166; + CURRENT_PROJECT_VERSION = 172; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -362,7 +362,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.166; + MARKETING_VERSION = 1.0.172; 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 = 166; + CURRENT_PROJECT_VERSION = 172; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -492,7 +492,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.166; + MARKETING_VERSION = 1.0.172; 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 = 166; + CURRENT_PROJECT_VERSION = 172; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -516,7 +516,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.166; + MARKETING_VERSION = 1.0.172; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/data/model/sftp/absolute_path.dart b/lib/data/model/sftp/absolute_path.dart index 79eb9984..3c4b9057 100644 --- a/lib/data/model/sftp/absolute_path.dart +++ b/lib/data/model/sftp/absolute_path.dart @@ -1,29 +1,35 @@ class AbsolutePath { - String path; - String? _prePath; - AbsolutePath(this.path); + String _path; + String get path => _path; + final List _prePath; + + AbsolutePath(this._path) : _prePath = ['/']; void update(String newPath) { - _prePath = path; + _prePath.add(_path); if (newPath == '..') { - path = path.substring(0, path.lastIndexOf('/')); - if (path == '') { - path = '/'; + _path = _path.substring(0, _path.lastIndexOf('/')); + if (_path == '') { + _path = '/'; } return; } if (newPath == '/') { - path = '/'; + _path = '/'; return; } - path = path + (path.endsWith('/') ? '' : '/') + newPath; + if (newPath.startsWith('/')) { + _path = newPath; + return; + } + _path = _path + (_path.endsWith('/') ? '' : '/') + newPath; } bool undo() { - if (_prePath == null || _prePath == path) { + if (_prePath.isEmpty) { return false; } - path = _prePath!; + _path = _prePath.removeLast(); return true; } } diff --git a/lib/data/model/sftp/browser_status.dart b/lib/data/model/sftp/browser_status.dart index 1de3c822..325e44a8 100644 --- a/lib/data/model/sftp/browser_status.dart +++ b/lib/data/model/sftp/browser_status.dart @@ -3,8 +3,6 @@ import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/sftp/absolute_path.dart'; class SftpBrowserStatus { - bool selected = false; - ServerPrivateInfo? spi; List? files; AbsolutePath? path; SftpClient? client; diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index a5bd043d..0c9896cc 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 = 169; + static const int build = 173; static const String engine = - "Flutter 3.3.9 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b8f7f1f986 (3 weeks ago) • 2022-11-23 06:43:51 +0900\nEngine • revision 8f2221fbef\nTools • Dart 2.18.5 • DevTools 2.15.0\n"; - static const String buildAt = "2022-12-11 12:36:17.737879"; - static const int modifications = 8; + "Flutter 3.3.9 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b8f7f1f986 (4 weeks ago) • 2022-11-23 06:43:51 +0900\nEngine • revision 8f2221fbef\nTools • Dart 2.18.5 • DevTools 2.15.0\n"; + static const String buildAt = "2022-12-20 15:01:02.224953"; + static const int modifications = 12; } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 169ddd83..7561c401 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -131,6 +131,7 @@ class MessageLookup extends MessageLookupByLibrary { "go": MessageLookupByLibrary.simpleMessage("Go"), "goSftpDlPage": MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"), + "goto": MessageLookupByLibrary.simpleMessage("Go to"), "host": MessageLookupByLibrary.simpleMessage("Host"), "httpFailedWithCode": m6, "imagesList": MessageLookupByLibrary.simpleMessage("Images list"), @@ -175,6 +176,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("No update available"), "ok": MessageLookupByLibrary.simpleMessage("OK"), "open": MessageLookupByLibrary.simpleMessage("Open"), + "path": MessageLookupByLibrary.simpleMessage("Path"), "ping": MessageLookupByLibrary.simpleMessage("Ping"), "pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"), "pingInputIP": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index a5cb7b72..eb32cb99 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -119,6 +119,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundNUpdate": m5, "go": MessageLookupByLibrary.simpleMessage("开始"), "goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"), + "goto": MessageLookupByLibrary.simpleMessage("前往"), "host": MessageLookupByLibrary.simpleMessage("主机"), "httpFailedWithCode": m6, "imagesList": MessageLookupByLibrary.simpleMessage("镜像列表"), @@ -154,6 +155,7 @@ class MessageLookup extends MessageLookupByLibrary { "noUpdateAvailable": MessageLookupByLibrary.simpleMessage("没有可用更新"), "ok": MessageLookupByLibrary.simpleMessage("好"), "open": MessageLookupByLibrary.simpleMessage("打开"), + "path": MessageLookupByLibrary.simpleMessage("路径"), "ping": MessageLookupByLibrary.simpleMessage("Ping"), "pingAvg": MessageLookupByLibrary.simpleMessage("平均:"), "pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 15681b9c..bcad610f 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1480,6 +1480,26 @@ class S { args: [count], ); } + + /// `Path` + String get path { + return Intl.message( + 'Path', + name: 'path', + desc: '', + args: [], + ); + } + + /// `Go to` + String get goto { + return Intl.message( + 'Go to', + name: 'goto', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c5062b4f..09f14ced 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -141,5 +141,7 @@ "preview": "Preview", "isBusy": "Is busy now", "imagesList": "Images list", - "dockerImagesFmt": "{count} images" + "dockerImagesFmt": "{count} images", + "path": "Path", + "goto": "Go to" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index c1c83f23..1498b594 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -141,5 +141,7 @@ "preview": "预览", "isBusy": "当前正忙", "imagesList": "镜像列表", - "dockerImagesFmt": "共 {count} 个镜像" + "dockerImagesFmt": "共 {count} 个镜像", + "path": "路径", + "goto": "前往" } \ No newline at end of file diff --git a/lib/view/page/private_key/list.dart b/lib/view/page/private_key/list.dart index 9e49a65a..44fabe43 100644 --- a/lib/view/page/private_key/list.dart +++ b/lib/view/page/private_key/list.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:toolbox/core/route.dart'; import 'package:toolbox/data/provider/private_key.dart'; -import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/font_style.dart'; import 'package:toolbox/data/res/padding.dart'; import 'package:toolbox/generated/l10n.dart'; diff --git a/lib/view/page/sftp/downloaded.dart b/lib/view/page/sftp/downloaded.dart index 95c8ecb2..fbe0b613 100644 --- a/lib/view/page/sftp/downloaded.dart +++ b/lib/view/page/sftp/downloaded.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:toolbox/core/extension/colorx.dart'; import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/route.dart'; diff --git a/lib/view/page/sftp/view.dart b/lib/view/page/sftp/view.dart index 3d714643..68045b59 100644 --- a/lib/view/page/sftp/view.dart +++ b/lib/view/page/sftp/view.dart @@ -31,12 +31,14 @@ class SFTPPage extends StatefulWidget { class _SFTPPageState extends State { final SftpBrowserStatus _status = SftpBrowserStatus(); - final ScrollController _scrollController = ScrollController(); late MediaQueryData _media; late S _s; + ServerInfo? _si; + SSHClient? _client; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -47,8 +49,9 @@ class _SFTPPageState extends State { @override void initState() { super.initState(); - _status.spi = widget.spi; - _status.selected = true; + final serverProvider = locator(); + _si = serverProvider.servers.firstWhere((s) => s.info == widget.spi); + _client = _si?.client; } @override @@ -86,9 +89,73 @@ class _SFTPPageState extends State { ], ), body: _buildFileView(), + bottomNavigationBar: _buildPath(), ); } + Widget _buildPath() { + return SafeArea( + child: Container( + padding: const EdgeInsets.fromLTRB(11, 7, 11, 11), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(), + (_status.path?.path ?? _s.loadingFiles).omitStartStr(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + padding: const EdgeInsets.all(0), + onPressed: () async { + await backward(); + }, + icon: const Icon(Icons.arrow_back), + ), + IconButton( + padding: const EdgeInsets.all(0), + onPressed: () async { + final p = await showRoundDialog( + context, + _s.goto, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: InputDecoration( + labelText: _s.path, + hintText: '/', + ), + onSubmitted: (value) => + Navigator.of(context).pop(value), + ), + ], + ), + [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(_s.cancel)) + ], + ); + + if (p != null) { + if (p.isEmpty) { + showSnackBar(context, Text(_s.fieldMustNotEmpty)); + return; + } + _status.path?.update(p); + listDir(path: p); + } + }, + icon: const Icon(Icons.gps_fixed), + ) + ], + ) + ], + ), + )); + } + Widget get centerCircleLoading => Center( child: Column( children: [ @@ -101,48 +168,36 @@ class _SFTPPageState extends State { ); Widget _buildFileView() { - if (!_status.selected) { - return ListView( - children: [ - _buildDestSelector(), - ], - ); + if (_client == null || + _si?.connectionState != ServerConnectionState.connected) { + return centerCircleLoading; } - final spi = _status.spi; - final si = - locator().servers.firstWhere((s) => s.info == spi); - final client = si.client; - if (client == null || - si.connectionState != ServerConnectionState.connected) { + + if (_status.isBusy) { return centerCircleLoading; } if (_status.files == null) { _status.path = AbsolutePath('/'); - listDir(path: '/', client: client); + listDir(path: '/', client: _client); return centerCircleLoading; } else { return RefreshIndicator( child: FadeIn( - key: Key(_status.spi!.name + _status.path!.path), + key: Key(widget.spi.name + _status.path!.path), child: ListView.builder( - itemCount: _status.files!.length + 1, + itemCount: _status.files!.length, controller: _scrollController, itemBuilder: (context, index) { - if (index == 0) { - return _buildDestSelector(); - } - final file = _status.files![index - 1]; + final file = _status.files![index]; final isDir = file.attr.isDirectory; return ListTile( leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), title: Text(file.filename), trailing: Text( - DateTime.fromMillisecondsSinceEpoch( - (file.attr.modifyTime ?? 0) * 1000) - .toString() - .replaceFirst('.000', ''), + '${getTime(file.attr.modifyTime)}\n${getMode(file.attr.mode)}', style: const TextStyle(color: Colors.grey), + textAlign: TextAlign.right, ), subtitle: isDir ? null : Text((file.attr.size ?? 0).convertBytes), @@ -164,6 +219,30 @@ class _SFTPPageState extends State { } } + String getTime(int? unixMill) { + return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000) + .toString() + .replaceFirst('.000', ''); + } + + String getMode(SftpFileMode? mode) { + if (mode == null) { + return '---'; + } + + final user = getRoleMode(mode.userRead, mode.userWrite, mode.userExecute); + final group = + getRoleMode(mode.groupRead, mode.groupWrite, mode.groupExecute); + final other = + getRoleMode(mode.otherRead, mode.otherWrite, mode.otherExecute); + + return '$user$group$other'; + } + + String getRoleMode(bool r, bool w, bool x) { + return '${r ? 'r' : '-'}${w ? 'w' : '-'}${x ? 'x' : '-'}'; + } + void onItemPress(BuildContext context, SftpName file, bool showDownload) { showRoundDialog( context, @@ -214,11 +293,11 @@ class _SFTPPageState extends State { final remotePath = prePath + (prePath.endsWith('/') ? '' : '/') + name.filename; final local = '${(await sftpDownloadDir).path}$remotePath'; - final pubKeyId = _status.spi!.pubKeyId; + final pubKeyId = widget.spi.pubKeyId; locator().add( DownloadItem( - _status.spi!, + widget.spi, remotePath, local, ), @@ -429,7 +508,7 @@ class _SFTPPageState extends State { } try { final fs = - await _status.client!.listdir(path ?? (_status.path?.path ?? '/')); + await _status.client!.listdir(path ?? _status.path?.path ?? '/'); fs.sort((a, b) => a.filename.compareTo(b.filename)); fs.removeAt(0); if (mounted) { @@ -450,41 +529,13 @@ class _SFTPPageState extends State { ) ], ); - if (_status.path!.undo()) { - await listDir(); - } + await backward(); } } - Widget _buildDestSelector() { - final str = _status.path?.path; - return ExpansionTile( - title: Text(_status.spi?.name ?? _s.chooseDestination), - subtitle: _status.selected - ? str!.omitStartStr(style: const TextStyle(color: Colors.grey)) - : null, - children: locator() - .servers - .map((e) => _buildDestSelectorItem(e.info)) - .toList()); - } - - Widget _buildDestSelectorItem(ServerPrivateInfo spi) { - return ListTile( - title: Text(spi.name), - subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'), - onTap: () { - _status.spi = spi; - _status.selected = true; - _status.path = AbsolutePath('/'); - listDir( - client: locator() - .servers - .firstWhere((s) => s.info == spi) - .client, - path: '/', - ); - }, - ); + Future backward() async { + if (_status.path!.undo()) { + await listDir(); + } } }