#24 #25 #33 new: sftp file edit

This commit is contained in:
lollipopkit
2023-05-28 18:21:03 +08:00
parent dbabe81e3c
commit 06be4503ca
28 changed files with 329 additions and 173 deletions

View File

@@ -900,6 +900,12 @@ abstract class S {
/// **'Save'** /// **'Save'**
String get save; String get save;
/// No description provided for @saved.
///
/// In en, this message translates to:
/// **'Saved'**
String get saved;
/// No description provided for @second. /// No description provided for @second.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -432,6 +432,9 @@ class SDe extends S {
@override @override
String get save => 'Speichern'; String get save => 'Speichern';
@override
String get saved => 'Gerettet';
@override @override
String get second => 's'; String get second => 's';

View File

@@ -432,6 +432,9 @@ class SEn extends S {
@override @override
String get save => 'Save'; String get save => 'Save';
@override
String get saved => 'Saved';
@override @override
String get second => 's'; String get second => 's';

View File

@@ -432,6 +432,9 @@ class SZh extends S {
@override @override
String get save => '保存'; String get save => '保存';
@override
String get saved => '已保存';
@override @override
String get second => ''; String get second => '';
@@ -1024,6 +1027,9 @@ class SZhTw extends SZh {
@override @override
String get save => '保存'; String get save => '保存';
@override
String get saved => '已保存';
@override @override
String get second => ''; String get second => '';

View File

@@ -0,0 +1,15 @@
import 'package:dartssh2/dartssh2.dart';
extension SftpFile on SftpFileMode {
String get str {
final user = getRoleMode(userRead, userWrite, userExecute);
final group = getRoleMode(groupRead, groupWrite, groupExecute);
final other = getRoleMode(otherRead, otherWrite, otherExecute);
return '$user$group$other';
}
}
String getRoleMode(bool r, bool w, bool x) {
return '${r ? 'r' : '-'}${w ? 'w' : '-'}${x ? 'x' : '-'}';
}

View File

@@ -7,8 +7,11 @@ class AppRoute {
AppRoute(this.page, this.title); AppRoute(this.page, this.title);
void go(BuildContext context) { Future<T?> go<T>(BuildContext context) {
Analysis.recordView(title); Analysis.recordView(title);
Navigator.push(context, MaterialPageRoute(builder: (context) => page)); return Navigator.push<T>(
context,
MaterialPageRoute(builder: (context) => page),
);
} }
} }

View File

@@ -67,3 +67,9 @@ String? getFileName(String? path) {
void rebuildAll(BuildContext context) { void rebuildAll(BuildContext context) {
RebuildWidget.restartApp(context); RebuildWidget.restartApp(context);
} }
String getTime(int? unixMill) {
return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000)
.toString()
.replaceFirst('.000', '');
}

View File

@@ -1,13 +1,13 @@
import '../../../core/extension/stringx.dart'; import '../../../core/extension/stringx.dart';
import '../../res/misc.dart'; import '../../res/misc.dart';
class ConnStatus { class Conn {
final int maxConn; final int maxConn;
final int active; final int active;
final int passive; final int passive;
final int fail; final int fail;
ConnStatus({ Conn({
required this.maxConn, required this.maxConn,
required this.active, required this.active,
required this.passive, required this.passive,
@@ -15,13 +15,13 @@ class ConnStatus {
}); });
} }
ConnStatus? parseConn(String raw) { Conn? parseConn(String raw) {
final lines = raw.split('\n'); final lines = raw.split('\n');
final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), final idx = lines.lastWhere((element) => element.startsWith('Tcp:'),
orElse: () => ''); orElse: () => '');
if (idx != '') { if (idx != '') {
final vals = idx.split(numReg); final vals = idx.split(numReg);
return ConnStatus( return Conn(
maxConn: vals[5].i, maxConn: vals[5].i,
active: vals[6].i, active: vals[6].i,
passive: vals[7].i, passive: vals[7].i,

View File

@@ -1,7 +1,7 @@
class CpuStatus { class Cpus {
List<OneTimeCpuStatus> _pre; List<OneTimeCpuStatus> _pre;
List<OneTimeCpuStatus> _now; List<OneTimeCpuStatus> _now;
CpuStatus(this._pre, this._now); Cpus(this._pre, this._now);
double usedPercent({int coreIdx = 0}) { double usedPercent({int coreIdx = 0}) {
if (_now.length != _pre.length) return 0; if (_now.length != _pre.length) return 0;

View File

@@ -1,6 +1,6 @@
import '../../res/misc.dart'; import '../../res/misc.dart';
class DiskInfo { class Disk {
final String path; final String path;
final String loc; final String loc;
final int usedPercent; final int usedPercent;
@@ -8,7 +8,7 @@ class DiskInfo {
final String size; final String size;
final String avail; final String avail;
DiskInfo({ Disk({
required this.path, required this.path,
required this.loc, required this.loc,
required this.usedPercent, required this.usedPercent,
@@ -18,8 +18,8 @@ class DiskInfo {
}); });
} }
List<DiskInfo> parseDisk(String raw) { List<Disk> parseDisk(String raw) {
final list = <DiskInfo>[]; final list = <Disk>[];
final items = raw.split('\n'); final items = raw.split('\n');
items.removeAt(0); items.removeAt(0);
var pathCache = ''; var pathCache = '';
@@ -36,7 +36,7 @@ List<DiskInfo> parseDisk(String raw) {
vals[0] = pathCache; vals[0] = pathCache;
pathCache = ''; pathCache = '';
} }
list.add(DiskInfo( list.add(Disk(
path: vals[0], path: vals[0],
loc: vals[5], loc: vals[5],
usedPercent: int.parse(vals[4].replaceFirst('%', '')), usedPercent: int.parse(vals[4].replaceFirst('%', '')),

View File

@@ -1,19 +1,19 @@
import 'package:toolbox/data/model/server/temp.dart'; import 'package:toolbox/data/model/server/temp.dart';
import 'cpu_status.dart'; import 'cpu.dart';
import 'disk_info.dart'; import 'disk.dart';
import 'memory.dart'; import 'memory.dart';
import 'net_speed.dart'; import 'net_speed.dart';
import 'conn_status.dart'; import 'conn.dart';
class ServerStatus { class ServerStatus {
CpuStatus cpu; Cpus cpu;
Memory mem; Memory mem;
Swap swap; Swap swap;
String sysVer; String sysVer;
String uptime; String uptime;
List<DiskInfo> disk; List<Disk> disk;
ConnStatus tcp; Conn tcp;
NetSpeed netSpeed; NetSpeed netSpeed;
Temperatures temps; Temperatures temps;
String? failedInfo; String? failedInfo;

View File

@@ -1,10 +1,10 @@
import '../../res/server_cmd.dart'; import '../../res/server_cmd.dart';
import 'cpu_status.dart'; import 'cpu.dart';
import 'disk_info.dart'; import 'disk.dart';
import 'memory.dart'; import 'memory.dart';
import 'net_speed.dart'; import 'net_speed.dart';
import 'server_status.dart'; import 'server_status.dart';
import 'conn_status.dart'; import 'conn.dart';
class ServerStatusUpdateReq { class ServerStatusUpdateReq {
final ServerStatus ss; final ServerStatus ss;

View File

@@ -85,7 +85,8 @@ class SftpDownloadWorker {
mainSendPort.send((i + form.length) / size * 100); mainSendPort.send((i + form.length) / size * 100);
} }
} }
localFile.close(); await localFile.close();
await file.close();
mainSendPort.send(watch.elapsed); mainSendPort.send(watch.elapsed);
mainSendPort.send(SftpWorkerStatus.finished); mainSendPort.send(SftpWorkerStatus.finished);
} catch (e) { } catch (e) {

View File

@@ -17,4 +17,4 @@ class VirtualKey {
}); });
} }
enum VirtualKeyFunc { toggleIME, backspace, copy, paste, snippet } enum VirtualKeyFunc { toggleIME, backspace, copy, paste, snippet, file }

View File

@@ -20,6 +20,7 @@ import 'package:highlight/languages/nix.dart';
import 'package:highlight/languages/objectivec.dart'; import 'package:highlight/languages/objectivec.dart';
import 'package:highlight/languages/perl.dart'; import 'package:highlight/languages/perl.dart';
import 'package:highlight/languages/php.dart'; import 'package:highlight/languages/php.dart';
import 'package:highlight/languages/plaintext.dart';
import 'package:highlight/languages/powershell.dart'; import 'package:highlight/languages/powershell.dart';
import 'package:highlight/languages/python.dart'; import 'package:highlight/languages/python.dart';
import 'package:highlight/languages/ruby.dart'; import 'package:highlight/languages/ruby.dart';
@@ -34,7 +35,7 @@ import 'package:highlight/languages/yaml.dart';
// KEY: fileNameSuffix // KEY: fileNameSuffix
// VAL: highlight // VAL: highlight
final _suffix2HighlightMap = { final suffix2HighlightMap = {
'dart': dart, 'dart': dart,
'go': go, 'go': go,
'rust': rust, 'rust': rust,
@@ -68,12 +69,16 @@ final _suffix2HighlightMap = {
'html': htmlbars, 'html': htmlbars,
'tex': tex, 'tex': tex,
'vim': vim, 'vim': vim,
'plaintext': plaintext,
}; };
extension HighlightString on String? { extension HighlightString on String? {
Mode? get highlight { Mode? get highlight {
return suffix2HighlightMap[highlightCode];
}
String? get highlightCode {
if (this == null) return null; if (this == null) return null;
final suffix = this!.split('.').last; return this!.split('.').last;
return _suffix2HighlightMap[suffix];
} }
} }

View File

@@ -6,6 +6,9 @@ final numReg = RegExp(r'\s{1,}');
/// Private Key max allowed size is 20kb /// Private Key max allowed size is 20kb
const privateKeyMaxSize = 20 * 1024; const privateKeyMaxSize = 20 * 1024;
// Editor max allowed size is 1mb
const editorMaxSize = 1024 * 1024;
/// Max debug log lines /// Max debug log lines
const maxDebugLogLines = 100; const maxDebugLogLines = 100;

View File

@@ -1,11 +1,11 @@
import 'package:toolbox/data/model/server/temp.dart'; import 'package:toolbox/data/model/server/temp.dart';
import '../model/server/cpu_status.dart'; import '../model/server/cpu.dart';
import '../model/server/disk_info.dart'; import '../model/server/disk.dart';
import '../model/server/memory.dart'; import '../model/server/memory.dart';
import '../model/server/net_speed.dart'; import '../model/server/net_speed.dart';
import '../model/server/server_status.dart'; import '../model/server/server_status.dart';
import '../model/server/conn_status.dart'; import '../model/server/conn.dart';
Memory get _initMemory => Memory( Memory get _initMemory => Memory(
total: 1, total: 1,
@@ -23,7 +23,7 @@ OneTimeCpuStatus get _initOneTimeCpuStatus => OneTimeCpuStatus(
0, 0,
0, 0,
); );
CpuStatus get initCpuStatus => CpuStatus( Cpus get initCpuStatus => Cpus(
[_initOneTimeCpuStatus], [_initOneTimeCpuStatus],
[_initOneTimeCpuStatus], [_initOneTimeCpuStatus],
); );
@@ -48,7 +48,7 @@ ServerStatus get initStatus => ServerStatus(
sysVer: 'Loading...', sysVer: 'Loading...',
uptime: '', uptime: '',
disk: [ disk: [
DiskInfo( Disk(
path: '/', path: '/',
loc: '/', loc: '/',
usedPercent: 0, usedPercent: 0,
@@ -57,7 +57,7 @@ ServerStatus get initStatus => ServerStatus(
avail: '0', avail: '0',
) )
], ],
tcp: ConnStatus(maxConn: 0, active: 0, passive: 0, fail: 0), tcp: Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
netSpeed: initNetSpeed, netSpeed: initNetSpeed,
swap: _initSwap, swap: _initSwap,
temps: Temperatures(), temps: Temperatures(),

View File

@@ -10,9 +10,9 @@ final virtualKeys = [
VirtualKey('Up', key: TerminalKey.arrowUp, icon: Icons.arrow_upward), VirtualKey('Up', key: TerminalKey.arrowUp, icon: Icons.arrow_upward),
VirtualKey('End', key: TerminalKey.end), VirtualKey('End', key: TerminalKey.end),
VirtualKey( VirtualKey(
'Del', 'File',
key: TerminalKey.delete, func: VirtualKeyFunc.file,
icon: Icons.backspace, icon: Icons.file_open,
), ),
VirtualKey('Snippet', func: VirtualKeyFunc.snippet, icon: Icons.code), VirtualKey('Snippet', func: VirtualKeyFunc.snippet, icon: Icons.code),
VirtualKey('Tab', key: TerminalKey.tab), VirtualKey('Tab', key: TerminalKey.tab),

View File

@@ -134,6 +134,7 @@
"result": "Result", "result": "Result",
"run": "Ausführen", "run": "Ausführen",
"save": "Speichern", "save": "Speichern",
"saved": "Gerettet",
"second": "s", "second": "s",
"server": "Server", "server": "Server",
"serverTabConnecting": "Verbinden...", "serverTabConnecting": "Verbinden...",

View File

@@ -134,6 +134,7 @@
"result": "Result", "result": "Result",
"run": "Run", "run": "Run",
"save": "Save", "save": "Save",
"saved": "Saved",
"second": "s", "second": "s",
"server": "Server", "server": "Server",
"serverTabConnecting": "Connecting...", "serverTabConnecting": "Connecting...",

View File

@@ -134,6 +134,7 @@
"result": "结果", "result": "结果",
"run": "运行", "run": "运行",
"save": "保存", "save": "保存",
"saved": "已保存",
"second": "秒", "second": "秒",
"server": "服务器", "server": "服务器",
"serverTabConnecting": "连接中...", "serverTabConnecting": "连接中...",

View File

@@ -134,6 +134,7 @@
"result": "結果", "result": "結果",
"run": "運行", "run": "運行",
"save": "保存", "save": "保存",
"saved": "已保存",
"second": "秒", "second": "秒",
"server": "服務器", "server": "服務器",
"serverTabConnecting": "連接中...", "serverTabConnecting": "連接中...",

View File

@@ -1,16 +1,22 @@
import 'dart:io';
import 'package:code_text_field/code_text_field.dart'; import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_highlight/theme_map.dart'; import 'package:flutter_highlight/theme_map.dart';
import 'package:flutter_highlight/themes/monokai.dart'; import 'package:flutter_highlight/themes/monokai.dart';
import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/data/res/highlight.dart'; import 'package:toolbox/data/res/highlight.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import '../widget/two_line_text.dart';
class EditorPage extends StatefulWidget { class EditorPage extends StatefulWidget {
final String? path;
final String? initCode; final String? initCode;
final String? fileName; const EditorPage({Key? key, this.path, this.initCode}) : super(key: key);
const EditorPage({Key? key, this.initCode, this.fileName}) : super(key: key);
@override @override
_EditorPageState createState() => _EditorPageState(); _EditorPageState createState() => _EditorPageState();
@@ -21,16 +27,30 @@ class _EditorPageState extends State<EditorPage> {
late final _focusNode = FocusNode(); late final _focusNode = FocusNode();
final _setting = locator<SettingStore>(); final _setting = locator<SettingStore>();
late Map<String, TextStyle> _codeTheme; late Map<String, TextStyle> _codeTheme;
late S _s;
late String? _langCode;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_focusNode.requestFocus(); _langCode = widget.path.highlightCode;
_controller = CodeController( _controller = CodeController(
text: widget.initCode, text: widget.initCode,
language: widget.fileName.highlight, language: suffix2HighlightMap[_langCode ?? 'plaintext'],
); );
_codeTheme = themeMap[_setting.editorTheme.fetch()] ?? monokaiTheme; _codeTheme = themeMap[_setting.editorTheme.fetch()] ?? monokaiTheme;
if (widget.initCode == null && widget.path != null) {
File(widget.path!)
.readAsString()
.then((value) => _controller.text = value);
}
_focusNode.requestFocus();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context)!;
} }
@override @override
@@ -45,23 +65,41 @@ class _EditorPageState extends State<EditorPage> {
return Scaffold( return Scaffold(
backgroundColor: _codeTheme['root']!.backgroundColor, backgroundColor: _codeTheme['root']!.backgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text(widget.fileName ?? ''), title: TwoLineText(up: getFileName(widget.path) ?? '', down: _s.editor),
actions: [ actions: [
IconButton( PopupMenuButton(
icon: const Icon(Icons.done), icon: const Icon(Icons.language),
onPressed: () { onSelected: (value) {
context.pop(_controller.text); _controller.language = suffix2HighlightMap[value];
}, },
), initialValue: _langCode,
itemBuilder: (BuildContext context) {
return suffix2HighlightMap.keys.map((e) {
return PopupMenuItem(
value: e,
child: Text(e),
);
}).toList();
},
)
], ],
), ),
body: CodeTheme( body: SingleChildScrollView(
data: CodeThemeData(styles: _codeTheme), child: CodeTheme(
child: CodeField( data: CodeThemeData(styles: _codeTheme),
controller: _controller, child: CodeField(
textStyle: const TextStyle(fontFamily: 'SourceCode'), focusNode: _focusNode,
controller: _controller,
textStyle: const TextStyle(fontFamily: 'SourceCode'),
),
), ),
), ),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.done),
onPressed: () {
context.pop(_controller.text);
},
),
); );
} }
} }

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/data/model/server/cpu_status.dart'; import 'package:toolbox/data/model/server/cpu.dart';
import 'package:toolbox/data/model/server/disk_info.dart'; import 'package:toolbox/data/model/server/disk.dart';
import 'package:toolbox/data/model/server/dist.dart'; import 'package:toolbox/data/model/server/dist.dart';
import 'package:toolbox/data/model/server/memory.dart'; import 'package:toolbox/data/model/server/memory.dart';
import 'package:toolbox/data/model/server/temp.dart'; import 'package:toolbox/data/model/server/temp.dart';
@@ -118,7 +118,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
); );
} }
Widget _buildCPUView(CpuStatus cs) { Widget _buildCPUView(Cpus cs) {
return RoundRectCard( return RoundRectCard(
Padding( Padding(
padding: roundRectCardPadding, padding: roundRectCardPadding,
@@ -171,7 +171,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
); );
} }
Widget _buildCPUProgress(CpuStatus cs) { Widget _buildCPUProgress(Cpus cs) {
final children = <Widget>[]; final children = <Widget>[];
for (var i = 0; i < cs.coresCount; i++) { for (var i = 0; i < cs.coresCount; i++) {
if (i == 0) continue; if (i == 0) continue;
@@ -288,7 +288,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
); );
} }
Widget _buildDiskView(List<DiskInfo> disk) { Widget _buildDiskView(List<Disk> disk) {
disk.removeWhere((e) { disk.removeWhere((e) {
for (final ingorePath in _setting.diskIgnorePath.fetch()!) { for (final ingorePath in _setting.diskIgnorePath.fetch()!) {
if (e.path.startsWith(ingorePath)) return true; if (e.path.startsWith(ingorePath)) return true;

View File

@@ -99,7 +99,7 @@ class _SettingPageState extends State<SettingPage> {
_buildTitle('SSH'), _buildTitle('SSH'),
_buildSSH(), _buildSSH(),
// Editor // Editor
_buildTitle('Editor'), _buildTitle(_s.editor),
_buildEditor(), _buildEditor(),
const SizedBox(height: 37), const SizedBox(height: 37),
], ],
@@ -535,17 +535,18 @@ class _SettingPageState extends State<SettingPage> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
context.pop(); context.pop();
final fontSize = double.tryParse(ctrller.text); final fontSize = double.tryParse(ctrller.text);
if (fontSize == null) { if (fontSize == null) {
showRoundDialog(context: context, child: Text(_s.failed)); showRoundDialog(context: context, child: Text(_s.failed));
return; return;
} }
_fontSize = fontSize; _fontSize = fontSize;
_setting.termFontSize.put(_fontSize); _setting.termFontSize.put(_fontSize);
}, },
child: Text(_s.ok)), child: Text(_s.ok),
),
], ],
); );
}, },
@@ -559,23 +560,25 @@ class _SettingPageState extends State<SettingPage> {
trailing: Text(_s.edit, style: textSize15), trailing: Text(_s.edit, style: textSize15),
onTap: () { onTap: () {
showRoundDialog( showRoundDialog(
context: context, context: context,
child: Input( title: Text(_s.diskIgnorePath),
controller: TextEditingController(text: json.encode(paths)), child: Input(
label: 'JSON', controller: TextEditingController(text: json.encode(paths)),
type: TextInputType.visiblePassword, label: 'JSON',
maxLines: 3, type: TextInputType.visiblePassword,
onSubmitted: (p0) { maxLines: 3,
try { onSubmitted: (p0) {
final list = List<String>.from(json.decode(p0)); try {
_setting.diskIgnorePath.put(list); final list = List<String>.from(json.decode(p0));
context.pop(); _setting.diskIgnorePath.put(list);
showSnackBar(context, Text(_s.success)); context.pop();
} catch (e) { showSnackBar(context, Text(_s.success));
showSnackBar(context, Text(e.toString())); } catch (e) {
} showSnackBar(context, Text(e.toString()));
}, }
)); },
),
);
}, },
); );
} }
@@ -641,7 +644,6 @@ class _SettingPageState extends State<SettingPage> {
_editorTheme = idx; _editorTheme = idx;
}); });
_setting.editorTheme.put(idx); _setting.editorTheme.put(idx);
_showRestartSnackbar();
}, },
child: Text( child: Text(
_editorTheme, _editorTheme,

View File

@@ -3,7 +3,9 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/view/page/editor.dart'; import 'package:toolbox/view/page/editor.dart';
import 'package:toolbox/view/widget/input_field.dart';
import '../../../core/extension/numx.dart'; import '../../../core/extension/numx.dart';
import '../../../core/extension/stringx.dart'; import '../../../core/extension/stringx.dart';
@@ -141,6 +143,55 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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,
child: Text(_s.fileTooLarge(fileName, stat.size, '1m')),
);
return;
}
final f = File(file.absolute.path);
final data = await f.readAsString();
final result = await AppRoute(
EditorPage(
initCode: data,
path: fileName,
),
'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( ListTile(
leading: const Icon(Icons.delete), leading: const Icon(Icons.delete),
title: Text(_s.delete), title: Text(_s.delete),
@@ -173,31 +224,8 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
shareFiles(context, [file.absolute.path]); shareFiles(context, [file.absolute.path]);
}, },
), ),
ListTile(
leading: const Icon(Icons.edit),
title: Text(_s.edit),
onTap: () async {
context.pop();
final stat = await file.stat();
if (stat.size > 1024 * 1024) {
showRoundDialog(
context: context,
child: Text(_s.fileTooLarge(fileName, stat.size, '1m')),
);
return;
}
final f = await File(file.absolute.path).readAsString();
AppRoute(EditorPage(initCode: f), 'sftp dled editor').go(context);
},
)
], ],
), ),
actions: [
TextButton(
onPressed: (() => context.pop()),
child: Text(_s.close),
)
],
); );
} }
} }

