improved sftp

This commit is contained in:
lollipopkit
2022-12-20 15:11:22 +08:00
parent 6bda94bd7b
commit 599bd2cfd3
12 changed files with 170 additions and 89 deletions

View File

@@ -354,7 +354,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 166; CURRENT_PROJECT_VERSION = 172;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -362,7 +362,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.166; MARKETING_VERSION = 1.0.172;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -484,7 +484,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 166; CURRENT_PROJECT_VERSION = 172;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -492,7 +492,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.166; MARKETING_VERSION = 1.0.172;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -508,7 +508,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 166; CURRENT_PROJECT_VERSION = 172;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -516,7 +516,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.166; MARKETING_VERSION = 1.0.172;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -1,29 +1,35 @@
class AbsolutePath { class AbsolutePath {
String path; String _path;
String? _prePath; String get path => _path;
AbsolutePath(this.path); final List<String> _prePath;
AbsolutePath(this._path) : _prePath = ['/'];
void update(String newPath) { void update(String newPath) {
_prePath = path; _prePath.add(_path);
if (newPath == '..') { if (newPath == '..') {
path = path.substring(0, path.lastIndexOf('/')); _path = _path.substring(0, _path.lastIndexOf('/'));
if (path == '') { if (_path == '') {
path = '/'; _path = '/';
} }
return; return;
} }
if (newPath == '/') { if (newPath == '/') {
path = '/'; _path = '/';
return; return;
} }
path = path + (path.endsWith('/') ? '' : '/') + newPath; if (newPath.startsWith('/')) {
_path = newPath;
return;
}
_path = _path + (_path.endsWith('/') ? '' : '/') + newPath;
} }
bool undo() { bool undo() {
if (_prePath == null || _prePath == path) { if (_prePath.isEmpty) {
return false; return false;
} }
path = _prePath!; _path = _prePath.removeLast();
return true; return true;
} }
} }

View File

