diff --git a/lib/core/extension/datetime.dart b/lib/core/extension/datetime.dart new file mode 100644 index 00000000..0326c0e7 --- /dev/null +++ b/lib/core/extension/datetime.dart @@ -0,0 +1,5 @@ +extension DateTimeX on DateTime { + String get hourMinute { + return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/data/model/app/path_with_prefix.dart b/lib/data/model/app/path_with_prefix.dart index 1ef9db54..acab1deb 100644 --- a/lib/data/model/app/path_with_prefix.dart +++ b/lib/data/model/app/path_with_prefix.dart @@ -24,6 +24,8 @@ class PathWithPrefix { _path = pathJoin(_path, newPath); } + bool get canBack => path != '$_prefixPath/'; + bool undo() { if (_prePath == null || _path == _prePath) { return false; diff --git a/lib/data/model/sftp/worker.dart b/lib/data/model/sftp/worker.dart index bcbe15fa..68700ee0 100644 --- a/lib/data/model/sftp/worker.dart +++ b/lib/data/model/sftp/worker.dart @@ -94,7 +94,7 @@ Future _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); diff --git a/lib/data/provider/sftp.dart b/lib/data/provider/sftp.dart index 2a5b5d04..3c603681 100644 --- a/lib/data/provider/sftp.dart +++ b/lib/data/provider/sftp.dart @@ -8,21 +8,8 @@ class SftpProvider extends ProviderBase { final List _status = []; List get status => _status; - Iterable gets({int? id, String? fileName}) { - Iterable 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(); + } } diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 05cba87e..a94f866b 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -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 leading: const Icon(Icons.download), title: Text(_s.download), onTap: () => AppRoute( - const SFTPDownloadedPage(), + const LocalStoragePage(), 'sftp local page', ).go(context), ), diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index ed9ec3a5..628e1704 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -291,9 +291,8 @@ class _ServerEditPageState extends State 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) { diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index fa931275..0817b8df 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -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 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(); diff --git a/lib/view/page/sftp/mission.dart b/lib/view/page/sftp/mission.dart deleted file mode 100644 index 43e8d509..00000000 --- a/lib/view/page/sftp/mission.dart +++ /dev/null @@ -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 { - 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(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()); diff --git a/lib/view/page/ssh/term.dart b/lib/view/page/ssh/term.dart index 069a01d6..88d13393 100644 --- a/lib/view/page/ssh/term.dart +++ b/lib/view/page/ssh/term.dart @@ -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 { return; } AppRoute( - SFTPPage( + SftpPage( widget.spi, initPath: initPath, ), diff --git a/lib/view/page/sftp/local.dart b/lib/view/page/storage/local.dart similarity index 92% rename from lib/view/page/sftp/local.dart rename to lib/view/page/storage/local.dart index 8e474091..0a233a76 100644 --- a/lib/view/page/sftp/local.dart +++ b/lib/view/page/storage/local.dart @@ -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 createState() => _SFTPDownloadedPageState(); + State createState() => _LocalStoragePageState(); } -class _SFTPDownloadedPageState extends State { +class _LocalStoragePageState extends State { PathWithPrefix? _path; - String? _prefixPath; late S _s; @override @@ -44,7 +44,9 @@ class _SFTPDownloadedPageState extends State { 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 { 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 { } 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 { ? 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 { 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(context); if (remotePath == null) { - showSnackBar(context, Text(_s.fieldMustNotEmpty)); return; } locator().add(SftpReq( diff --git a/lib/view/page/sftp/remote.dart b/lib/view/page/storage/sftp.dart similarity index 88% rename from lib/view/page/sftp/remote.dart rename to lib/view/page/storage/sftp.dart index 8c7508fe..2e2fb0c3 100644 --- a/lib/view/page/sftp/remote.dart +++ b/lib/view/page/storage/sftp.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/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 { +class _SftpPageState extends State { final SftpBrowserStatus _status = SftpBrowserStatus(); final ScrollController _scrollController = ScrollController(); @@ -82,7 +82,7 @@ class _SFTPPageState extends State { IconButton( icon: const Icon(Icons.downloading), onPressed: () => AppRoute( - const SFTPDownloadingPage(), + const SftpMissionPage(), 'sftp downloading', ).go(context), ), @@ -154,7 +154,7 @@ class _SFTPPageState extends State { switch (idx) { case 0: return await AppRoute( - const SFTPDownloadedPage( + const LocalStoragePage( isPickFile: true, ), 'sftp dled pick') @@ -262,43 +262,49 @@ class _SFTPPageState extends State { _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 { child: Text(_s.cancel), ), TextButton( - onPressed: () { + onPressed: () async { if (textController.text == '') { showRoundDialog( context: context, @@ -493,8 +499,8 @@ class _SFTPPageState extends State { ); 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 { ); 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(); }, diff --git a/lib/view/page/storage/sftp_mission.dart b/lib/view/page/storage/sftp_mission.dart new file mode 100644 index 00000000..d9992d14 --- /dev/null +++ b/lib/view/page/storage/sftp_mission.dart @@ -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 { + 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(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().cancel(id); + context.pop(); + }, + child: Text(_s.ok), + ), + ]), + icon: const Icon(Icons.delete), + ); + } +}