diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart index 767a51bd..4d7824b1 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart @@ -900,6 +900,12 @@ abstract class S { /// **'Save'** String get save; + /// No description provided for @saved. + /// + /// In en, this message translates to: + /// **'Saved'** + String get saved; + /// No description provided for @second. /// /// In en, this message translates to: diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart index 2af102eb..b22b8fb5 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart @@ -432,6 +432,9 @@ class SDe extends S { @override String get save => 'Speichern'; + @override + String get saved => 'Gerettet'; + @override String get second => 's'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart index 234c04fe..3fa5db14 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart @@ -432,6 +432,9 @@ class SEn extends S { @override String get save => 'Save'; + @override + String get saved => 'Saved'; + @override String get second => 's'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart index 81cf6336..8a1f2009 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart @@ -432,6 +432,9 @@ class SZh extends S { @override String get save => '保存'; + @override + String get saved => '已保存'; + @override String get second => '秒'; @@ -1024,6 +1027,9 @@ class SZhTw extends SZh { @override String get save => '保存'; + @override + String get saved => '已保存'; + @override String get second => '秒'; diff --git a/lib/core/extension/sftpfile.dart b/lib/core/extension/sftpfile.dart new file mode 100644 index 00000000..9eb17171 --- /dev/null +++ b/lib/core/extension/sftpfile.dart @@ -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' : '-'}'; +} diff --git a/lib/core/route.dart b/lib/core/route.dart index 6369628c..c9263177 100644 --- a/lib/core/route.dart +++ b/lib/core/route.dart @@ -7,8 +7,11 @@ class AppRoute { AppRoute(this.page, this.title); - void go(BuildContext context) { + Future go(BuildContext context) { Analysis.recordView(title); - Navigator.push(context, MaterialPageRoute(builder: (context) => page)); + return Navigator.push( + context, + MaterialPageRoute(builder: (context) => page), + ); } } diff --git a/lib/core/utils/misc.dart b/lib/core/utils/misc.dart index d336835f..e8468da9 100644 --- a/lib/core/utils/misc.dart +++ b/lib/core/utils/misc.dart @@ -67,3 +67,9 @@ String? getFileName(String? path) { void rebuildAll(BuildContext context) { RebuildWidget.restartApp(context); } + +String getTime(int? unixMill) { + return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000) + .toString() + .replaceFirst('.000', ''); +} diff --git a/lib/data/model/server/conn_status.dart b/lib/data/model/server/conn.dart similarity index 86% rename from lib/data/model/server/conn_status.dart rename to lib/data/model/server/conn.dart index 331f4e21..a95870ac 100644 --- a/lib/data/model/server/conn_status.dart +++ b/lib/data/model/server/conn.dart @@ -1,13 +1,13 @@ import '../../../core/extension/stringx.dart'; import '../../res/misc.dart'; -class ConnStatus { +class Conn { final int maxConn; final int active; final int passive; final int fail; - ConnStatus({ + Conn({ required this.maxConn, required this.active, required this.passive, @@ -15,13 +15,13 @@ class ConnStatus { }); } -ConnStatus? parseConn(String raw) { +Conn? parseConn(String raw) { final lines = raw.split('\n'); final idx = lines.lastWhere((element) => element.startsWith('Tcp:'), orElse: () => ''); if (idx != '') { final vals = idx.split(numReg); - return ConnStatus( + return Conn( maxConn: vals[5].i, active: vals[6].i, passive: vals[7].i, diff --git a/lib/data/model/server/cpu_status.dart b/lib/data/model/server/cpu.dart similarity index 97% rename from lib/data/model/server/cpu_status.dart rename to lib/data/model/server/cpu.dart index 4b6a69aa..7364f5c6 100644 --- a/lib/data/model/server/cpu_status.dart +++ b/lib/data/model/server/cpu.dart @@ -1,7 +1,7 @@ -class CpuStatus { +class Cpus { List _pre; List _now; - CpuStatus(this._pre, this._now); + Cpus(this._pre, this._now); double usedPercent({int coreIdx = 0}) { if (_now.length != _pre.length) return 0; diff --git a/lib/data/model/server/disk_info.dart b/lib/data/model/server/disk.dart similarity index 87% rename from lib/data/model/server/disk_info.dart rename to lib/data/model/server/disk.dart index 58349905..420fa38a 100644 --- a/lib/data/model/server/disk_info.dart +++ b/lib/data/model/server/disk.dart @@ -1,6 +1,6 @@ import '../../res/misc.dart'; -class DiskInfo { +class Disk { final String path; final String loc; final int usedPercent; @@ -8,7 +8,7 @@ class DiskInfo { final String size; final String avail; - DiskInfo({ + Disk({ required this.path, required this.loc, required this.usedPercent, @@ -18,8 +18,8 @@ class DiskInfo { }); } -List parseDisk(String raw) { - final list = []; +List parseDisk(String raw) { + final list = []; final items = raw.split('\n'); items.removeAt(0); var pathCache = ''; @@ -36,7 +36,7 @@ List parseDisk(String raw) { vals[0] = pathCache; pathCache = ''; } - list.add(DiskInfo( + list.add(Disk( path: vals[0], loc: vals[5], usedPercent: int.parse(vals[4].replaceFirst('%', '')), diff --git a/lib/data/model/server/server_status.dart b/lib/data/model/server/server_status.dart index e8bde5e7..3eccd31a 100644 --- a/lib/data/model/server/server_status.dart +++ b/lib/data/model/server/server_status.dart @@ -1,19 +1,19 @@ import 'package:toolbox/data/model/server/temp.dart'; -import 'cpu_status.dart'; -import 'disk_info.dart'; +import 'cpu.dart'; +import 'disk.dart'; import 'memory.dart'; import 'net_speed.dart'; -import 'conn_status.dart'; +import 'conn.dart'; class ServerStatus { - CpuStatus cpu; + Cpus cpu; Memory mem; Swap swap; String sysVer; String uptime; - List disk; - ConnStatus tcp; + List disk; + Conn tcp; NetSpeed netSpeed; Temperatures temps; String? failedInfo; diff --git a/lib/data/model/server/server_status_update_req.dart b/lib/data/model/server/server_status_update_req.dart index 06492e24..b75c4f7b 100644 --- a/lib/data/model/server/server_status_update_req.dart +++ b/lib/data/model/server/server_status_update_req.dart @@ -1,10 +1,10 @@ import '../../res/server_cmd.dart'; -import 'cpu_status.dart'; -import 'disk_info.dart'; +import 'cpu.dart'; +import 'disk.dart'; import 'memory.dart'; import 'net_speed.dart'; import 'server_status.dart'; -import 'conn_status.dart'; +import 'conn.dart'; class ServerStatusUpdateReq { final ServerStatus ss; diff --git a/lib/data/model/sftp/download_worker.dart b/lib/data/model/sftp/download_worker.dart index 2e37dd63..58a1c51e 100644 --- a/lib/data/model/sftp/download_worker.dart +++ b/lib/data/model/sftp/download_worker.dart @@ -85,7 +85,8 @@ class SftpDownloadWorker { mainSendPort.send((i + form.length) / size * 100); } } - localFile.close(); + await localFile.close(); + await file.close(); mainSendPort.send(watch.elapsed); mainSendPort.send(SftpWorkerStatus.finished); } catch (e) { diff --git a/lib/data/model/ssh/virtual_key.dart b/lib/data/model/ssh/virtual_key.dart index 876cc81b..fe61cbc8 100644 --- a/lib/data/model/ssh/virtual_key.dart +++ b/lib/data/model/ssh/virtual_key.dart @@ -17,4 +17,4 @@ class VirtualKey { }); } -enum VirtualKeyFunc { toggleIME, backspace, copy, paste, snippet } +enum VirtualKeyFunc { toggleIME, backspace, copy, paste, snippet, file } diff --git a/lib/data/res/highlight.dart b/lib/data/res/highlight.dart index 26a022f6..8033b75b 100644 --- a/lib/data/res/highlight.dart +++ b/lib/data/res/highlight.dart @@ -20,6 +20,7 @@ import 'package:highlight/languages/nix.dart'; import 'package:highlight/languages/objectivec.dart'; import 'package:highlight/languages/perl.dart'; import 'package:highlight/languages/php.dart'; +import 'package:highlight/languages/plaintext.dart'; import 'package:highlight/languages/powershell.dart'; import 'package:highlight/languages/python.dart'; import 'package:highlight/languages/ruby.dart'; @@ -34,7 +35,7 @@ import 'package:highlight/languages/yaml.dart'; // KEY: fileNameSuffix // VAL: highlight -final _suffix2HighlightMap = { +final suffix2HighlightMap = { 'dart': dart, 'go': go, 'rust': rust, @@ -68,12 +69,16 @@ final _suffix2HighlightMap = { 'html': htmlbars, 'tex': tex, 'vim': vim, + 'plaintext': plaintext, }; extension HighlightString on String? { Mode? get highlight { + return suffix2HighlightMap[highlightCode]; + } + + String? get highlightCode { if (this == null) return null; - final suffix = this!.split('.').last; - return _suffix2HighlightMap[suffix]; + return this!.split('.').last; } } diff --git a/lib/data/res/misc.dart b/lib/data/res/misc.dart index 0c421bf0..3b3d6940 100644 --- a/lib/data/res/misc.dart +++ b/lib/data/res/misc.dart @@ -6,6 +6,9 @@ final numReg = RegExp(r'\s{1,}'); /// Private Key max allowed size is 20kb const privateKeyMaxSize = 20 * 1024; +// Editor max allowed size is 1mb +const editorMaxSize = 1024 * 1024; + /// Max debug log lines const maxDebugLogLines = 100; diff --git a/lib/data/res/status.dart b/lib/data/res/status.dart index b8f92efc..3a7c53c6 100644 --- a/lib/data/res/status.dart +++ b/lib/data/res/status.dart @@ -1,11 +1,11 @@ import 'package:toolbox/data/model/server/temp.dart'; -import '../model/server/cpu_status.dart'; -import '../model/server/disk_info.dart'; +import '../model/server/cpu.dart'; +import '../model/server/disk.dart'; import '../model/server/memory.dart'; import '../model/server/net_speed.dart'; import '../model/server/server_status.dart'; -import '../model/server/conn_status.dart'; +import '../model/server/conn.dart'; Memory get _initMemory => Memory( total: 1, @@ -23,7 +23,7 @@ OneTimeCpuStatus get _initOneTimeCpuStatus => OneTimeCpuStatus( 0, 0, ); -CpuStatus get initCpuStatus => CpuStatus( +Cpus get initCpuStatus => Cpus( [_initOneTimeCpuStatus], [_initOneTimeCpuStatus], ); @@ -48,7 +48,7 @@ ServerStatus get initStatus => ServerStatus( sysVer: 'Loading...', uptime: '', disk: [ - DiskInfo( + Disk( path: '/', loc: '/', usedPercent: 0, @@ -57,7 +57,7 @@ ServerStatus get initStatus => ServerStatus( avail: '0', ) ], - tcp: ConnStatus(maxConn: 0, active: 0, passive: 0, fail: 0), + tcp: Conn(maxConn: 0, active: 0, passive: 0, fail: 0), netSpeed: initNetSpeed, swap: _initSwap, temps: Temperatures(), diff --git a/lib/data/res/virtual_key.dart b/lib/data/res/virtual_key.dart index 43d4d0f8..62f3dcad 100644 --- a/lib/data/res/virtual_key.dart +++ b/lib/data/res/virtual_key.dart @@ -10,9 +10,9 @@ final virtualKeys = [ VirtualKey('Up', key: TerminalKey.arrowUp, icon: Icons.arrow_upward), VirtualKey('End', key: TerminalKey.end), VirtualKey( - 'Del', - key: TerminalKey.delete, - icon: Icons.backspace, + 'File', + func: VirtualKeyFunc.file, + icon: Icons.file_open, ), VirtualKey('Snippet', func: VirtualKeyFunc.snippet, icon: Icons.code), VirtualKey('Tab', key: TerminalKey.tab), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d7d9e08e..71bf0132 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -134,6 +134,7 @@ "result": "Result", "run": "Ausführen", "save": "Speichern", + "saved": "Gerettet", "second": "s", "server": "Server", "serverTabConnecting": "Verbinden...", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67cb02b0..398bde7a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -134,6 +134,7 @@ "result": "Result", "run": "Run", "save": "Save", + "saved": "Saved", "second": "s", "server": "Server", "serverTabConnecting": "Connecting...", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index dd104a4e..29b8f55e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -134,6 +134,7 @@ "result": "结果", "run": "运行", "save": "保存", + "saved": "已保存", "second": "秒", "server": "服务器", "serverTabConnecting": "连接中...", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 9f0ca81f..08de02c7 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -134,6 +134,7 @@ "result": "結果", "run": "運行", "save": "保存", + "saved": "已保存", "second": "秒", "server": "服務器", "serverTabConnecting": "連接中...", diff --git a/lib/view/page/editor.dart b/lib/view/page/editor.dart index 602995a7..073509ff 100644 --- a/lib/view/page/editor.dart +++ b/lib/view/page/editor.dart @@ -1,16 +1,22 @@ +import 'dart:io'; + import 'package:code_text_field/code_text_field.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/themes/monokai.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/store/setting.dart'; import 'package:toolbox/locator.dart'; +import '../widget/two_line_text.dart'; + class EditorPage extends StatefulWidget { + final String? path; final String? initCode; - final String? fileName; - const EditorPage({Key? key, this.initCode, this.fileName}) : super(key: key); + const EditorPage({Key? key, this.path, this.initCode}) : super(key: key); @override _EditorPageState createState() => _EditorPageState(); @@ -21,16 +27,30 @@ class _EditorPageState extends State { late final _focusNode = FocusNode(); final _setting = locator(); late Map _codeTheme; + late S _s; + late String? _langCode; @override void initState() { super.initState(); - _focusNode.requestFocus(); + _langCode = widget.path.highlightCode; _controller = CodeController( text: widget.initCode, - language: widget.fileName.highlight, + language: suffix2HighlightMap[_langCode ?? 'plaintext'], ); _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 @@ -45,23 +65,41 @@ class _EditorPageState extends State { return Scaffold( backgroundColor: _codeTheme['root']!.backgroundColor, appBar: AppBar( - title: Text(widget.fileName ?? ''), + title: TwoLineText(up: getFileName(widget.path) ?? '', down: _s.editor), actions: [ - IconButton( - icon: const Icon(Icons.done), - onPressed: () { - context.pop(_controller.text); + PopupMenuButton( + icon: const Icon(Icons.language), + onSelected: (value) { + _controller.language = suffix2HighlightMap[value]; }, - ), + initialValue: _langCode, + itemBuilder: (BuildContext context) { + return suffix2HighlightMap.keys.map((e) { + return PopupMenuItem( + value: e, + child: Text(e), + ); + }).toList(); + }, + ) ], ), - body: CodeTheme( - data: CodeThemeData(styles: _codeTheme), - child: CodeField( - controller: _controller, - textStyle: const TextStyle(fontFamily: 'SourceCode'), + body: SingleChildScrollView( + child: CodeTheme( + data: CodeThemeData(styles: _codeTheme), + child: CodeField( + focusNode: _focusNode, + controller: _controller, + textStyle: const TextStyle(fontFamily: 'SourceCode'), + ), ), ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.done), + onPressed: () { + context.pop(_controller.text); + }, + ), ); } } diff --git a/lib/view/page/server/detail.dart b/lib/view/page/server/detail.dart index 300f5413..0d39f426 100644 --- a/lib/view/page/server/detail.dart +++ b/lib/view/page/server/detail.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:provider/provider.dart'; import 'package:toolbox/core/extension/order.dart'; -import 'package:toolbox/data/model/server/cpu_status.dart'; -import 'package:toolbox/data/model/server/disk_info.dart'; +import 'package:toolbox/data/model/server/cpu.dart'; +import 'package:toolbox/data/model/server/disk.dart'; import 'package:toolbox/data/model/server/dist.dart'; import 'package:toolbox/data/model/server/memory.dart'; import 'package:toolbox/data/model/server/temp.dart'; @@ -118,7 +118,7 @@ class _ServerDetailPageState extends State ); } - Widget _buildCPUView(CpuStatus cs) { + Widget _buildCPUView(Cpus cs) { return RoundRectCard( Padding( padding: roundRectCardPadding, @@ -171,7 +171,7 @@ class _ServerDetailPageState extends State ); } - Widget _buildCPUProgress(CpuStatus cs) { + Widget _buildCPUProgress(Cpus cs) { final children = []; for (var i = 0; i < cs.coresCount; i++) { if (i == 0) continue; @@ -288,7 +288,7 @@ class _ServerDetailPageState extends State ); } - Widget _buildDiskView(List disk) { + Widget _buildDiskView(List disk) { disk.removeWhere((e) { for (final ingorePath in _setting.diskIgnorePath.fetch()!) { if (e.path.startsWith(ingorePath)) return true; diff --git a/lib/view/page/setting.dart b/lib/view/page/setting.dart index 721ac25d..5cbb2d40 100644 --- a/lib/view/page/setting.dart +++ b/lib/view/page/setting.dart @@ -99,7 +99,7 @@ class _SettingPageState extends State { _buildTitle('SSH'), _buildSSH(), // Editor - _buildTitle('Editor'), + _buildTitle(_s.editor), _buildEditor(), const SizedBox(height: 37), ], @@ -535,17 +535,18 @@ class _SettingPageState extends State { ), actions: [ TextButton( - onPressed: () { - context.pop(); - final fontSize = double.tryParse(ctrller.text); - if (fontSize == null) { - showRoundDialog(context: context, child: Text(_s.failed)); - return; - } - _fontSize = fontSize; - _setting.termFontSize.put(_fontSize); - }, - child: Text(_s.ok)), + onPressed: () { + context.pop(); + final fontSize = double.tryParse(ctrller.text); + if (fontSize == null) { + showRoundDialog(context: context, child: Text(_s.failed)); + return; + } + _fontSize = fontSize; + _setting.termFontSize.put(_fontSize); + }, + child: Text(_s.ok), + ), ], ); }, @@ -559,23 +560,25 @@ class _SettingPageState extends State { trailing: Text(_s.edit, style: textSize15), onTap: () { showRoundDialog( - context: context, - child: Input( - controller: TextEditingController(text: json.encode(paths)), - label: 'JSON', - type: TextInputType.visiblePassword, - maxLines: 3, - onSubmitted: (p0) { - try { - final list = List.from(json.decode(p0)); - _setting.diskIgnorePath.put(list); - context.pop(); - showSnackBar(context, Text(_s.success)); - } catch (e) { - showSnackBar(context, Text(e.toString())); - } - }, - )); + context: context, + title: Text(_s.diskIgnorePath), + child: Input( + controller: TextEditingController(text: json.encode(paths)), + label: 'JSON', + type: TextInputType.visiblePassword, + maxLines: 3, + onSubmitted: (p0) { + try { + final list = List.from(json.decode(p0)); + _setting.diskIgnorePath.put(list); + context.pop(); + showSnackBar(context, Text(_s.success)); + } catch (e) { + showSnackBar(context, Text(e.toString())); + } + }, + ), + ); }, ); } @@ -641,7 +644,6 @@ class _SettingPageState extends State { _editorTheme = idx; }); _setting.editorTheme.put(idx); - _showRestartSnackbar(); }, child: Text( _editorTheme, diff --git a/lib/view/page/sftp/downloaded.dart b/lib/view/page/sftp/downloaded.dart index 7555f60f..0b29adc2 100644 --- a/lib/view/page/sftp/downloaded.dart +++ b/lib/view/page/sftp/downloaded.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.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/widget/input_field.dart'; import '../../../core/extension/numx.dart'; import '../../../core/extension/stringx.dart'; @@ -141,6 +143,55 @@ class _SFTPDownloadedPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, 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(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( leading: const Icon(Icons.delete), title: Text(_s.delete), @@ -173,31 +224,8 @@ class _SFTPDownloadedPageState extends State { 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), - ) - ], ); } } diff --git a/lib/view/page/sftp/view.dart b/lib/view/page/sftp/view.dart index d2bf29b1..370acd19 100644 --- a/lib/view/page/sftp/view.dart +++ b/lib/view/page/sftp/view.dart @@ -1,13 +1,18 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.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/stringx.dart'; import '../../../core/route.dart'; +import '../../../core/utils/misc.dart'; import '../../../core/utils/ui.dart'; import '../../../data/model/server/server.dart'; import '../../../data/model/server/server_private_info.dart'; @@ -39,7 +44,7 @@ class _SFTPPageState extends State { late S _s; - Server? _si; + ServerState? _state; SSHClient? _client; @override @@ -52,8 +57,8 @@ class _SFTPPageState extends State { void initState() { super.initState(); final serverProvider = locator(); - _si = serverProvider.servers[widget.spi.id]; - _client = _si?.client; + _client = serverProvider.servers[widget.spi.id]?.client; + _state = serverProvider.servers[widget.spi.id]?.state; } @override @@ -92,7 +97,7 @@ class _SFTPPageState extends State { IconButton( padding: const EdgeInsets.all(0), onPressed: () async { - await backward(); + await _backward(); }, icon: const Icon(Icons.arrow_back), ), @@ -116,11 +121,11 @@ class _SFTPPageState extends State { ListTile( leading: const Icon(Icons.folder), title: Text(_s.createFolder), - onTap: () => mkdir(context)), + onTap: () => _mkdir(context)), ListTile( leading: const Icon(Icons.insert_drive_file), title: Text(_s.createFile), - onTap: () => newFile(context)), + onTap: () => _newFile(context)), ], ), actions: [ @@ -163,14 +168,14 @@ class _SFTPPageState extends State { return; } _status.path?.update(p!); - listDir(path: p); + _listDir(path: p); }, icon: const Icon(Icons.gps_fixed), ); } Widget _buildFileView() { - if (_client == null || _si?.state != ServerState.connected) { + if (_client == null || _state != ServerState.connected) { return centerLoading; } @@ -180,7 +185,7 @@ class _SFTPPageState extends State { if (_status.files == null) { _status.path = AbsolutePath('/'); - listDir(path: '/', client: _client); + _listDir(path: '/', client: _client); return centerLoading; } else { return RefreshIndicator( @@ -196,7 +201,7 @@ class _SFTPPageState extends State { leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), title: Text(file.filename), 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), textAlign: TextAlign.right, ), @@ -205,80 +210,105 @@ class _SFTPPageState extends State { onTap: () { if (isDir) { _status.path?.update(file.filename); - listDir(path: _status.path?.path); + _listDir(path: _status.path?.path); } 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) { - 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) { + void _onItemPress(BuildContext context, SftpName file, bool notDir) { showRoundDialog( context: context, child: Column( mainAxisSize: MainAxisSize.min, children: [ + notDir + ? ListTile( + leading: const Icon(Icons.edit), + title: Text(_s.edit), + onTap: () => _edit(context, file), + ) + : placeholder, ListTile( leading: const Icon(Icons.delete), title: Text(_s.delete), - onTap: () => delete(context, file), + onTap: () => _delete(context, file), ), ListTile( - leading: const Icon(Icons.edit), + leading: const Icon(Icons.abc), title: Text(_s.rename), - onTap: () => rename(context, file), + onTap: () => _rename(context, file), ), - showDownload + notDir ? ListTile( leading: const Icon(Icons.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 _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(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( context: context, child: Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'), @@ -313,7 +343,7 @@ class _SFTPPageState extends State { ); } - void delete(BuildContext context, SftpName file) { + void _delete(BuildContext context, SftpName file) { context.pop(); final isDir = file.attr.isDirectory; final dirText = isDir ? '\n${_s.sureDirEmpty}' : ''; @@ -358,7 +388,7 @@ class _SFTPPageState extends State { ); return; } - listDir(); + _listDir(); }, child: Text( _s.delete, @@ -369,7 +399,7 @@ class _SFTPPageState extends State { ); } - void mkdir(BuildContext context) { + void _mkdir(BuildContext context) { context.pop(); final textController = TextEditingController(); showRoundDialog( @@ -402,7 +432,7 @@ class _SFTPPageState extends State { _status.client! .mkdir('${_status.path!.path}/${textController.text}'); context.pop(); - listDir(); + _listDir(); }, child: Text( _s.ok, @@ -413,7 +443,7 @@ class _SFTPPageState extends State { ); } - void newFile(BuildContext context) { + void _newFile(BuildContext context) { context.pop(); final textController = TextEditingController(); showRoundDialog( @@ -447,7 +477,7 @@ class _SFTPPageState extends State { .open('${_status.path!.path}/${textController.text}')) .writeBytes(Uint8List(0)); context.pop(); - listDir(); + _listDir(); }, child: Text( _s.ok, @@ -458,7 +488,7 @@ class _SFTPPageState extends State { ); } - void rename(BuildContext context, SftpName file) { + void _rename(BuildContext context, SftpName file) { context.pop(); final textController = TextEditingController(); showRoundDialog( @@ -487,7 +517,7 @@ class _SFTPPageState extends State { } await _status.client!.rename(file.filename, textController.text); context.pop(); - listDir(); + _listDir(); }, child: Text( _s.rename, @@ -503,7 +533,7 @@ class _SFTPPageState extends State { return prePath + (prePath.endsWith('/') ? '' : '/') + name.filename; } - Future listDir({String? path, SSHClient? client}) async { + Future _listDir({String? path, SSHClient? client}) async { if (_status.isBusy) { return; } @@ -535,13 +565,13 @@ class _SFTPPageState extends State { ) ], ); - await backward(); + await _backward(); } } - Future backward() async { + Future _backward() async { if (_status.path!.undo()) { - await listDir(); + await _listDir(); } } } diff --git a/lib/view/page/ssh.dart b/lib/view/page/ssh.dart index c10d6c77..45f17be2 100644 --- a/lib/view/page/ssh.dart +++ b/lib/view/page/ssh.dart @@ -228,6 +228,9 @@ class _SSHPageState extends State { _terminal.keyInput(TerminalKey.enter); }); break; + case VirtualKeyFunc.file: + // TODO + showRoundDialog(context: context, child: const Text('TODO')); } }