mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 23:34:24 +01:00
improved sftp
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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或域名"),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -141,5 +141,7 @@
|
||||
"preview": "Preview",
|
||||
"isBusy": "Is busy now",
|
||||
"imagesList": "Images list",
|
||||
"dockerImagesFmt": "{count} images"
|
||||
"dockerImagesFmt": "{count} images",
|
||||
"path": "Path",
|
||||
"goto": "Go to"
|
||||
}
|
||||
@@ -141,5 +141,7 @@
|
||||
"preview": "预览",
|
||||
"isBusy": "当前正忙",
|
||||
"imagesList": "镜像列表",
|
||||
"dockerImagesFmt": "共 {count} 个镜像"
|
||||
"dockerImagesFmt": "共 {count} 个镜像",
|
||||
"path": "路径",
|
||||
"goto": "前往"
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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> {
|
||||
)
|
||||
],
|
||||
);
|
||||
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<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: '/',
|
||||
);
|
||||
},
|
||||
);
|
||||
Future<void> backward() async {
|
||||
if (_status.path!.undo()) {
|
||||
await listDir();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user