New feat
- SFTP download - open downloaded files in other apps
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:toolbox/data/model/docker/ps.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/docker.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/center_loading.dart';
|
||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
@@ -25,6 +26,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
final _docker = locator<DockerProvider>();
|
||||
final greyTextStyle = const TextStyle(color: Colors.grey);
|
||||
late MediaQueryData _media;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -36,6 +38,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -46,7 +49,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
.firstWhere((element) => element.info == widget.spi)
|
||||
.client;
|
||||
if (client == null) {
|
||||
showSnackBar(context, const Text('No client found'));
|
||||
showSnackBar(context, Text(s.noClient));
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +90,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
padding: const EdgeInsets.all(7),
|
||||
children: [
|
||||
_buildVersion(
|
||||
docker.edition ?? 'Unknown', docker.version ?? 'Unknown'),
|
||||
docker.edition ?? s.unknown, docker.version ?? s.unknown),
|
||||
_buildPsItems(running, docker)
|
||||
].map((e) => RoundRectCard(e)).toList(),
|
||||
);
|
||||
@@ -97,14 +100,14 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
Widget _buildSolution(String err) {
|
||||
switch (err) {
|
||||
case 'docker not found':
|
||||
return const UrlText(
|
||||
text: 'Please https://docs.docker.com/engine/install docker first.',
|
||||
replace: 'install',
|
||||
return UrlText(
|
||||
text: s.installDockerWithUrl,
|
||||
replace: s.install,
|
||||
);
|
||||
case 'no client':
|
||||
return const Text('Plz wait for the connection to be established.');
|
||||
return Text(s.dockerWaitConnection);
|
||||
default:
|
||||
return const Text('Unknown error');
|
||||
return Text(s.unknownError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +123,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
|
||||
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
|
||||
return ExpansionTile(
|
||||
title: const Text('Container Status'),
|
||||
title: Text(s.containerStatus),
|
||||
subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
|
||||
children: running.map((item) {
|
||||
return ListTile(
|
||||
@@ -186,8 +189,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
final runningCount = running.where((element) => element.running).length;
|
||||
final stoped = running.length - runningCount;
|
||||
if (stoped == 0) {
|
||||
return '$runningCount container running.';
|
||||
return s.dockerStatusRunningFmt(runningCount);
|
||||
}
|
||||
return '$runningCount running, $stoped stoped.';
|
||||
return s.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:toolbox/view/page/ping.dart';
|
||||
import 'package:toolbox/view/page/private_key/list.dart';
|
||||
import 'package:toolbox/view/page/server/tab.dart';
|
||||
import 'package:toolbox/view/page/setting.dart';
|
||||
import 'package:toolbox/view/page/sftp/downloaded.dart';
|
||||
import 'package:toolbox/view/page/snippet/list.dart';
|
||||
import 'package:toolbox/view/widget/url_text.dart';
|
||||
|
||||
@@ -233,6 +234,13 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
const StoredPrivateKeysPage(), 'private key list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(s.download),
|
||||
onTap: () =>
|
||||
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.snippet_folder),
|
||||
title: Text(s.snippet),
|
||||
|
||||
@@ -21,7 +21,7 @@ import 'package:toolbox/view/page/apt.dart';
|
||||
import 'package:toolbox/view/page/docker.dart';
|
||||
import 'package:toolbox/view/page/server/detail.dart';
|
||||
import 'package:toolbox/view/page/server/edit.dart';
|
||||
import 'package:toolbox/view/page/sftp.dart';
|
||||
import 'package:toolbox/view/page/sftp/view.dart';
|
||||
import 'package:toolbox/view/page/snippet/list.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
|
||||
187
lib/view/page/sftp/downloaded.dart
Normal file
187
lib/view/page/sftp/downloaded.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_plus/share_plus.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';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/app/path_with_prefix.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/view/page/sftp/downloading.dart';
|
||||
import 'package:toolbox/view/widget/fade_in.dart';
|
||||
|
||||
class SFTPDownloadedPage extends StatefulWidget {
|
||||
const SFTPDownloadedPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SFTPDownloadedPage> createState() => _SFTPDownloadedPageState();
|
||||
}
|
||||
|
||||
class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
|
||||
PathWithPrefix? _path;
|
||||
String? _prefixPath;
|
||||
late S s;
|
||||
late ThemeData _theme;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sftpDownloadDir.then((dir) {
|
||||
_path = PathWithPrefix(dir.path);
|
||||
_prefixPath = dir.path + '/';
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
s = S.of(context);
|
||||
_theme = Theme.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 SFTPDownloadingPage(), 'sftp downloading')
|
||||
.go(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: FadeIn(
|
||||
child: _buildBody(),
|
||||
key: UniqueKey(),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: _buildPath(),
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: (() {
|
||||
if (_path!.path == _prefixPath) {
|
||||
showSnackBar(context, Text(s.alreadyLastDir));
|
||||
return;
|
||||
}
|
||||
_path!.update('..');
|
||||
setState(() {});
|
||||
}),
|
||||
child: const Icon(Icons.keyboard_arrow_left),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPath() {
|
||||
return Container(
|
||||
color: _theme.appBarTheme.foregroundColor,
|
||||
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Divider(),
|
||||
(_path?.path ?? s.loadingFiles).omitStartStr(
|
||||
style: TextStyle(
|
||||
color:
|
||||
primaryColor.isBrightColor ? Colors.black : Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_path == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
final dir = Directory(_path!.path);
|
||||
final files = dir.listSync();
|
||||
return ListView.builder(
|
||||
itemCount: files.length,
|
||||
itemBuilder: (context, index) {
|
||||
var file = files[index];
|
||||
var fileName = file.path.split('/').last;
|
||||
var stat = file.statSync();
|
||||
var isDir = stat.type == FileSystemEntityType.directory;
|
||||
|
||||
return ListTile(
|
||||
leading: isDir
|
||||
? const Icon(Icons.folder)
|
||||
: const Icon(Icons.insert_drive_file),
|
||||
title: Text(fileName),
|
||||
subtitle: isDir ? null : Text(stat.size.convertBytes),
|
||||
trailing: Text(
|
||||
stat.modified
|
||||
.toString()
|
||||
.substring(0, stat.modified.toString().length - 4),
|
||||
style: grey,
|
||||
),
|
||||
onTap: () {
|
||||
if (!isDir) {
|
||||
showFileActionDialog(file);
|
||||
return;
|
||||
}
|
||||
_path!.update(fileName);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showFileActionDialog(FileSystemEntity file) {
|
||||
final fileName = file.path.split('/').last;
|
||||
showRoundDialog(
|
||||
context,
|
||||
s.choose,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(s.delete),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showRoundDialog(
|
||||
context, s.sureDelete(fileName), const SizedBox(), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
file.deleteSync();
|
||||
setState(() {});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(s.ok),
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(s.open),
|
||||
onTap: () {
|
||||
Share.shareFiles([file.absolute.path],
|
||||
text: '$fileName from ServerBox');
|
||||
}),
|
||||
],
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: (() => Navigator.of(context).pop()),
|
||||
child: Text(s.close))
|
||||
]);
|
||||
}
|
||||
}
|
||||
105
lib/view/page/sftp/downloading.dart
Normal file
105
lib/view/page/sftp/downloading.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/sftp/download_status.dart';
|
||||
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/view/widget/center_loading.dart';
|
||||
import 'package:toolbox/view/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.download,
|
||||
style: size18,
|
||||
),
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<SftpDownloadProvider>(builder: (__, pro, _) {
|
||||
if (pro.status.isEmpty) {
|
||||
return Center(
|
||||
child: Text(s.sftpNoDownloadTask),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
itemCount: pro.status.length,
|
||||
itemBuilder: (context, index) {
|
||||
final status = pro.status[index];
|
||||
return _buildItem(status);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _wrapInCard(SftpDownloadStatus status, String? subtitle,
|
||||
{Widget? trailing}) {
|
||||
return RoundRectCard(ListTile(
|
||||
title: Text(status.fileName),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(
|
||||
subtitle,
|
||||
style: grey,
|
||||
),
|
||||
trailing: trailing,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildItem(SftpDownloadStatus status) {
|
||||
if (status.error != null) {
|
||||
showSnackBar(context, Text(status.error.toString()));
|
||||
}
|
||||
switch (status.status) {
|
||||
case SftpWorkerStatus.finished:
|
||||
return _wrapInCard(status,
|
||||
'${s.downloadFinished}, ${s.spentTime(status.spentTime ?? s.unknown)}',
|
||||
trailing: IconButton(
|
||||
onPressed: () => Share.shareFiles([status.item.localPath],
|
||||
text: '${status.fileName} from ServerBox'),
|
||||
icon: const Icon(Icons.open_in_new)));
|
||||
case SftpWorkerStatus.downloading:
|
||||
return _wrapInCard(
|
||||
status,
|
||||
s.downloadStatus((status.progress ?? 0.0).toStringAsFixed(2),
|
||||
(status.size ?? 0).convertBytes),
|
||||
trailing:
|
||||
CircularProgressIndicator(value: (status.progress ?? 0) / 100));
|
||||
case SftpWorkerStatus.preparing:
|
||||
return _wrapInCard(status, s.sftpDlPrepare, trailing: centerLoading);
|
||||
case SftpWorkerStatus.sshConnectted:
|
||||
return _wrapInCard(status, s.sftpSSHConnected, trailing: centerLoading);
|
||||
default:
|
||||
return _wrapInCard(status, s.unknown,
|
||||
trailing: const Icon(
|
||||
Icons.error,
|
||||
size: 40,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/sftp/absolute_path.dart';
|
||||
import 'package:toolbox/data/model/sftp/sftp_side_status.dart';
|
||||
import 'package:toolbox/data/model/sftp/download_worker.dart';
|
||||
import 'package:toolbox/data/model/sftp/browser_status.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/sftp_download.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/sftp/downloading.dart';
|
||||
import 'package:toolbox/view/widget/fade_in.dart';
|
||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
|
||||
@@ -20,16 +28,18 @@ class SFTPPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SFTPPageState extends State<SFTPPage> {
|
||||
final SFTPSideViewStatus _status = SFTPSideViewStatus();
|
||||
final SftpBrowserStatus _status = SftpBrowserStatus();
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
late MediaQueryData _media;
|
||||
late S s;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
s = S.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -133,94 +143,66 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: const Text('Delete'),
|
||||
title: Text(s.delete),
|
||||
onTap: () => delete(context, file),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: const Text('Create Folder'),
|
||||
title: Text(s.createFolder),
|
||||
onTap: () => mkdir(context)),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: const Text('Rename'),
|
||||
title: Text(s.rename),
|
||||
onTap: () => rename(context, file),
|
||||
),
|
||||
// ListTile(
|
||||
// leading: const Icon(Icons.download),
|
||||
// title: const Text('Download'),
|
||||
// onTap: () => download(context, file),
|
||||
// )
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
title: Text(s.download),
|
||||
onTap: () => download(context, file),
|
||||
)
|
||||
],
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'))
|
||||
child: Text(s.cancel))
|
||||
]);
|
||||
}
|
||||
|
||||
// void download(BuildContext context, SftpName name) {
|
||||
// showRoundDialog(
|
||||
// context, 'Download', Text('Download ${name.filename} to local?'), [
|
||||
// TextButton(
|
||||
// onPressed: () => Navigator.of(context).pop(),
|
||||
// child: const Text('Cancel')),
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// var result = '';
|
||||
// try {
|
||||
// Navigator.of(context).pop();
|
||||
// showRoundDialog(
|
||||
// context,
|
||||
// name.filename,
|
||||
// const Text('Downloading...\nKepp this app in the foreground.',
|
||||
// textAlign: TextAlign.center),
|
||||
// [],
|
||||
// barrierDismiss: false);
|
||||
// final path = await sftpDownloadDir;
|
||||
// final local = File('${path.path}/${name.filename}');
|
||||
// if (await local.exists()) {
|
||||
// await local.delete();
|
||||
// }
|
||||
|
||||
// final localFile =
|
||||
// await local.open(mode: FileMode.writeOnlyAppend);
|
||||
// final remotePath = _status.path!.path + '/' + name.filename;
|
||||
// final file = await _status.client!.open(remotePath);
|
||||
// final size = (await file.stat()).size;
|
||||
// if (size == null) {
|
||||
// throw Exception('can not get file size');
|
||||
// }
|
||||
|
||||
// const chunkSize = 1024 * 128;
|
||||
// for (var i = 0; i < size; i += chunkSize) {
|
||||
// final data = file.read(length: chunkSize);
|
||||
// await for (var item in data) {
|
||||
// localFile.writeFrom(item);
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// result = e.toString();
|
||||
// } finally {
|
||||
// Navigator.of(context).pop();
|
||||
// if (result.isEmpty) {
|
||||
// result = 'Donwloaded successfully.';
|
||||
// }
|
||||
// showRoundDialog(context, 'Result', Text(result), [
|
||||
// TextButton(
|
||||
// onPressed: () => Navigator.of(context).pop(),
|
||||
// child: const Text('OK'))
|
||||
// ]);
|
||||
// }
|
||||
// },
|
||||
// child: const Text('Download'))
|
||||
// ]);
|
||||
// }
|
||||
void download(BuildContext context, SftpName name) {
|
||||
showRoundDialog(
|
||||
context, s.download, Text(s.dl2Local(name.filename)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
final prePath = _status.path!.path;
|
||||
final remotePath =
|
||||
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
|
||||
final local = '${(await sftpDownloadDir).path}$remotePath';
|
||||
final pubKeyId = _status.spi!.pubKeyId;
|
||||
locator<SftpDownloadProvider>().add(
|
||||
DownloadItem(_status.spi!, remotePath, local),
|
||||
key: pubKeyId == null
|
||||
? null
|
||||
: locator<PrivateKeyStore>().get(pubKeyId).privateKey);
|
||||
showRoundDialog(context, s.goSftpDlPage, const SizedBox(), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(onPressed: () => AppRoute(const SFTPDownloadingPage(), 'sftp downloading'), child: Text(s.ok))
|
||||
]);
|
||||
},
|
||||
child: Text(s.download))
|
||||
]);
|
||||
}
|
||||
|
||||
void delete(BuildContext context, SftpName file) {
|
||||
Navigator.of(context).pop();
|
||||
showRoundDialog(
|
||||
context, 'Confirm', Text('Are you sure to delete ${file.filename}?'), [
|
||||
context, s.attention, Text(s.sureDelete(file.filename)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
@@ -230,9 +212,9 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
child: Text(
|
||||
s.delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
@@ -242,25 +224,25 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
'Create Folder',
|
||||
s.createFolder,
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Folder Name',
|
||||
decoration: InputDecoration(
|
||||
labelText: s.name,
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(context, 'Attention',
|
||||
const Text('You need input a name.'), [
|
||||
showRoundDialog(context, s.attention,
|
||||
Text(s.fieldMustNotEmpty), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK')),
|
||||
child: Text(s.ok)),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -269,9 +251,9 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: const Text(
|
||||
'Create',
|
||||
style: TextStyle(color: Colors.red),
|
||||
child: Text(
|
||||
s.ok,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
@@ -281,25 +263,25 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
'Rename',
|
||||
s.rename,
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'New Name',
|
||||
decoration: InputDecoration(
|
||||
labelText: s.name,
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(context, 'Attention',
|
||||
const Text('You need input a name.'), [
|
||||
showRoundDialog(context, s.attention,
|
||||
Text(s.fieldMustNotEmpty), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK')),
|
||||
child: Text(s.ok)),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -308,9 +290,9 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
Navigator.of(context).pop();
|
||||
listDir();
|
||||
},
|
||||
child: const Text(
|
||||
'Rename',
|
||||
style: TextStyle(color: Colors.red),
|
||||
child: Text(
|
||||
s.rename,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
@@ -336,10 +318,10 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
await showRoundDialog(context, 'Error', Text(e.toString()), [
|
||||
await showRoundDialog(context, s.error, Text(e.toString()), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'))
|
||||
child: Text(s.ok))
|
||||
]);
|
||||
if (_status.path!.undo()) {
|
||||
await listDir();
|
||||
@@ -350,43 +332,9 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
Widget _buildDestSelector() {
|
||||
final str = _status.path?.path;
|
||||
return ExpansionTile(
|
||||
title: Text(_status.spi?.name ?? 'Choose target'),
|
||||
title: Text(_status.spi?.name ?? s.chooseDestination),
|
||||
subtitle: _status.selected
|
||||
? LayoutBuilder(builder: (context, size) {
|
||||
bool exceeded = false;
|
||||
int len = 0;
|
||||
for (; !exceeded && len < str!.length; len++) {
|
||||
// Build the textspan
|
||||
var span = TextSpan(
|
||||
text: '...' + str.substring(str.length - len),
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.bodyText1?.fontSize ??
|
||||
14),
|
||||
);
|
||||
|
||||
// Use a textpainter to determine if it will exceed max lines
|
||||
var tp = TextPainter(
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.left,
|
||||
textDirection: TextDirection.ltr,
|
||||
text: span,
|
||||
);
|
||||
|
||||
// trigger it to layout
|
||||
tp.layout(maxWidth: size.maxWidth);
|
||||
|
||||
// whether the text overflowed or not
|
||||
exceeded = tp.didExceedMaxLines;
|
||||
}
|
||||
|
||||
return Text(
|
||||
(exceeded ? '...' : '') + str!.substring(str.length - len),
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
);
|
||||
})
|
||||
? str!.omitStartStr(style: const TextStyle(color: Colors.grey))
|
||||
: null,
|
||||
children: locator<ServerProvider>()
|
||||
.servers
|
||||
@@ -29,8 +29,8 @@ class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user