@@ -3,8 +3,6 @@ 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/absolute_path.dart';
class SftpBrowserStatus { class SftpBrowserStatus {
bool selected = false;
ServerPrivateInfo? spi;
List<SftpName>? files; List<SftpName>? files;
AbsolutePath? path; AbsolutePath? path;
SftpClient? client; SftpClient? client;

View File

@@ -2,9 +2,9 @@
class BuildData { class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 169; static const int build = 173;
static const String engine = 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"; "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-11 12:36:17.737879"; static const String buildAt = "2022-12-20 15:01:02.224953";
static const int modifications = 8; static const int modifications = 12;
} }

View File

@@ -131,6 +131,7 @@ class MessageLookup extends MessageLookupByLibrary {
"go": MessageLookupByLibrary.simpleMessage("Go"), "go": MessageLookupByLibrary.simpleMessage("Go"),
"goSftpDlPage": "goSftpDlPage":
MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"), MessageLookupByLibrary.simpleMessage("Go to SFTP download page?"),
"goto": MessageLookupByLibrary.simpleMessage("Go to"),
"host": MessageLookupByLibrary.simpleMessage("Host"), "host": MessageLookupByLibrary.simpleMessage("Host"),
"httpFailedWithCode": m6, "httpFailedWithCode": m6,
"imagesList": MessageLookupByLibrary.simpleMessage("Images list"), "imagesList": MessageLookupByLibrary.simpleMessage("Images list"),
@@ -175,6 +176,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("No update available"), MessageLookupByLibrary.simpleMessage("No update available"),
"ok": MessageLookupByLibrary.simpleMessage("OK"), "ok": MessageLookupByLibrary.simpleMessage("OK"),
"open": MessageLookupByLibrary.simpleMessage("Open"), "open": MessageLookupByLibrary.simpleMessage("Open"),
"path": MessageLookupByLibrary.simpleMessage("Path"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"), "ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"), "pingAvg": MessageLookupByLibrary.simpleMessage("Avg:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage( "pingInputIP": MessageLookupByLibrary.simpleMessage(

View File

@@ -119,6 +119,7 @@ class MessageLookup extends MessageLookupByLibrary {
"foundNUpdate": m5, "foundNUpdate": m5,
"go": MessageLookupByLibrary.simpleMessage("开始"), "go": MessageLookupByLibrary.simpleMessage("开始"),
"goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"), "goSftpDlPage": MessageLookupByLibrary.simpleMessage("前往下载页?"),
"goto": MessageLookupByLibrary.simpleMessage("前往"),
"host": MessageLookupByLibrary.simpleMessage("主机"), "host": MessageLookupByLibrary.simpleMessage("主机"),
"httpFailedWithCode": m6, "httpFailedWithCode": m6,
"imagesList": MessageLookupByLibrary.simpleMessage("镜像列表"), "imagesList": MessageLookupByLibrary.simpleMessage("镜像列表"),
@@ -154,6 +155,7 @@ class MessageLookup extends MessageLookupByLibrary {
"noUpdateAvailable": MessageLookupByLibrary.simpleMessage("没有可用更新"), "noUpdateAvailable": MessageLookupByLibrary.simpleMessage("没有可用更新"),
"ok": MessageLookupByLibrary.simpleMessage(""), "ok": MessageLookupByLibrary.simpleMessage(""),
"open": MessageLookupByLibrary.simpleMessage("打开"), "open": MessageLookupByLibrary.simpleMessage("打开"),
"path": MessageLookupByLibrary.simpleMessage("路径"),
"ping": MessageLookupByLibrary.simpleMessage("Ping"), "ping": MessageLookupByLibrary.simpleMessage("Ping"),
"pingAvg": MessageLookupByLibrary.simpleMessage("平均:"), "pingAvg": MessageLookupByLibrary.simpleMessage("平均:"),
"pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"), "pingInputIP": MessageLookupByLibrary.simpleMessage("请输入目标IP或域名"),

View File

@@ -1480,6 +1480,26 @@ class S {
args: [count], 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<S> { class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -141,5 +141,7 @@
"preview": "Preview", "preview": "Preview",
"isBusy": "Is busy now", "isBusy": "Is busy now",
"imagesList": "Images list", "imagesList": "Images list",
"dockerImagesFmt": "{count} images" "dockerImagesFmt": "{count} images",
"path": "Path",
"goto": "Go to"
} }

View File

@@ -141,5 +141,7 @@
"preview": "预览", "preview": "预览",
"isBusy": "当前正忙", "isBusy": "当前正忙",
"imagesList": "镜像列表", "imagesList": "镜像列表",
"dockerImagesFmt": "共 {count} 个镜像" "dockerImagesFmt": "共 {count} 个镜像",
"path": "路径",
"goto": "前往"
} }

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/provider/private_key.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/font_style.dart';
import 'package:toolbox/data/res/padding.dart'; import 'package:toolbox/data/res/padding.dart';
import 'package:toolbox/generated/l10n.dart'; import 'package:toolbox/generated/l10n.dart';

View File

@@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/colorx.dart';
import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';

View File

@@ -31,12 +31,14 @@ class SFTPPage extends StatefulWidget {
class _SFTPPageState extends State<SFTPPage> { class _SFTPPageState extends State<SFTPPage> {
final SftpBrowserStatus _status = SftpBrowserStatus(); final SftpBrowserStatus _status = SftpBrowserStatus();
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
late MediaQueryData _media; late MediaQueryData _media;
late S _s; late S _s;
ServerInfo? _si;
SSHClient? _client;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -47,8 +49,9 @@ class _SFTPPageState extends State<SFTPPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_status.spi = widget.spi; final serverProvider = locator<ServerProvider>();
_status.selected = true; _si = serverProvider.servers.firstWhere((s) => s.info == widget.spi);
_client = _si?.client;
} }
@override @override
@@ -86,9 +89,73 @@ class _SFTPPageState extends State<SFTPPage> {
], ],
), ),
body: _buildFileView(), 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<String?>(
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( Widget get centerCircleLoading => Center(
child: Column( child: Column(
children: [ children: [
@@ -101,48 +168,36 @@ class _SFTPPageState extends State<SFTPPage> {
); );
Widget _buildFileView() { Widget _buildFileView() {
if (!_status.selected) { if (_client == null ||
return ListView( _si?.connectionState != ServerConnectionState.connected) {
children: [ return centerCircleLoading;
_buildDestSelector(),
],
);
} }
final spi = _status.spi;
final si = if (_status.isBusy) {
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
final client = si.client;
if (client == null ||
si.connectionState != ServerConnectionState.connected) {
return centerCircleLoading; return centerCircleLoading;
} }
if (_status.files == null) { if (_status.files == null) {
_status.path = AbsolutePath('/'); _status.path = AbsolutePath('/');
listDir(path: '/', client: client); listDir(path: '/', client: _client);
return centerCircleLoading; return centerCircleLoading;
} else { } else {
return RefreshIndicator( return RefreshIndicator(
child: FadeIn( child: FadeIn(
key: Key(_status.spi!.name + _status.path!.path), key: Key(widget.spi.name + _status.path!.path),
child: ListView.builder( child: ListView.builder(
itemCount: _status.files!.length + 1, itemCount: _status.files!.length,
controller: _scrollController, controller: _scrollController,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { final file = _status.files![index];
return _buildDestSelector();
}
final file = _status.files![index - 1];
final isDir = file.attr.isDirectory; final isDir = file.attr.isDirectory;
return ListTile( return ListTile(
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename), title: Text(file.filename),
trailing: Text( trailing: Text(
DateTime.fromMillisecondsSinceEpoch( '${getTime(file.attr.modifyTime)}\n${getMode(file.attr.mode)}',
(file.attr.modifyTime ?? 0) * 1000)
.toString()
.replaceFirst('.000', ''),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
textAlign: TextAlign.right,
), ),
subtitle: subtitle:
isDir ? null : Text((file.attr.size ?? 0).convertBytes), isDir ? null : Text((file.attr.size ?? 0).convertBytes),
@@ -164,6 +219,30 @@ class _SFTPPageState extends State<SFTPPage> {
} }
} }
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) { void onItemPress(BuildContext context, SftpName file, bool showDownload) {
showRoundDialog( showRoundDialog(
context, context,
@@ -214,11 +293,11 @@ class _SFTPPageState extends State<SFTPPage> {
final remotePath = final remotePath =
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename; prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
final local = '${(await sftpDownloadDir).path}$remotePath'; final local = '${(await sftpDownloadDir).path}$remotePath';
final pubKeyId = _status.spi!.pubKeyId; final pubKeyId = widget.spi.pubKeyId;
locator<SftpDownloadProvider>().add( locator<SftpDownloadProvider>().add(
DownloadItem( DownloadItem(
_status.spi!, widget.spi,
remotePath, remotePath,
local, local,
), ),
@@ -429,7 +508,7 @@ class _SFTPPageState extends State<SFTPPage> {
} }
try { try {
final fs = 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.sort((a, b) => a.filename.compareTo(b.filename));
fs.removeAt(0); fs.removeAt(0);
if (mounted) { if (mounted) {
@@ -450,41 +529,13 @@ class _SFTPPageState extends State<SFTPPage> {
) )
], ],
); );
await backward();
}
}
Future<void> backward() async {
if (_status.path!.undo()) { if (_status.path!.undo()) {
await listDir(); await listDir();
} }
} }
}
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<ServerProvider>()
.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<ServerProvider>()
.servers
.firstWhere((s) => s.info == spi)
.client,
path: '/',
);
},
);
}
} }