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 = {
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";

View File

@@ -1,29 +1,35 @@
class AbsolutePath {
String path;
String? _prePath;
AbsolutePath(this.path);
String _path;
String get path => _path;
final List<String> _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;
}
}

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';
class SftpBrowserStatus {
bool selected = false;
ServerPrivateInfo? spi;
List<SftpName>? files;
AbsolutePath? path;
SftpClient? client;

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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或域名"),

View File

@@ -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<S> {

View File

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

View File

@@ -141,5 +141,7 @@
"preview": "预览",
"isBusy": "当前正忙",
"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: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';

View File

@@ -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';

View File

@@ -31,12 +31,14 @@ class SFTPPage extends StatefulWidget {
class _SFTPPageState extends State<SFTPPage> {
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<SFTPPage> {
@override
void initState() {
super.initState();
_status.spi = widget.spi;
_status.selected = true;
final serverProvider = locator<ServerProvider>();
_si = serverProvider.servers.firstWhere((s) => s.info == widget.spi);
_client = _si?.client;
}
@override
@@ -86,9 +89,73 @@ class _SFTPPageState extends State<SFTPPage> {
],
),
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(
child: Column(
children: [
@@ -101,48 +168,36 @@ class _SFTPPageState extends State<SFTPPage> {
);
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<ServerProvider>().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<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) {
showRoundDialog(
context,
@@ -214,11 +293,11 @@ class _SFTPPageState extends State<SFTPPage> {
final remotePath =
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
final local = '${(await sftpDownloadDir).path}$remotePath';
final pubKeyId = _status.spi!.pubKeyId;
final pubKeyId = widget.spi.pubKeyId;
locator<SftpDownloadProvider>().add(
DownloadItem(
_status.spi!,
widget.spi,
remotePath,
local,
),
@@ -429,7 +508,7 @@ class _SFTPPageState extends State<SFTPPage> {
}
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<SFTPPage> {
)
],
);
await backward();
}
}
Future<void> backward() async {
if (_status.path!.undo()) {
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: '/',
);
},
);
}
}