opt.: cancel sftp mission

This commit is contained in:
lollipopkit
2023-07-29 18:24:22 +08:00
parent e13c5910ec
commit 0f83d10bfa
12 changed files with 277 additions and 230 deletions

View File

@@ -0,0 +1,5 @@
extension DateTimeX on DateTime {
String get hourMinute {
return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
}
}

View File

@@ -24,6 +24,8 @@ class PathWithPrefix {
_path = pathJoin(_path, newPath);
}
bool get canBack => path != '$_prefixPath/';
bool undo() {
if (_prePath == null || _path == _prePath) {
return false;

View File

@@ -94,7 +94,7 @@ Future<void> _download(
return;
}
// Read 10m each time
const defaultChunkSize = 1024 * 1024 * 10;
const defaultChunkSize = 1024 * 1024;
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
mainSendPort.send(size);
mainSendPort.send(SftpWorkerStatus.downloading);

View File

@@ -8,21 +8,8 @@ class SftpProvider extends ProviderBase {
final List<SftpReqStatus> _status = [];
List<SftpReqStatus> get status => _status;
Iterable<SftpReqStatus> gets({int? id, String? fileName}) {
Iterable<SftpReqStatus> found = [];
if (id != null) {
found = _status.where((e) => e.id == id);
}
if (fileName != null) {
found = found.where((e) => e.req.localPath.split('/').last == fileName);
}
return found;
}
SftpReqStatus? get({int? id, String? name}) {
final found = gets(id: id, fileName: name);
if (found.isEmpty) return null;
return found.first;
SftpReqStatus? get(int id) {
return _status.singleWhere((element) => element.id == id);
}
void add(SftpReq req, {Completer? completer}) {
@@ -32,4 +19,19 @@ class SftpProvider extends ProviderBase {
req: req,
));
}
@override
void dispose() {
for (final item in _status) {
item.worker.dispose();
}
super.dispose();
}
void cancel(int id) {
final idx = _status.indexWhere((element) => element.id == id);
_status[idx].worker.dispose();
_status.removeAt(idx);
notifyListeners();
}
}

View File

@@ -25,7 +25,7 @@ import 'convert.dart';
import 'debug.dart';
import 'private_key/list.dart';
import 'setting.dart';
import 'sftp/local.dart';
import 'storage/local.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@@ -219,7 +219,7 @@ class _HomePageState extends State<HomePage>
leading: const Icon(Icons.download),
title: Text(_s.download),
onTap: () => AppRoute(
const SFTPDownloadedPage(),
const LocalStoragePage(),
'sftp local page',
).go(context),
),

View File

@@ -291,9 +291,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
pwd: authorization,
pubKeyId: usePublicKey ? _keyInfo!.id : null,
tags: _tags,
alterUrl: _alterUrlController.text == ''
? null
: _alterUrlController.text,
alterUrl:
_alterUrlController.text == '' ? null : _alterUrlController.text,
);
if (widget.spi == null) {

View File

@@ -29,7 +29,7 @@ import '../../widget/popup_menu.dart';
import '../../widget/round_rect_card.dart';
import '../docker.dart';
import '../pkg.dart';
import '../sftp/remote.dart';
import '../storage/sftp.dart';
import '../ssh/term.dart';
import 'detail.dart';
import 'edit.dart';
@@ -286,7 +286,7 @@ class _ServerPageState extends State<ServerPage>
AppRoute(PkgManagePage(spi), 'pkg manage').go(context);
break;
case ServerTabMenuType.sftp:
AppRoute(SFTPPage(spi), 'SFTP').go(context);
AppRoute(SftpPage(spi), 'SFTP').go(context);
break;
case ServerTabMenuType.snippet:
final provider = locator<SnippetProvider>();

View File

@@ -1,141 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.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 SFTPDownloadingPage extends StatefulWidget {
const SFTPDownloadingPage({Key? key}) : super(key: key);
@override
_SFTPDownloadingPageState createState() => _SFTPDownloadingPageState();
}
class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
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 _wrapInCard(SftpReqStatus status, String? subtitle,
{Widget? trailing}) {
return RoundRectCard(
ListTile(
title: Text(
status.fileName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
subtitle: subtitle == null
? null
: Text(
subtitle,
style: grey,
),
trailing: trailing,
),
);
}
Widget _buildItem(SftpReqStatus status) {
if (status.error != null) {
final err = status.error.toString();
Future.delayed(
const Duration(milliseconds: 377),
() => showSnackBar(context, Text(err)),
);
status.error = null;
}
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,
str,
trailing: 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 percent = (status.progress ?? 0) / 100;
final size = (status.size ?? 0).convertBytes;
return _wrapInCard(
status,
_s.downloadStatus(percentStr, size),
trailing: SizedBox(
height: 27,
width: 27,
child: CircularProgressIndicator(
value: percent,
),
),
);
case SftpWorkerStatus.preparing:
return _wrapInCard(
status,
_s.sftpDlPrepare,
trailing: _loading,
);
case SftpWorkerStatus.sshConnectted:
return _wrapInCard(
status,
_s.sftpSSHConnected,
trailing: _loading,
);
default:
return _wrapInCard(
status,
_s.unknown,
trailing: const Icon(Icons.error),
);
}
}
}
const _loading =
SizedBox(height: 27, width: 27, child: CircularProgressIndicator());

