This commit is contained in:
Junyuan Feng
2022-02-10 20:27:30 +08:00
parent feb3b10f1f
commit 282e61afac
9 changed files with 406 additions and 27 deletions

View File

@@ -354,7 +354,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 96; CURRENT_PROJECT_VERSION = 97;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -362,7 +362,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.96; MARKETING_VERSION = 1.0.97;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -484,7 +484,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 96; CURRENT_PROJECT_VERSION = 97;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -492,7 +492,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.96; MARKETING_VERSION = 1.0.97;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -508,7 +508,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 96; CURRENT_PROJECT_VERSION = 97;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -516,7 +516,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.96; MARKETING_VERSION = 1.0.97;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -191,8 +191,9 @@ class ServerProvider extends BusyProvider {
} catch (e) { } catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].connectionState = ServerConnectionState.failed;
servers[idx].status.failedInfo = e.toString(); servers[idx].status.failedInfo = e.toString();
notifyListeners();
logger.warning(e); logger.warning(e);
} finally {
notifyListeners();
} }
} }
@@ -219,7 +220,6 @@ class ServerProvider extends BusyProvider {
results.add(NetSpeedPart(device, bytesIn, bytesOut, time)); results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
} }
info.status.netSpeed = info.status.netSpeed.update(results); info.status.netSpeed = info.status.netSpeed.update(results);
notifyListeners();
} }
void _getSysVer(ServerPrivateInfo spi, String raw) { void _getSysVer(ServerPrivateInfo spi, String raw) {
@@ -228,8 +228,6 @@ class ServerProvider extends BusyProvider {
if (s.length == 2) { if (s.length == 2) {
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', ''); info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
} }
notifyListeners();
} }
String _getCPUTemp(String raw) { String _getCPUTemp(String raw) {
@@ -264,14 +262,11 @@ class ServerProvider extends BusyProvider {
info.status.cpu2Status = info.status.cpu2Status =
info.status.cpu2Status.update(cpus, _getCPUTemp(temp)); info.status.cpu2Status.update(cpus, _getCPUTemp(temp));
} }
notifyListeners();
} }
void _getUpTime(ServerPrivateInfo spi, String raw) { void _getUpTime(ServerPrivateInfo spi, String raw) {
_servers.firstWhere((e) => e.info == spi).status.uptime = _servers.firstWhere((e) => e.info == spi).status.uptime =
raw.split('up ')[1].split(', ')[0]; raw.split('up ')[1].split(', ')[0];
notifyListeners();
} }
void _getTcp(ServerPrivateInfo spi, String raw) { void _getTcp(ServerPrivateInfo spi, String raw) {
@@ -283,7 +278,6 @@ class ServerProvider extends BusyProvider {
final vals = idx.split(RegExp(r'\s{1,}')); final vals = idx.split(RegExp(r'\s{1,}'));
info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i); info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
} }
notifyListeners();
} }
void _getDisk(ServerPrivateInfo spi, String raw) { void _getDisk(ServerPrivateInfo spi, String raw) {
@@ -299,7 +293,6 @@ class ServerProvider extends BusyProvider {
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3])); int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
} }
info.status.disk = list; info.status.disk = list;
notifyListeners();
} }
void _getMem(ServerPrivateInfo spi, String raw) { void _getMem(ServerPrivateInfo spi, String raw) {
@@ -318,7 +311,6 @@ class ServerProvider extends BusyProvider {
avail: memList[5]); avail: memList[5]);
} }
} }
notifyListeners();
} }
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async { Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {

View File

@@ -4,7 +4,7 @@ class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 97; static const int build = 97;
static const String engine = static const String engine =
"Flutter 2.10.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 5f105a6ca7 (7 days ago) • 2022-02-01 14:15:42 -0800\nEngine • revision 776efd2034\nTools • Dart 2.16.0 • DevTools 2.9.2\n"; "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (8 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
static const String buildAt = "2022-02-09 10:58:45.586008"; static const String buildAt = "2022-02-10 19:30:23.388434";
static const int modifications = 8; static const int modifications = 9;
} }

View File

@@ -101,7 +101,6 @@ class _MyHomePageState extends State<MyHomePage>
animationDuration: const Duration(milliseconds: 300), animationDuration: const Duration(milliseconds: 300),
animateChildDecoration: true, animateChildDecoration: true,
rtlOpening: false, rtlOpening: false,
disabledGestures: true,
childDecoration: const BoxDecoration( childDecoration: const BoxDecoration(
// NOTICE: Uncomment if you want to add shadow behind the page. // NOTICE: Uncomment if you want to add shadow behind the page.
// Keep in mind that it may cause animation jerks. // Keep in mind that it may cause animation jerks.

View File

@@ -49,7 +49,8 @@ class _PingPageState extends State<PingPage>
RoundRectCard( RoundRectCard(
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: Padding(padding: const EdgeInsets.all(7), child: Text(_result)), child: Padding(
padding: const EdgeInsets.all(7), child: Text(_result)),
), ),
), ),
])), ])),