View File

@@ -1,13 +1,18 @@
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/navigator.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 '../../../core/extension/numx.dart'; import '../../../core/extension/numx.dart';
import '../../../core/extension/stringx.dart'; import '../../../core/extension/stringx.dart';
import '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../core/utils/misc.dart';
import '../../../core/utils/ui.dart'; import '../../../core/utils/ui.dart';
import '../../../data/model/server/server.dart'; import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart'; import '../../../data/model/server/server_private_info.dart';
@@ -39,7 +44,7 @@ class _SFTPPageState extends State<SFTPPage> {
late S _s; late S _s;
Server? _si; ServerState? _state;
SSHClient? _client; SSHClient? _client;
@override @override
@@ -52,8 +57,8 @@ class _SFTPPageState extends State<SFTPPage> {
void initState() { void initState() {
super.initState(); super.initState();
final serverProvider = locator<ServerProvider>(); final serverProvider = locator<ServerProvider>();
_si = serverProvider.servers[widget.spi.id]; _client = serverProvider.servers[widget.spi.id]?.client;
_client = _si?.client; _state = serverProvider.servers[widget.spi.id]?.state;
} }
@override @override
@@ -92,7 +97,7 @@ class _SFTPPageState extends State<SFTPPage> {
IconButton( IconButton(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
onPressed: () async { onPressed: () async {
await backward(); await _backward();
}, },
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
), ),
@@ -116,11 +121,11 @@ class _SFTPPageState extends State<SFTPPage> {
ListTile( ListTile(
leading: const Icon(Icons.folder), leading: const Icon(Icons.folder),
title: Text(_s.createFolder), title: Text(_s.createFolder),
onTap: () => mkdir(context)), onTap: () => _mkdir(context)),
ListTile( ListTile(
leading: const Icon(Icons.insert_drive_file), leading: const Icon(Icons.insert_drive_file),
title: Text(_s.createFile), title: Text(_s.createFile),
onTap: () => newFile(context)), onTap: () => _newFile(context)),
], ],
), ),
actions: [ actions: [
@@ -163,14 +168,14 @@ class _SFTPPageState extends State<SFTPPage> {
return; return;
} }
_status.path?.update(p!); _status.path?.update(p!);
listDir(path: p); _listDir(path: p);
}, },
icon: const Icon(Icons.gps_fixed), icon: const Icon(Icons.gps_fixed),
); );
} }
Widget _buildFileView() { Widget _buildFileView() {
if (_client == null || _si?.state != ServerState.connected) { if (_client == null || _state != ServerState.connected) {
return centerLoading; return centerLoading;
} }
@@ -180,7 +185,7 @@ class _SFTPPageState extends State<SFTPPage> {
if (_status.files == null) { if (_status.files == null) {
_status.path = AbsolutePath('/'); _status.path = AbsolutePath('/');
listDir(path: '/', client: _client); _listDir(path: '/', client: _client);
return centerLoading; return centerLoading;
} else { } else {
return RefreshIndicator( return RefreshIndicator(
@@ -196,7 +201,7 @@ class _SFTPPageState extends State<SFTPPage> {
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename), title: Text(file.filename),
trailing: Text( trailing: Text(
'${getTime(file.attr.modifyTime)}\n${getMode(file.attr.mode)}', '${getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
@@ -205,80 +210,105 @@ class _SFTPPageState extends State<SFTPPage> {
onTap: () { onTap: () {
if (isDir) { if (isDir) {
_status.path?.update(file.filename); _status.path?.update(file.filename);
listDir(path: _status.path?.path); _listDir(path: _status.path?.path);
} else { } else {
onItemPress(context, file, true); _onItemPress(context, file, true);
} }
}, },
onLongPress: () => onItemPress(context, file, false), onLongPress: () => _onItemPress(context, file, false),
); );
}, },
), ),
), ),
onRefresh: () => listDir(path: _status.path?.path), onRefresh: () => _listDir(path: _status.path?.path),
); );
} }
} }
String getTime(int? unixMill) { void _onItemPress(BuildContext context, SftpName file, bool notDir) {
return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000)
.toString()
.replaceFirst('.000', '');
}
String getMode(SftpFileMode? mode) {
if (mode == null) {
return '---';
}
final user = getRoleMode(mode.userRead, mode.userWrite, mode.userExecute);
final group =
getRoleMode(mode.groupRead, mode.groupWrite, mode.groupExecute);
final other =
getRoleMode(mode.otherRead, mode.otherWrite, mode.otherExecute);
return '$user$group$other';
}
String getRoleMode(bool r, bool w, bool x) {
return '${r ? 'r' : '-'}${w ? 'w' : '-'}${x ? 'x' : '-'}';
}
void onItemPress(BuildContext context, SftpName file, bool showDownload) {
showRoundDialog( showRoundDialog(
context: context, context: context,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
notDir
? ListTile(
leading: const Icon(Icons.edit),
title: Text(_s.edit),
onTap: () => _edit(context, file),
)
: placeholder,
ListTile( ListTile(
leading: const Icon(Icons.delete), leading: const Icon(Icons.delete),
title: Text(_s.delete), title: Text(_s.delete),
onTap: () => delete(context, file), onTap: () => _delete(context, file),
), ),
ListTile( ListTile(
leading: const Icon(Icons.edit), leading: const Icon(Icons.abc),
title: Text(_s.rename), title: Text(_s.rename),
onTap: () => rename(context, file), onTap: () => _rename(context, file),
), ),
showDownload notDir
? ListTile( ? ListTile(
leading: const Icon(Icons.download), leading: const Icon(Icons.download),
title: Text(_s.download), title: Text(_s.download),
onTap: () => download(context, file), onTap: () => _download(context, file),
) )
: placeholder : placeholder,
], ],
), ),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(_s.cancel),
)
],
); );
} }
void download(BuildContext context, SftpName name) { 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;
}
final file = await _status.client!.open(
_getRemotePath(name),
mode: SftpFileOpenMode.read | SftpFileOpenMode.write,
);
final localPath = '${(await sftpDir).path}${_getRemotePath(name)}';
await Directory(localPath.substring(0, localPath.lastIndexOf('/')))
.create(recursive: true);
final local = File(localPath);
if (await local.exists()) {
await local.delete();
}
final localFile = local.openWrite(mode: FileMode.append);
const defaultChunkSize = 1024 * 1024;
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
for (var i = 0; i < size; i += chunkSize) {
final fileData = file.read(length: chunkSize);
await for (var form in fileData) {
localFile.add(form);
}
}
await localFile.close();
context.pop();
final result = await AppRoute(
EditorPage(path: localPath),
'SFTP edit',
).go<String>(context);
if (result != null) {
await local.writeAsString(result);
await file.writeBytes(result.uint8List);
showSnackBar(context, Text(_s.saved));
}
await file.close();
}
void _download(BuildContext context, SftpName name) {
showRoundDialog( showRoundDialog(
context: context, context: context,
child: Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'), child: Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'),
@@ -313,7 +343,7 @@ class _SFTPPageState extends State<SFTPPage> {
); );
} }
void delete(BuildContext context, SftpName file) { void _delete(BuildContext context, SftpName file) {
context.pop(); context.pop();
final isDir = file.attr.isDirectory; final isDir = file.attr.isDirectory;
final dirText = isDir ? '\n${_s.sureDirEmpty}' : ''; final dirText = isDir ? '\n${_s.sureDirEmpty}' : '';
@@ -358,7 +388,7 @@ class _SFTPPageState extends State<SFTPPage> {
); );
return; return;
} }
listDir(); _listDir();
}, },
child: Text( child: Text(
_s.delete, _s.delete,
@@ -369,7 +399,7 @@ class _SFTPPageState extends State<SFTPPage> {
); );
} }
void mkdir(BuildContext context) { void _mkdir(BuildContext context) {
context.pop(); context.pop();
final textController = TextEditingController(); final textController = TextEditingController();
showRoundDialog( showRoundDialog(
@@ -402,7 +432,7 @@ class _SFTPPageState extends State<SFTPPage> {
_status.client! _status.client!
.mkdir('${_status.path!.path}/${textController.text}'); .mkdir('${_status.path!.path}/${textController.text}');
context.pop(); context.pop();
listDir(); _listDir();
}, },
child: Text( child: Text(
_s.ok, _s.ok,
@@ -413,7 +443,7 @@ class _SFTPPageState extends State<SFTPPage> {
); );
} }
void newFile(BuildContext context) { void _newFile(BuildContext context) {
context.pop(); context.pop();
final textController = TextEditingController(); final textController = TextEditingController();
showRoundDialog( showRoundDialog(
@@ -447,7 +477,7 @@ class _SFTPPageState extends State<SFTPPage> {
.open('${_status.path!.path}/${textController.text}')) .open('${_status.path!.path}/${textController.text}'))
.writeBytes(Uint8List(0)); .writeBytes(Uint8List(0));
context.pop(); context.pop();
listDir(); _listDir();
}, },
child: Text( child: Text(
_s.ok, _s.ok,
@@ -458,7 +488,7 @@ class _SFTPPageState extends State<SFTPPage> {
); );
} }
void rename(BuildContext context, SftpName file) { void _rename(BuildContext context, SftpName file) {
context.pop(); context.pop();
final textController = TextEditingController(); final textController = TextEditingController();
showRoundDialog( showRoundDialog(
@@ -487,7 +517,7 @@ class _SFTPPageState extends State<SFTPPage> {
} }
await _status.client!.rename(file.filename, textController.text); await _status.client!.rename(file.filename, textController.text);
context.pop(); context.pop();
listDir(); _listDir();
}, },
child: Text( child: Text(
_s.rename, _s.rename,
@@ -503,7 +533,7 @@ class _SFTPPageState extends State<SFTPPage> {
return prePath + (prePath.endsWith('/') ? '' : '/') + name.filename; return prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
} }
Future<void> listDir({String? path, SSHClient? client}) async { Future<void> _listDir({String? path, SSHClient? client}) async {
if (_status.isBusy) { if (_status.isBusy) {
return; return;
} }
@@ -535,13 +565,13 @@ class _SFTPPageState extends State<SFTPPage> {
) )
], ],
); );
await backward(); await _backward();
} }
} }
Future<void> backward() async { Future<void> _backward() async {
if (_status.path!.undo()) { if (_status.path!.undo()) {
await listDir(); await _listDir();
} }
} }
} }

View File

@@ -228,6 +228,9 @@ class _SSHPageState extends State<SSHPage> {
_terminal.keyInput(TerminalKey.enter); _terminal.keyInput(TerminalKey.enter);
}); });
break; break;
case VirtualKeyFunc.file:
// TODO
showRoundDialog(context: context, child: const Text('TODO'));
} }
} }