View File

@@ -23,7 +23,7 @@ import '../../../data/res/color.dart';
import '../../../data/res/terminal.dart';
import '../../../data/store/setting.dart';
import '../../../locator.dart';
import '../sftp/remote.dart';
import '../storage/sftp.dart';
class SSHPage extends StatefulWidget {
final ServerPrivateInfo spi;
@@ -260,7 +260,7 @@ class _SSHPageState extends State<SSHPage> {
return;
}
AppRoute(
SFTPPage(
SftpPage(
widget.spi,
initPath: initPath,
),

View File

@@ -9,7 +9,7 @@ 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/sftp/remote.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';
@@ -23,20 +23,20 @@ import '../../../data/model/app/path_with_prefix.dart';
import '../../../data/res/path.dart';
import '../../../data/res/ui.dart';
import '../../widget/fade_in.dart';
import 'mission.dart';
import 'sftp_mission.dart';
class SFTPDownloadedPage extends StatefulWidget {
class LocalStoragePage extends StatefulWidget {
final bool isPickFile;
const SFTPDownloadedPage({Key? key, this.isPickFile = false})
final String? initDir;
const LocalStoragePage({Key? key, this.isPickFile = false, this.initDir})
: super(key: key);
@override
State<SFTPDownloadedPage> createState() => _SFTPDownloadedPageState();
State<LocalStoragePage> createState() => _LocalStoragePageState();
}
class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
class _LocalStoragePageState extends State<LocalStoragePage> {
PathWithPrefix? _path;
String? _prefixPath;
late S _s;
@override
@@ -44,7 +44,9 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
super.initState();
sftpDir.then((dir) {
_path = PathWithPrefix(dir.path);
_prefixPath = '${dir.path}/';
if (widget.initDir != null) {
_path!.update(widget.initDir!.replaceFirst('${dir.path}/', ''));
}
setState(() {});
});
}
@@ -64,7 +66,7 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
IconButton(
icon: const Icon(Icons.downloading),
onPressed: () =>
AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
AppRoute(const SftpMissionPage(), 'sftp downloading')
.go(context),
)
],
@@ -100,7 +102,7 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
}
final dir = Directory(_path!.path);
final files = dir.listSync();
final canGoBack = _path!.path != _prefixPath;
final canGoBack = _path!.canBack;
return ListView.builder(
itemCount: canGoBack ? files.length + 1 : files.length,
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
@@ -126,7 +128,7 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
? const Icon(Icons.folder)
: const Icon(Icons.insert_drive_file),
title: Text(fileName),
subtitle: isDir ? null : Text(stat.size.convertBytes),
subtitle: isDir ? null : Text(stat.size.convertBytes, style: grey),
trailing: Text(
stat.modified
.toString()
@@ -266,18 +268,16 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
final id = ids[idx];
final spi = serverProvider.servers[id]?.spi;
if (spi == null) {
showSnackBar(context, Text(_s.noResult));
return;
}
final remotePath = await AppRoute(
SFTPPage(
SftpPage(
spi,
selectPath: true,
),
'SFTP page (select)',
).go<String>(context);
if (remotePath == null) {
showSnackBar(context, Text(_s.fieldMustNotEmpty));
return;
}
locator<SftpProvider>().add(SftpReq(

View File

@@ -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/local.dart';
import 'package:toolbox/view/page/storage/local.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import '../../../core/extension/numx.dart';
@@ -29,14 +29,14 @@ import '../../../locator.dart';
import '../../widget/fade_in.dart';
import '../../widget/input_field.dart';
import '../../widget/two_line_text.dart';
import 'mission.dart';
import 'sftp_mission.dart';
class SFTPPage extends StatefulWidget {
class SftpPage extends StatefulWidget {
final ServerPrivateInfo spi;
final String? initPath;
final bool selectPath;
const SFTPPage(
const SftpPage(
this.spi, {
Key? key,
this.initPath,
@@ -44,10 +44,10 @@ class SFTPPage extends StatefulWidget {
}) : super(key: key);
@override
_SFTPPageState createState() => _SFTPPageState();
_SftpPageState createState() => _SftpPageState();
}
class _SFTPPageState extends State<SFTPPage> {
class _SftpPageState extends State<SftpPage> {
final SftpBrowserStatus _status = SftpBrowserStatus();
final ScrollController _scrollController = ScrollController();
@@ -82,7 +82,7 @@ class _SFTPPageState extends State<SFTPPage> {
IconButton(
icon: const Icon(Icons.downloading),
onPressed: () => AppRoute(
const SFTPDownloadingPage(),
const SftpMissionPage(),
'sftp downloading',
).go(context),
),
@@ -154,7 +154,7 @@ class _SFTPPageState extends State<SFTPPage> {
switch (idx) {
case 0:
return await AppRoute(
const SFTPDownloadedPage(
const LocalStoragePage(
isPickFile: true,
),
'sftp dled pick')
@@ -262,43 +262,49 @@ class _SFTPPageState extends State<SFTPPage> {
_status.path = AbsolutePath(p_);
_listDir(path: p_, client: _client);
return centerLoading;
} else {
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: (context, index) {
final file = _status.files![index];
final isDir = file.attr.isDirectory;
return RoundRectCard(ListTile(
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename),
trailing: Text(
'${getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
style: const TextStyle(color: Colors.grey),
textAlign: TextAlign.right,
),
subtitle:
isDir ? null : Text((file.attr.size ?? 0).convertBytes),
onTap: () {
if (isDir) {
_status.path?.update(file.filename);
_listDir(path: _status.path?.path);
} else {
_onItemPress(context, file, true);
}
},
onLongPress: () => _onItemPress(context, file, !isDir),
));
},
),
),
onRefresh: () => _listDir(path: _status.path?.path),
);
}
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) {
@@ -479,7 +485,7 @@ class _SFTPPageState extends State<SFTPPage> {
child: Text(_s.cancel),
),
TextButton(
onPressed: () {
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context: context,
@@ -493,8 +499,8 @@ class _SFTPPageState extends State<SFTPPage> {
);
return;
}
_status.client!
.mkdir('${_status.path!.path}/${textController.text}');
final dir = '${_status.path!.path}/${textController.text}';
await _status.client!.mkdir(dir);
context.pop();
_listDir();
},
@@ -538,9 +544,9 @@ class _SFTPPageState extends State<SFTPPage> {
);
return;
}
(await _status.client!
.open('${_status.path!.path}/${textController.text}'))
.writeBytes(Uint8List(0));
final path = '${_status.path!.path}/${textController.text}';
final file = await _status.client!.open(path);
await file.writeBytes(Uint8List(0));
context.pop();
_listDir();
},

View 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),
);
}
}