View File

@@ -19,6 +19,7 @@ import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/server/detail.dart'; import 'package:toolbox/view/page/server/detail.dart';
import 'package:toolbox/view/page/server/edit.dart'; import 'package:toolbox/view/page/server/edit.dart';
import 'package:toolbox/view/page/sftp.dart';
import 'package:toolbox/view/page/snippet/list.dart'; import 'package:toolbox/view/page/snippet/list.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
@@ -241,9 +242,16 @@ class _ServerPageState extends State<ServerPage>
switch (item) { switch (item) {
case MenuItems.ssh: case MenuItems.ssh:
case MenuItems.apt: case MenuItems.apt:
case MenuItems.sftp:
showSnackBar(context, const Text('Now is not supported')); showSnackBar(context, const Text('Now is not supported'));
break; break;
case MenuItems.sftp:
AppRoute(
SFTPPage(
spi: spi,
),
'SFTP')
.go(context);
break;
case MenuItems.snippet: case MenuItems.snippet:
AppRoute( AppRoute(
SnippetListPage( SnippetListPage(

337
lib/view/page/sftp.dart Normal file
View File

@@ -0,0 +1,337 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.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/provider/server.dart';
import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/fade_in.dart';
class SFTPPage extends StatefulWidget {
final ServerPrivateInfo? spi;
const SFTPPage({this.spi, Key? key}) : super(key: key);
@override
_SFTPPageState createState() => _SFTPPageState();
}
class _SFTPPageState extends State<SFTPPage> {
/// Whether the Left/Right Destination is selected.
final List<bool> _selectedDest = List<bool>.filled(2, false);
final List<ServerPrivateInfo?> _destSpi =
List<ServerPrivateInfo?>.filled(2, null);
final List<List<SftpName>?> _files = List<List<SftpName>?>.filled(2, null);
final List<String> _paths = List<String>.filled(2, '');
final List<SftpClient?> _clients = List<SftpClient?>.filled(2, null);
final ScrollController _leftScrollController = ScrollController();
final ScrollController _rightScrollController = ScrollController();
late MediaQueryData _media;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
}
@override
void initState() {
super.initState();
if (widget.spi != null) {
_destSpi[0] = widget.spi;
_selectedDest[0] = true;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(_titleText),
),
body: Row(
children: [
_buildSingleColumn(true),
const VerticalDivider(
width: 2,
),
_buildSingleColumn(false),
],
),
);
}
String get _titleText {
List<String> titles = [
'',
'',
];
if (_selectedDest[0]) {
titles[0] = _destSpi[0]?.name ?? '';
}
if (_selectedDest[1]) {
titles[1] = _destSpi[1]?.name ?? '';
}
return titles[0] == '' || titles[1] == '' ? 'SFTP' : titles.join(' - ');
}
Widget _buildSingleColumn(bool left) {
Widget child;
if (!_selectedDest[left ? 0 : 1]) {
child = _buildDestSelector(left);
} else {
child = _buildFileView(left);
}
return SizedBox(
width: (_media.size.width - 2) / 2,
child: child,
);
}
Widget get centerCircleLoading => Center(
child: Column(
children: [
SizedBox(
height: _media.size.height * 0.4,
),
const CircularProgressIndicator(),
],
),
);
Widget _buildFileView(bool left) {
final spi = _destSpi[left ? 0 : 1];
final si =
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
final client = si.client;
if (client == null ||
si.connectionState != ServerConnectionState.connected) {
return centerCircleLoading;
}
if (_files[left ? 0 : 1] == null) {
updatePath('/', left);
listDir(client, '/', left);
return centerCircleLoading;
} else {
return RefreshIndicator(
child: FadeIn(
child: ListView.builder(
itemCount: _files[left ? 0 : 1]!.length,
controller: left ? _leftScrollController : _rightScrollController,
itemBuilder: (context, index) {
final file = _files[left ? 0 : 1]![index];
final isDir = file.attr.mode?.isDirectory ?? true;
return ListTile(
leading:
Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename),
subtitle: isDir
? null
: Text((convertBytes(file.attr.size ?? 0))),
onTap: () {
if (isDir) {
updatePath(file.filename, left);
listDir(client, _paths[left ? 0 : 1], left);
} else {
// downloadFile(client, file.name);
}
},
onLongPress: () => onItemLongPress(context, left, file));
},
),
key: Key(_paths[left ? 0 : 1]),
),
onRefresh: () => listDir(client, _paths[left ? 0 : 1], left));
}
}
void onItemLongPress(BuildContext context, bool left, SftpName file) {
showRoundDialog(
context,
'Action',
Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.delete),
title: const Text('Delete'),
onTap: () => showRoundDialog(context, 'Confirm',
Text('Are you sure to delete ${file.filename}?'), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
TextButton(
onPressed: () {},
child: const Text(
'Delete',
style: TextStyle(color: Colors.red),
)),
]),
),
ListTile(
leading: const Icon(Icons.folder),
title: const Text('Create Folder'),
onTap: () => mkdir(context, left)),
ListTile(
leading: Icon(left ? Icons.arrow_forward : Icons.arrow_back),
title: const Text('Copy'),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.edit),
title: const Text('Rename'),
onTap: () => rename(context, left, file),
),
ListTile(
leading: const Icon(Icons.file_download),
title: const Text('Download'),
onTap: () {},
),
],
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'))
]);
}
void mkdir(BuildContext context, bool left) {
final textController = TextEditingController();
showRoundDialog(
context,
'Create Folder',
TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'Folder Name',
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
TextButton(
onPressed: () {
if (textController.text == '') {
showRoundDialog(context, 'Attention',
const Text('You need input a name.'), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK')),
]);
return;
}
_clients[left ? 0 : 1]!
.mkdir(_paths[left ? 0 : 1] + '/' + textController.text);
},
child: const Text(
'Create',
style: TextStyle(color: Colors.red),
)),
]);
}
void rename(BuildContext context, bool left, SftpName file) {
final textController = TextEditingController();
showRoundDialog(
context,
'Create Folder',
TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'New Name',
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(context, 'Attention',
const Text('You need input a name.'), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK')),
]);
return;
}
await _clients[left ? 0 : 1]!
.rename(file.filename, textController.text);
},
child: const Text(
'Create',
style: TextStyle(color: Colors.red),
)),
]);
}
String convertBytes(int bytes) {
const suffix = ['B', 'KB', 'MB', 'GB', 'TB'];
double value = bytes.toDouble();
int squareTimes = 0;
for (; value / 1024 > 1 && squareTimes < 3; squareTimes++) {
value /= 1024;
}
var finalValue = value.toStringAsFixed(1);
if (finalValue.endsWith('.0')) {
finalValue = finalValue.replaceFirst('.0', '');
}
return '$finalValue ${suffix[squareTimes]}';
}
void updatePath(String filename, bool left) {
if (filename == '..') {
_paths[left ? 0 : 1] = _paths[left ? 0 : 1]
.substring(0, _paths[left ? 0 : 1].lastIndexOf('/'));
if (_paths[left ? 0 : 1] == '') {
_paths[left ? 0 : 1] = '/';
}
return;
}
_paths[left ? 0 : 1] = _paths[left ? 0 : 1] +
(_paths[left ? 0 : 1].endsWith('/') ? '' : '/') +
filename;
}
Future<void> listDir(SSHClient client, String path, bool left) async {
final sftpc = await client.sftp();
_clients[left ? 0 : 1] = sftpc;
final fs = await sftpc.listdir(path);
fs.sort((a, b) => a.filename.compareTo(b.filename));
fs.removeAt(0);
if (mounted) {
setState(() {
_files[left ? 0 : 1] = fs;
});
}
}
Widget _buildDestSelector(bool left) {
return Column(
children: locator<ServerProvider>()
.servers
.map((e) => _buildDestSelectorItem(e.info, left))
.toList(),
);
}
Widget _buildDestSelectorItem(ServerPrivateInfo spi, bool left) {
return ListTile(
title: Text(spi.name),
subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'),
onTap: () {
setState(() {
_destSpi[left ? 0 : 1] = spi;
_selectedDest[left ? 0 : 1] = true;
});
},
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
/// 渐隐渐显实现
class FadeIn extends StatefulWidget {
final Widget child;
const FadeIn({Key? key, required this.child}) : super(key: key);
@override
_MyFadeInState createState() => _MyFadeInState();
}
class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 377),
);
_animation = Tween(
begin: 0.0,
end: 1.0,
).animate(_controller);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
_controller.forward();
return FadeTransition(
opacity: _animation,
child: widget.child,
);
}
}

View File

@@ -44,13 +44,12 @@ Future<String> getFlutterVersion() async {
} }
Future<Map<String, dynamic>> getBuildData() async { Future<Map<String, dynamic>> getBuildData() async {
final modifiedCount = await getGitModificationCount();
final data = { final data = {
'name': appName, 'name': appName,
'build': await getGitCommitCount() + (modifiedCount == 0 ? 0 : 1), 'build': await getGitCommitCount(),
'engine': await getFlutterVersion(), 'engine': await getFlutterVersion(),
'buildAt': DateTime.now().toString(), 'buildAt': DateTime.now().toString(),
'modifications': modifiedCount, 'modifications': await getGitModificationCount(),
}; };
return data; return data;
} }
@@ -84,8 +83,7 @@ void flutterRun(String? mode) {
Future<void> flutterBuild(String source, String target, bool isAndroid) async { Future<void> flutterBuild(String source, String target, bool isAndroid) async {
final startTime = DateTime.now(); final startTime = DateTime.now();
final build = await getGitCommitCount() + final build = await getGitCommitCount();
(await getGitModificationCount() == 0 ? 0 : 1);
final args = [ final args = [
'build', 'build',