#33 new: upload file to sftp from file picker
This commit is contained in:
@@ -26,7 +26,7 @@ import 'ping.dart';
|
||||
import 'private_key/list.dart';
|
||||
import 'server/tab.dart';
|
||||
import 'setting.dart';
|
||||
import 'sftp/downloaded.dart';
|
||||
import 'sftp/local.dart';
|
||||
import 'snippet/list.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:after_layout/after_layout.dart';
|
||||
import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
@@ -26,7 +25,7 @@ import '../../widget/round_rect_card.dart';
|
||||
import '../../widget/url_text.dart';
|
||||
import '../docker.dart';
|
||||
import '../pkg.dart';
|
||||
import '../sftp/view.dart';
|
||||
import '../sftp/remote.dart';
|
||||
import '../ssh.dart';
|
||||
import 'detail.dart';
|
||||
import 'edit.dart';
|
||||
@@ -80,9 +79,53 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagsSwitcher(ServerProvider pro) {
|
||||
if (pro.tags.isEmpty) return placeholder;
|
||||
final items = <String?>[null, ...pro.tags];
|
||||
Widget _buildBody() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
await _serverProvider.refreshData(onlyFailed: true),
|
||||
child: Consumer<ServerProvider>(
|
||||
builder: (_, pro, __) {
|
||||
if (!pro.tags.contains(_tag)) {
|
||||
_tag = null;
|
||||
}
|
||||
if (pro.serverOrder.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
final filtered = pro.serverOrder
|
||||
.where((e) => pro.servers.containsKey(e))
|
||||
.where((e) =>
|
||||
_tag == null ||
|
||||
(pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
|
||||
.toList();
|
||||
return ReorderableListView.builder(
|
||||
header: _buildTagsSwitcher(pro.tags),
|
||||
padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
|
||||
onReorder: (oldIndex, newIndex) => setState(() {
|
||||
pro.serverOrder.moveById(
|
||||
filtered[oldIndex],
|
||||
filtered[newIndex],
|
||||
_settingStore.serverOrder,
|
||||
);
|
||||
}),
|
||||
itemBuilder: (_, index) => _buildEachServerCard(
|
||||
pro.servers[filtered[index]],
|
||||
index,
|
||||
),
|
||||
itemCount: filtered.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagsSwitcher(List<String> tags) {
|
||||
if (tags.isEmpty) return placeholder;
|
||||
final items = <String?>[null, ...tags];
|
||||
return Container(
|
||||
height: 37,
|
||||
width: _media.size.width,
|
||||
@@ -125,51 +168,6 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
await _serverProvider.refreshData(onlyFailed: true),
|
||||
child: Consumer<ServerProvider>(
|
||||
builder: (_, pro, __) {
|
||||
if (!pro.tags.contains(_tag)) {
|
||||
_tag = null;
|
||||
}
|
||||
if (pro.serverOrder.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
final filtered = pro.serverOrder
|
||||
.where((e) => pro.servers.containsKey(e))
|
||||
.where((e) =>
|
||||
_tag == null ||
|
||||
(pro.servers[e]?.spi.tags?.contains(_tag) ?? false))
|
||||
.toList();
|
||||
return AnimationLimiter(
|
||||
key: ValueKey(_tag),
|
||||
child: ReorderableListView.builder(
|
||||
header: _buildTagsSwitcher(pro),
|
||||
padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
onReorder: (oldIndex, newIndex) => setState(() {
|
||||
pro.serverOrder.moveById(
|
||||
filtered[oldIndex],
|
||||
filtered[newIndex],
|
||||
_settingStore.serverOrder,
|
||||
);
|
||||
}),
|
||||
itemBuilder: (context, index) =>
|
||||
_buildEachServerCard(pro.servers[filtered[index]], index),
|
||||
itemCount: filtered.length,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(Server? si, int index) {
|
||||
if (si == null) {
|
||||
return placeholder;
|
||||
@@ -180,18 +178,12 @@ class _ServerPageState extends State<ServerPage>
|
||||
ServerDetailPage(si.spi.id),
|
||||
'server detail page',
|
||||
).go(context),
|
||||
child: AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: RoundRectCard(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: _buildRealServerCard(si.status, si.state, si.spi),
|
||||
),
|
||||
)))),
|
||||
child: RoundRectCard(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: _buildRealServerCard(si.status, si.state, si.spi),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import '../../../data/model/app/path_with_prefix.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import 'downloading.dart';
|
||||
import 'mission.dart';
|
||||
|
||||
class SFTPDownloadedPage extends StatefulWidget {
|
||||
final bool isPickFile;
|
||||
@@ -31,7 +31,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_s.download,
|
||||
_s.mission,
|
||||
style: textSize18,
|
||||
),
|
||||
),
|
||||
@@ -61,7 +61,11 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
|
||||
{Widget? trailing}) {
|
||||
return RoundRectCard(
|
||||
ListTile(
|
||||
title: Text(status.fileName),
|
||||
title: Text(
|
||||
status.fileName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(
|
||||
@@ -81,21 +85,26 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
|
||||
switch (status.status) {
|
||||
case SftpWorkerStatus.finished:
|
||||
final time = status.spentTime.toString();
|
||||
final str = '${_s.finished} ${_s.spentTime(
|
||||
time == 'null' ? _s.unknown : (time.substring(0, time.length - 7)),
|
||||
)}';
|
||||
return _wrapInCard(
|
||||
status,
|
||||
'${_s.downloadFinished} ${_s.spentTime(time == 'null' ? _s.unknown : (time.substring(0, time.length - 7)))}',
|
||||
str,
|
||||
trailing: IconButton(
|
||||
onPressed: () => shareFiles(context, [status.item.localPath]),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
),
|
||||
);
|
||||
case SftpWorkerStatus.downloading:
|
||||
final percentStr = (status.progress ?? 0.0).toStringAsFixed(2);
|
||||
final percent = (status.progress ?? 0) / 100;
|
||||
final size = (status.size ?? 0).convertBytes;
|
||||
return _wrapInCard(
|
||||
status,
|
||||
_s.downloadStatus((status.progress ?? 0.0).toStringAsFixed(2),
|
||||
(status.size ?? 0).convertBytes),
|
||||
trailing:
|
||||
CircularProgressIndicator(value: (status.progress ?? 0) / 100));
|
||||
status,
|
||||
_s.downloadStatus(percentStr, size),
|
||||
trailing: CircularProgressIndicator(value: percent),
|
||||
);
|
||||
case SftpWorkerStatus.preparing:
|
||||
return _wrapInCard(status, _s.sftpDlPrepare, trailing: loadingIcon);
|
||||
case SftpWorkerStatus.sshConnectted:
|
||||
@@ -104,10 +113,7 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
|
||||
return _wrapInCard(
|
||||
status,
|
||||
_s.unknown,
|
||||
trailing: const Icon(
|
||||
Icons.error,
|
||||
size: 40,
|
||||
),
|
||||
trailing: const Icon(Icons.error, size: 40),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
@@ -8,7 +8,7 @@ import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/extension/sftpfile.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/view/page/editor.dart';
|
||||
import 'package:toolbox/view/page/sftp/downloaded.dart';
|
||||
import 'package:toolbox/view/page/sftp/local.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/extension/stringx.dart';
|
||||
@@ -24,12 +24,11 @@ import '../../../data/provider/server.dart';
|
||||
import '../../../data/provider/sftp.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/private_key.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import '../../widget/input_field.dart';
|
||||
import '../../widget/two_line_text.dart';
|
||||
import 'downloading.dart';
|
||||
import 'mission.dart';
|
||||
|
||||
class SFTPPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
@@ -44,6 +43,8 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
final SftpBrowserStatus _status = SftpBrowserStatus();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
final _sftp = locator<SftpProvider>();
|
||||
|
||||
late S _s;
|
||||
|
||||
ServerState? _state;
|
||||
@@ -117,12 +118,38 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
Widget _buildUploadBtn() {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final path = await AppRoute(
|
||||
const SFTPDownloadedPage(
|
||||
isPickFile: true,
|
||||
final idx = await showRoundDialog(
|
||||
context: context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(_s.system),
|
||||
onTap: () => context.pop(1),
|
||||
),
|
||||
'sftp dled pick')
|
||||
.go<String>(context);
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: Text(_s.inner),
|
||||
onTap: () => context.pop(0),
|
||||
),
|
||||
],
|
||||
));
|
||||
final path = await () async {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return await AppRoute(
|
||||
const SFTPDownloadedPage(
|
||||
isPickFile: true,
|
||||
),
|
||||
'sftp dled pick')
|
||||
.go<String>(context);
|
||||
case 1:
|
||||
return await pickOneFile();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
@@ -131,7 +158,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
showSnackBar(context, const Text('remote path is null'));
|
||||
return;
|
||||
}
|
||||
locator<SftpProvider>().add(
|
||||
_sftp.add(
|
||||
SftpReqItem(widget.spi, remotePath, path),
|
||||
SftpReqType.upload,
|
||||
);
|
||||
@@ -302,39 +329,20 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
final file = await _status.client!.open(
|
||||
_getRemotePath(name),
|
||||
mode: SftpFileOpenMode.read | SftpFileOpenMode.write,
|
||||
);
|
||||
final localPath = '${(await sftpDir).path}${_getRemotePath(name)}';
|
||||
await Directory(localPath.substring(0, localPath.lastIndexOf('/')))
|
||||
.create(recursive: true);
|
||||
final local = File(localPath);
|
||||
if (await local.exists()) {
|
||||
await local.delete();
|
||||
}
|
||||
final localFile = local.openWrite(mode: FileMode.append);
|
||||
const defaultChunkSize = 1024 * 1024;
|
||||
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
|
||||
for (var i = 0; i < size; i += chunkSize) {
|
||||
final fileData = file.read(length: chunkSize);
|
||||
await for (var form in fileData) {
|
||||
localFile.add(form);
|
||||
}
|
||||
}
|
||||
await localFile.close();
|
||||
context.pop();
|
||||
final remotePath = _getRemotePath(name);
|
||||
final localPath = await _getLocalPath(remotePath);
|
||||
final completer = Completer();
|
||||
final req = SftpReqItem(widget.spi, remotePath, localPath);
|
||||
_sftp.add(req, SftpReqType.download, completer: completer);
|
||||
await completer.future;
|
||||
|
||||
final result = await AppRoute(
|
||||
EditorPage(path: localPath),
|
||||
'SFTP edit',
|
||||
).go<String>(context);
|
||||
if (result != null) {
|
||||
await local.writeAsString(result);
|
||||
await file.writeBytes(result.uint8List);
|
||||
showSnackBar(context, Text(_s.saved));
|
||||
_sftp.add(req, SftpReqType.upload);
|
||||
}
|
||||
await file.close();
|
||||
}
|
||||
|
||||
void _download(BuildContext context, SftpName name) {
|
||||
@@ -351,18 +359,14 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
final remotePath = _getRemotePath(name);
|
||||
final local = '${(await sftpDir).path}$remotePath';
|
||||
final pubKeyId = widget.spi.pubKeyId;
|
||||
final key = locator<PrivateKeyStore>().get(pubKeyId)?.privateKey;
|
||||
|
||||
locator<SftpProvider>().add(
|
||||
_sftp.add(
|
||||
SftpReqItem(
|
||||
widget.spi,
|
||||
remotePath,
|
||||
local,
|
||||
await _getLocalPath(remotePath),
|
||||
),
|
||||
SftpReqType.download,
|
||||
key: key,
|
||||
);
|
||||
|
||||
context.pop();
|
||||
@@ -564,6 +568,10 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
return pathJoin(prePath, name.filename);
|
||||
}
|
||||
|
||||
Future<String> _getLocalPath(String remotePath) async {
|
||||
return '${(await sftpDir).path}$remotePath';
|
||||
}
|
||||
|
||||
Future<void> _listDir({String? path, SSHClient? client}) async {
|
||||
if (_status.isBusy) {
|
||||
return;
|
||||
@@ -23,7 +23,7 @@ import '../../data/res/terminal.dart';
|
||||
import '../../data/res/virtual_key.dart';
|
||||
import '../../data/store/setting.dart';
|
||||
import '../../locator.dart';
|
||||
import 'sftp/view.dart';
|
||||
import 'sftp/remote.dart';
|
||||
|
||||
class SSHPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
@@ -3,8 +3,13 @@ import 'package:flutter/material.dart';
|
||||
/// 渐隐渐显实现
|
||||
class FadeIn extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
|
||||
const FadeIn({Key? key, required this.child}) : super(key: key);
|
||||
const FadeIn({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.duration = const Duration(milliseconds: 377),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MyFadeInState createState() => _MyFadeInState();
|
||||
@@ -19,7 +24,7 @@ class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 377),
|
||||
duration: widget.duration,
|
||||
);
|
||||
_animation = Tween(
|
||||
begin: 0.0,
|
||||
|
||||
Reference in New Issue
Block a user