opt.: cancel sftp mission
This commit is contained in:
303
lib/view/page/storage/local.dart
Normal file
303
lib/view/page/storage/local.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/data/model/sftp/req.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/sftp.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/editor.dart';
|
||||
import 'package:toolbox/view/page/storage/sftp.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/picker.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/extension/stringx.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/app/path_with_prefix.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import 'sftp_mission.dart';
|
||||
|
||||
class LocalStoragePage extends StatefulWidget {
|
||||
final bool isPickFile;
|
||||
final String? initDir;
|
||||
const LocalStoragePage({Key? key, this.isPickFile = false, this.initDir})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<LocalStoragePage> createState() => _LocalStoragePageState();
|
||||
}
|
||||
|
||||
class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
PathWithPrefix? _path;
|
||||
late S _s;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sftpDir.then((dir) {
|
||||
_path = PathWithPrefix(dir.path);
|
||||
if (widget.initDir != null) {
|
||||
_path!.update(widget.initDir!.replaceFirst('${dir.path}/', ''));
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_s = S.of(context)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_s.download),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () =>
|
||||
AppRoute(const SftpMissionPage(), 'sftp downloading')
|
||||
.go(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: FadeIn(
|
||||
key: UniqueKey(),
|
||||
child: _buildBody(),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: _buildPath(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPath() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Divider(),
|
||||
(_path?.path ?? _s.loadingFiles).omitStartStr(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_path == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
final dir = Directory(_path!.path);
|
||||
final files = dir.listSync();
|
||||
final canGoBack = _path!.canBack;
|
||||
return ListView.builder(
|
||||
itemCount: canGoBack ? files.length + 1 : files.length,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 && canGoBack) {
|
||||
return RoundRectCard(ListTile(
|
||||
leading: const Icon(Icons.keyboard_arrow_left),
|
||||
title: const Text('..'),
|
||||
onTap: () {
|
||||
_path!.update('..');
|
||||
setState(() {});
|
||||
},
|
||||
));
|
||||
}
|
||||
index = canGoBack ? index - 1 : index;
|
||||
var file = files[index];
|
||||
var fileName = file.path.split('/').last;
|
||||
var stat = file.statSync();
|
||||
var isDir = stat.type == FileSystemEntityType.directory;
|
||||
|
||||
return RoundRectCard(ListTile(
|
||||
leading: isDir
|
||||
? const Icon(Icons.folder)
|
||||
: const Icon(Icons.insert_drive_file),
|
||||
title: Text(fileName),
|
||||
subtitle: isDir ? null : Text(stat.size.convertBytes, style: grey),
|
||||
trailing: Text(
|
||||
stat.modified
|
||||
.toString()
|
||||
.substring(0, stat.modified.toString().length - 4),
|
||||
style: grey,
|
||||
),
|
||||
onTap: () async {
|
||||
if (!isDir) {
|
||||
await showFileActionDialog(file);
|
||||
return;
|
||||
}
|
||||
_path!.update(fileName);
|
||||
setState(() {});
|
||||
},
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showFileActionDialog(FileSystemEntity file) async {
|
||||
final fileName = file.path.split('/').last;
|
||||
if (widget.isPickFile) {
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.pickFile),
|
||||
child: Text(fileName),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
context.pop(file.path);
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text(_s.edit),
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
final stat = await file.stat();
|
||||
if (stat.size > editorMaxSize) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.fileTooLarge(fileName, stat.size, '1m')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final f = File(file.absolute.path);
|
||||
final result = await AppRoute(
|
||||
EditorPage(
|
||||
path: file.absolute.path,
|
||||
),
|
||||
'sftp dled editor',
|
||||
).go<String>(context);
|
||||
if (result != null) {
|
||||
f.writeAsString(result);
|
||||
showSnackBar(context, Text(_s.saved));
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.abc),
|
||||
title: Text(_s.rename),
|
||||
onTap: () {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.rename),
|
||||
child: Input(
|
||||
controller: TextEditingController(text: fileName),
|
||||
onSubmitted: (p0) {
|
||||
context.pop();
|
||||
final newPath = '${file.parent.path}/$p0';
|
||||
file.renameSync(newPath);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(_s.delete),
|
||||
onTap: () {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.delete),
|
||||
child: Text(_s.sureDelete(fileName)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
file.deleteSync();
|
||||
setState(() {});
|
||||
context.pop();
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.upload),
|
||||
title: Text(_s.upload),
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
final serverProvider = locator<ServerProvider>();
|
||||
final ids = serverProvider.serverOrder;
|
||||
var idx = 0;
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.server),
|
||||
child: Picker(
|
||||
items: ids.map((e) => Text(e)).toList(),
|
||||
onSelected: (idx_) => idx = idx_,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(), child: Text(_s.ok)),
|
||||
],
|
||||
);
|
||||
final id = ids[idx];
|
||||
final spi = serverProvider.servers[id]?.spi;
|
||||
if (spi == null) {
|
||||
return;
|
||||
}
|
||||
final remotePath = await AppRoute(
|
||||
SftpPage(
|
||||
spi,
|
||||
selectPath: true,
|
||||
),
|
||||
'SFTP page (select)',
|
||||
).go<String>(context);
|
||||
if (remotePath == null) {
|
||||
return;
|
||||
}
|
||||
locator<SftpProvider>().add(SftpReq(
|
||||
spi,
|
||||
remotePath,
|
||||
file.absolute.path,
|
||||
SftpReqType.upload,
|
||||
));
|
||||
showSnackBar(context, Text(_s.added2List));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(_s.open),
|
||||
onTap: () {
|
||||
shareFiles(context, [file.absolute.path]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
653
lib/view/page/storage/sftp.dart
Normal file
653
lib/view/page/storage/sftp.dart
Normal file
@@ -0,0 +1,653 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
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/storage/local.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/extension/stringx.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/sftp/absolute_path.dart';
|
||||
import '../../../data/model/sftp/browser_status.dart';
|
||||
import '../../../data/model/sftp/req.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/provider/sftp.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import '../../widget/input_field.dart';
|
||||
import '../../widget/two_line_text.dart';
|
||||
import 'sftp_mission.dart';
|
||||
|
||||
class SftpPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
final String? initPath;
|
||||
final bool selectPath;
|
||||
|
||||
const SftpPage(
|
||||
this.spi, {
|
||||
Key? key,
|
||||
this.initPath,
|
||||
this.selectPath = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SftpPageState createState() => _SftpPageState();
|
||||
}
|
||||
|
||||
class _SftpPageState extends State<SftpPage> {
|
||||
final SftpBrowserStatus _status = SftpBrowserStatus();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
final _sftp = locator<SftpProvider>();
|
||||
|
||||
late S _s;
|
||||
|
||||
ServerState? _state;
|
||||
SSHClient? _client;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_s = S.of(context)!;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final serverProvider = locator<ServerProvider>();
|
||||
_client = serverProvider.servers[widget.spi.id]?.client;
|
||||
_state = serverProvider.servers[widget.spi.id]?.state;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () => AppRoute(
|
||||
const SftpMissionPage(),
|
||||
'sftp downloading',
|
||||
).go(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildFileView(),
|
||||
bottomNavigationBar: _buildBottom(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottom() {
|
||||
final children = widget.selectPath
|
||||
? [
|
||||
IconButton(
|
||||
onPressed: () => context.pop(_status.path?.path),
|
||||
icon: const Icon(Icons.done))
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () async {
|
||||
await _backward();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
_buildAddBtn(),
|
||||
_buildGotoBtn(),
|
||||
_buildUploadBtn(),
|
||||
];
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
(_status.path?.path ?? _s.loadingFiles).omitStartStr(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadBtn() {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final idx = await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.choose),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(_s.system),
|
||||
onTap: () => context.pop(1),
|
||||
),
|
||||
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 LocalStoragePage(
|
||||
isPickFile: true,
|
||||
),
|
||||
'sftp dled pick')
|
||||
.go<String>(context);
|
||||
case 1:
|
||||
return await pickOneFile();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
final remotePath = _status.path?.path;
|
||||
if (remotePath == null) {
|
||||
showSnackBar(context, const Text('remote path is null'));
|
||||
return;
|
||||
}
|
||||
_sftp.add(
|
||||
SftpReq(
|
||||
widget.spi,
|
||||
remotePath,
|
||||
path,
|
||||
SftpReqType.upload,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.upload_file));
|
||||
}
|
||||
|
||||
Widget _buildAddBtn() {
|
||||
return IconButton(
|
||||
onPressed: (() => showRoundDialog(
|
||||
context: context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: Text(_s.createFolder),
|
||||
onTap: () => _mkdir(context)),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
title: Text(_s.createFile),
|
||||
onTap: () => _newFile(context)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.close),
|
||||
)
|
||||
],
|
||||
)),
|
||||
icon: const Icon(Icons.add),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGotoBtn() {
|
||||
return IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () async {
|
||||
final p = await showRoundDialog<String>(
|
||||
context: context,
|
||||
title: Text(_s.goto),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Input(
|
||||
label: _s.path,
|
||||
onSubmitted: (value) => context.pop(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.close),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
// p == null || p.isEmpty
|
||||
if (p?.isEmpty ?? true) {
|
||||
return;
|
||||
}
|
||||
_status.path?.update(p!);
|
||||
_listDir(path: p);
|
||||
},
|
||||
icon: const Icon(Icons.gps_fixed),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileView() {
|
||||
if (_client == null || _state != ServerState.connected) {
|
||||
return centerLoading;
|
||||
}
|
||||
|
||||
if (_status.isBusy) {
|
||||
return centerLoading;
|
||||
}
|
||||
|
||||
if (_status.files == null) {
|
||||
final p_ = widget.initPath ?? '/';
|
||||
_status.path = AbsolutePath(p_);
|
||||
_listDir(path: p_, client: _client);
|
||||
return centerLoading;
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
child: FadeIn(
|
||||
key: Key(widget.spi.name + _status.path!.path),
|
||||
child: ListView.builder(
|
||||
itemCount: _status.files!.length,
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
itemBuilder: (_, index) => _buildItem(_status.files![index]),
|
||||
),
|
||||
),
|
||||
onRefresh: () => _listDir(path: _status.path?.path),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(SftpName file) {
|
||||
final isDir = file.attr.isDirectory;
|
||||
final trailing = Text(
|
||||
'${getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
|
||||
style: grey,
|
||||
textAlign: TextAlign.right,
|
||||
);
|
||||
return RoundRectCard(ListTile(
|
||||
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
||||
title: Text(file.filename),
|
||||
trailing: trailing,
|
||||
subtitle: isDir
|
||||
? null
|
||||
: Text(
|
||||
(file.attr.size ?? 0).convertBytes,
|
||||
style: grey,
|
||||
),
|
||||
onTap: () {
|
||||
if (isDir) {
|
||||
_status.path?.update(file.filename);
|
||||
_listDir(path: _status.path?.path);
|
||||
} else {
|
||||
_onItemPress(context, file, true);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _onItemPress(context, file, !isDir),
|
||||
));
|
||||
}
|
||||
|
||||
void _onItemPress(BuildContext context, SftpName file, bool notDir) {
|
||||
final children = [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(_s.delete),
|
||||
onTap: () => _delete(context, file),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.abc),
|
||||
title: Text(_s.rename),
|
||||
onTap: () => _rename(context, file),
|
||||
),
|
||||
];
|
||||
if (notDir) {
|
||||
children.addAll([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text(_s.edit),
|
||||
onTap: () => _edit(context, file),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(_s.download),
|
||||
onTap: () => _download(context, file),
|
||||
),
|
||||
]);
|
||||
}
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _edit(BuildContext context, SftpName name) async {
|
||||
final size = name.attr.size;
|
||||
if (size == null || size > editorMaxSize) {
|
||||
showSnackBar(
|
||||
context,
|
||||
Text(_s.fileTooLarge(
|
||||
name.filename,
|
||||
size ?? 0,
|
||||
editorMaxSize,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
context.pop();
|
||||
|
||||
final remotePath = _getRemotePath(name);
|
||||
final localPath = await _getLocalPath(remotePath);
|
||||
final completer = Completer();
|
||||
final req = SftpReq(
|
||||
widget.spi,
|
||||
remotePath,
|
||||
localPath,
|
||||
SftpReqType.download,
|
||||
);
|
||||
_sftp.add(req, completer: completer);
|
||||
showRoundDialog(context: context, child: centerSizedLoading);
|
||||
await completer.future;
|
||||
context.pop();
|
||||
|
||||
final result = await AppRoute(
|
||||
EditorPage(path: localPath),
|
||||
'SFTP edit',
|
||||
).go<String>(context);
|
||||
if (result != null) {
|
||||
_sftp.add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload));
|
||||
}
|
||||
}
|
||||
|
||||
void _download(BuildContext context, SftpName name) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
final remotePath = _getRemotePath(name);
|
||||
|
||||
_sftp.add(
|
||||
SftpReq(
|
||||
widget.spi,
|
||||
remotePath,
|
||||
await _getLocalPath(remotePath),
|
||||
SftpReqType.download,
|
||||
),
|
||||
);
|
||||
|
||||
context.pop();
|
||||
},
|
||||
child: Text(_s.download),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _delete(BuildContext context, SftpName file) {
|
||||
context.pop();
|
||||
final isDir = file.attr.isDirectory;
|
||||
final dirText = isDir ? '\n${_s.sureDirEmpty}' : '';
|
||||
final text = '${_s.sureDelete(file.filename)}$dirText';
|
||||
final child = Text(text);
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: child,
|
||||
title: Text(_s.attention),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: centerSizedLoading,
|
||||
barrierDismiss: false,
|
||||
);
|
||||
final remotePath = _getRemotePath(file);
|
||||
try {
|
||||
if (file.attr.isDirectory) {
|
||||
await _status.client!.rmdir(remotePath);
|
||||
} else {
|
||||
await _status.client!.remove(remotePath);
|
||||
}
|
||||
context.pop();
|
||||
} catch (e) {
|
||||
context.pop();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _mkdir(BuildContext context) {
|
||||
context.pop();
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.createFolder),
|
||||
child: Input(
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: Text(_s.fieldMustNotEmpty),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
final dir = '${_status.path!.path}/${textController.text}';
|
||||
await _status.client!.mkdir(dir);
|
||||
context.pop();
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _newFile(BuildContext context) {
|
||||
context.pop();
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.createFile),
|
||||
child: Input(
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.fieldMustNotEmpty),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
final path = '${_status.path!.path}/${textController.text}';
|
||||
final file = await _status.client!.open(path);
|
||||
await file.writeBytes(Uint8List(0));
|
||||
context.pop();
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _rename(BuildContext context, SftpName file) {
|
||||
context.pop();
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.rename),
|
||||
child: Input(
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => context.pop(), child: Text(_s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.fieldMustNotEmpty),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _status.client!.rename(file.filename, textController.text);
|
||||
context.pop();
|
||||
_listDir();
|
||||
},
|
||||
child: Text(
|
||||
_s.rename,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getRemotePath(SftpName name) {
|
||||
final prePath = _status.path!.path;
|
||||
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;
|
||||
}
|
||||
_status.isBusy = true;
|
||||
if (client != null) {
|
||||
final sftpc = await client.sftp();
|
||||
_status.client = sftpc;
|
||||
}
|
||||
try {
|
||||
final fs =
|
||||
await _status.client!.listdir(path ?? _status.path?.path ?? '/');
|
||||
fs.sort((a, b) => a.filename.compareTo(b.filename));
|
||||
fs.removeAt(0);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_status.files = fs;
|
||||
_status.isBusy = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(_s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
await _backward();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backward() async {
|
||||
if (_status.path!.undo()) {
|
||||
await _listDir();
|
||||
}
|
||||
}
|
||||
}
|
||||
174
lib/view/page/storage/sftp_mission.dart
Normal file
174
lib/view/page/storage/sftp_mission.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/datetime.dart';
|
||||
import 'package:toolbox/core/extension/navigator.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/storage/local.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/sftp/req.dart';
|
||||
import '../../../data/provider/sftp.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/round_rect_card.dart';
|
||||
|
||||
class SftpMissionPage extends StatefulWidget {
|
||||
const SftpMissionPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SftpMissionPageState createState() => _SftpMissionPageState();
|
||||
}
|
||||
|
||||
class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
late S _s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_s = S.of(context)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_s.mission,
|
||||
style: textSize18,
|
||||
),
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<SftpProvider>(builder: (__, pro, _) {
|
||||
if (pro.status.isEmpty) {
|
||||
return Center(
|
||||
child: Text(_s.sftpNoDownloadTask),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(11),
|
||||
itemCount: pro.status.length,
|
||||
itemBuilder: (context, index) {
|
||||
final status = pro.status[index];
|
||||
return _buildItem(status);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildItem(SftpReqStatus status) {
|
||||
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: status,
|
||||
subtitle: str,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final idx = status.req.localPath.lastIndexOf('/');
|
||||
final dir = status.req.localPath.substring(0, idx);
|
||||
AppRoute(
|
||||
LocalStoragePage(initDir: dir),
|
||||
'sftp local',
|
||||
).go(context);
|
||||
},
|
||||
icon: const Icon(Icons.file_open)),
|
||||
IconButton(
|
||||
onPressed: () => shareFiles(context, [status.req.localPath]),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
case SftpWorkerStatus.downloading:
|
||||
final percentStr = (status.progress ?? 0.0).toStringAsFixed(2);
|
||||
final size = (status.size ?? 0).convertBytes;
|
||||
return _wrapInCard(
|
||||
status: status,
|
||||
subtitle: _s.downloadStatus(percentStr, size),
|
||||
trailing: _buildDelete(status.fileName, status.id),
|
||||
);
|
||||
case SftpWorkerStatus.preparing:
|
||||
return _wrapInCard(
|
||||
status: status,
|
||||
subtitle: _s.sftpDlPrepare,
|
||||
trailing: _buildDelete(status.fileName, status.id),
|
||||
);
|
||||
case SftpWorkerStatus.sshConnectted:
|
||||
return _wrapInCard(
|
||||
status: status,
|
||||
subtitle: _s.sftpSSHConnected,
|
||||
trailing: _buildDelete(status.fileName, status.id),
|
||||
);
|
||||
default:
|
||||
return _wrapInCard(
|
||||
status: status,
|
||||
subtitle: _s.unknown,
|
||||
trailing: IconButton(
|
||||
onPressed: () => showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.error),
|
||||
child: Text((status.error ?? _s.unknown).toString()),
|
||||
),
|
||||
icon: const Icon(Icons.error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _wrapInCard({
|
||||
required SftpReqStatus status,
|
||||
String? subtitle,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final time = DateTime.fromMicrosecondsSinceEpoch(status.id);
|
||||
return RoundRectCard(
|
||||
ListTile(
|
||||
leading: Text(time.hourMinute),
|
||||
title: Text(
|
||||
status.fileName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(
|
||||
subtitle,
|
||||
style: grey,
|
||||
),
|
||||
trailing: trailing,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelete(String name, int id) {
|
||||
return IconButton(
|
||||
onPressed: () => showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.attention),
|
||||
child: Text(_s.sureDelete(name)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
locator<SftpProvider>().cancel(id);
|
||||
context.pop();
|
||||
},
|
||||
child: Text(_s.ok),
|
||||
),
|
||||
]),
|
||||
icon: const Icon(Icons.delete),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user