From b34b504bf3921b8be30e8cb9d2a59b2590130e0c Mon Sep 17 00:00:00 2001 From: PaperCube Date: Fri, 16 Feb 2024 12:38:14 +0000 Subject: [PATCH 1/6] Auto-close the tab where session is done --- lib/view/page/server/tab.dart | 4 ++-- lib/view/page/ssh/page.dart | 4 ++++ lib/view/page/ssh/tab.dart | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index bf05d183..92f62b76 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -206,7 +206,7 @@ class _ServerPageState extends State late final List children; if (srv.state == ServerState.finished) { if (cardStatus.value.flip) { - children = [title, ..._buildFlipedCard(srv)]; + children = [title, ..._buildFlippedCard(srv)]; } else { children = [title, ..._buildNormalCard(srv.status, srv.spi)]; } @@ -228,7 +228,7 @@ class _ServerPageState extends State ); } - List _buildFlipedCard(Server srv) { + List _buildFlippedCard(Server srv) { return [ UIs.height13, Row( diff --git a/lib/view/page/ssh/page.dart b/lib/view/page/ssh/page.dart index cdb75ad3..af532416 100644 --- a/lib/view/page/ssh/page.dart +++ b/lib/view/page/ssh/page.dart @@ -33,11 +33,14 @@ class SSHPage extends StatefulWidget { final ServerPrivateInfo spi; final String? initCmd; final bool pop; + final Function()? onSessionEnd; + const SSHPage({ super.key, required this.spi, this.initCmd, this.pop = true, + this.onSessionEnd, }); @override @@ -381,6 +384,7 @@ class _SSHPageState extends State with AutomaticKeepAliveClientMixin { if (mounted && widget.pop) { context.pop(); } + widget.onSessionEnd?.call(); } void _listen(Stream? stream) { diff --git a/lib/view/page/ssh/tab.dart b/lib/view/page/ssh/tab.dart index f2d830fd..9f597559 100644 --- a/lib/view/page/ssh/tab.dart +++ b/lib/view/page/ssh/tab.dart @@ -71,6 +71,7 @@ class _SSHTabPageState extends State if (confirm != true) { return; } + // debugPrint("Removing a tab whose tabId = $e"); _tabIds.remove(e); _refreshTabs(); }, @@ -104,6 +105,12 @@ class _SSHTabPageState extends State key: key, spi: spi, pop: false, + onSessionEnd: () { + // debugPrint("Session done received on page whose tabId = $name"); + // debugPrint("key = $key"); + _tabIds.remove(name); + _refreshTabs(); + }, ); _refreshTabs(); _tabController.animateTo(_tabIds.length - 1); From 1d8f6bed6b6886537a986c39db7e00c416783317 Mon Sep 17 00:00:00 2001 From: PaperCube Date: Fri, 16 Feb 2024 13:48:57 +0000 Subject: [PATCH 2/6] Experimental support for haptic feedback --- android/app/src/main/AndroidManifest.xml | 1 + lib/core/utils/platform/base.dart | 1 + lib/view/page/ssh/page.dart | 8 ++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4fe2fdbf..be725c75 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + with AutomaticKeepAliveClientMixin { _terminalTheme = _isDark ? TerminalThemes.dark : TerminalThemes.light; // Because the virtual keyboard only displayed on mobile devices - if (isMobile) { + if (isMobile || isDebuggingMobileLayoutOnDesktop) { _virtKeyWidth = _media.size.width / 7; _virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length; } @@ -118,7 +118,9 @@ class _SSHPageState extends State with AutomaticKeepAliveClientMixin { Widget child = Scaffold( backgroundColor: _terminalTheme.background, body: _buildBody(), - bottomNavigationBar: isDesktop ? null : _buildBottom(), + bottomNavigationBar: isDesktop && !isDebuggingMobileLayoutOnDesktop + ? null + : _buildBottom(), ); if (isIOS) { child = AnnotatedRegion( @@ -232,10 +234,12 @@ class _SSHPageState extends State with AutomaticKeepAliveClientMixin { void _doVirtualKey(VirtKey item) { if (item.func != null) { + HapticFeedback.mediumImpact(); _doVirtualKeyFunc(item.func!); return; } if (item.key != null) { + HapticFeedback.mediumImpact(); _doVirtualKeyInput(item.key!); } } From 18b33ee0a2627a640a320909cf3de624f06641e9 Mon Sep 17 00:00:00 2001 From: PaperCube Date: Fri, 16 Feb 2024 17:48:03 +0000 Subject: [PATCH 3/6] Sort files with directories first in SFTP --- lib/core/utils/comparator.dart | 83 +++++++++++++++++++++++++++++++++ lib/view/page/storage/sftp.dart | 22 +++++++-- 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 lib/core/utils/comparator.dart diff --git a/lib/core/utils/comparator.dart b/lib/core/utils/comparator.dart new file mode 100644 index 00000000..6416683b --- /dev/null +++ b/lib/core/utils/comparator.dart @@ -0,0 +1,83 @@ +class ChainComparator { + final ChainComparator? _parent; + final Comparator _comparator; + + ChainComparator._create(this._parent, this._comparator); + ChainComparator.empty() : this._create(null, (a, b) => 0); + ChainComparator.create() : this._create(null, (a, b) => 0); + + static ChainComparator comparing>( + F Function(T) extractor) { + return ChainComparator._create( + null, (a, b) => extractor(a).compareTo(extractor(b))); + } + + int compare(T a, T b) { + final parent = _parent; + if (parent != null) { + final int result = parent.compare(a, b); + if (result != 0) return result; + } + return _comparator(a, b); + } + + int call(T a, T b) { + return compare(a, b); + } + + ChainComparator thenCompareBy>( + F Function(T) extractor, + {bool reversed = false}) { + return ChainComparator._create( + this, + reversed + ? (a, b) => extractor(b).compareTo(extractor(a)) + : (a, b) => extractor(a).compareTo(extractor(b)), + ); + } + + ChainComparator thenWithComparator(Comparator comparator, + {bool reversed = false}) { + return ChainComparator._create( + this, + !reversed ? comparator : (a, b) => comparator(b, a), + ); + } + + ChainComparator thenCompareByReversed>( + F Function(T) extractor) { + return ChainComparator._create( + this, (a, b) => -extractor(a).compareTo(extractor(b))); + } + + ChainComparator thenTrueFirst(bool Function(T) f) { + return ChainComparator._create(this, (a, b) { + final fa = f(a), fb = f(b); + return fa == fb ? 0 : (fa ? -1 : 1); + }); + } + + ChainComparator reversed() { + return ChainComparator._create(null, (a, b) => this.compare(b, a)); + } +} + +class Comparators { + static Comparator compareStringCaseInsensitive( + {bool uppercaseFirst = false}) { + return (String a, String b) { + final r = a.toLowerCase().compareTo(b.toLowerCase()); + if (r != 0) return r; + return uppercaseFirst ? a.compareTo(b) : b.compareTo(a); + }; + } +} + +/* + +Comparator.comparing(Type1::getType2) +.thenCompare(Type1::getType3) +.thenCompare(Type1::getType4) +.thenCompareReversed(Type1::getType5) + + */ \ No newline at end of file diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index f6f1c0c9..48eb8169 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -8,6 +8,7 @@ import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/snackbar.dart'; import 'package:toolbox/core/extension/sftpfile.dart'; +import 'package:toolbox/core/utils/comparator.dart'; import 'package:toolbox/core/utils/platform/base.dart'; import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/misc.dart'; @@ -800,15 +801,30 @@ enum _SortType { List sort(List files) { switch (this) { case _SortType.name: - files.sort((a, b) => a.filename.compareTo(b.filename)); + files.sort( + ChainComparator.create() + .thenTrueFirst((x) => x.attr.isDirectory) + .thenWithComparator((a, b) => + Comparators.compareStringCaseInsensitive()( + a.filename, b.filename)) + .compare, + ); break; case _SortType.time: files.sort( - (a, b) => (a.attr.modifyTime ?? 0).compareTo(b.attr.modifyTime ?? 0), + ChainComparator.create() + .thenTrueFirst((x) => x.attr.isDirectory) + .thenCompareBy((x) => x.attr.modifyTime ?? 0) + .compare, ); break; case _SortType.size: - files.sort((a, b) => (a.attr.size ?? 0).compareTo(b.attr.size ?? 0)); + files.sort( + ChainComparator.create() + .thenTrueFirst((x) => x.attr.isDirectory) + .thenCompareBy((x) => x.attr.size ?? 0) + .compare, + ); break; } return files; From 8d722da799bdf7840a2ac218d28dd44680217737 Mon Sep 17 00:00:00 2001 From: PaperCube Date: Sat, 17 Feb 2024 00:26:01 +0000 Subject: [PATCH 4/6] SFTP supports reversed sorting --- lib/view/page/storage/sftp.dart | 90 +++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 48eb8169..82952bf3 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -10,6 +10,7 @@ import 'package:toolbox/core/extension/context/snackbar.dart'; import 'package:toolbox/core/extension/sftpfile.dart'; import 'package:toolbox/core/utils/comparator.dart'; import 'package:toolbox/core/utils/platform/base.dart'; +import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; @@ -51,7 +52,8 @@ class _SftpPageState extends State with AfterLayoutMixin { final _status = SftpBrowserStatus(); late final _client = widget.spi.server?.client; - final _sortType = ValueNotifier(_SortType.name); + final _sortOption = + ValueNotifier(_SortOption(sortBy: _SortType.name, reversed: false)); @override Widget build(BuildContext context) { @@ -70,29 +72,47 @@ class _SftpPageState extends State with AfterLayoutMixin { icon: const Icon(Icons.downloading), onPressed: () => AppRoute.sftpMission().go(context), ), - ValueListenableBuilder<_SortType>( - valueListenable: _sortType, + ValueListenableBuilder( + valueListenable: _sortOption, builder: (context, value, child) { return PopupMenuButton<_SortType>( icon: const Icon(Icons.sort), itemBuilder: (context) { - return [ - PopupMenuItem( - value: _SortType.name, - child: Text(l10n.name), - ), - PopupMenuItem( - value: _SortType.size, - child: Text(l10n.size), - ), - PopupMenuItem( - value: _SortType.time, - child: Text(l10n.time), - ), + final currentSelectedOption = _sortOption.value; + final options = [ + (_SortType.name, l10n.name), + (_SortType.size, l10n.size), + (_SortType.time, l10n.time), ]; + return options.map((r) { + final (type, name) = r; + return PopupMenuItem( + value: type, + child: Text( + type == currentSelectedOption.sortBy + ? "$name (${currentSelectedOption.reversed ? '-' : '+'})" + : name, + style: TextStyle( + color: type == currentSelectedOption.sortBy + ? primaryColor + : null, + fontWeight: type == currentSelectedOption.sortBy + ? FontWeight.bold + : null, + ), + ), + ); + }).toList(); }, - onSelected: (value) { - _sortType.value = value; + onSelected: (sortBy) { + final oldValue = _sortOption.value; + if (oldValue.sortBy == sortBy) { + _sortOption.value = _SortOption( + sortBy: sortBy, reversed: !oldValue.reversed); + } else { + _sortOption.value = + _SortOption(sortBy: sortBy, reversed: false); + } }, ); }, @@ -275,9 +295,10 @@ class _SftpPageState extends State with AfterLayoutMixin { child: FadeIn( key: Key(widget.spi.name + _status.path!.path), child: ValueListenableBuilder( - valueListenable: _sortType, - builder: (_, sortType, __) { - final files = sortType.sort(_status.files!); + valueListenable: _sortOption, + builder: (_, sortOption, __) { + final files = sortOption.sortBy + .sort(_status.files!, reversed: sortOption.reversed); return ListView.builder( itemCount: files.length, padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), @@ -798,15 +819,17 @@ enum _SortType { size, ; - List sort(List files) { + List sort(List files, {bool reversed = false}) { switch (this) { case _SortType.name: files.sort( ChainComparator.create() .thenTrueFirst((x) => x.attr.isDirectory) - .thenWithComparator((a, b) => - Comparators.compareStringCaseInsensitive()( - a.filename, b.filename)) + .thenWithComparator( + (a, b) => Comparators.compareStringCaseInsensitive()( + a.filename, b.filename), + reversed: reversed, + ) .compare, ); break; @@ -814,7 +837,10 @@ enum _SortType { files.sort( ChainComparator.create() .thenTrueFirst((x) => x.attr.isDirectory) - .thenCompareBy((x) => x.attr.modifyTime ?? 0) + .thenCompareBy( + (x) => x.attr.modifyTime ?? 0, + reversed: reversed, + ) .compare, ); break; @@ -822,7 +848,10 @@ enum _SortType { files.sort( ChainComparator.create() .thenTrueFirst((x) => x.attr.isDirectory) - .thenCompareBy((x) => x.attr.size ?? 0) + .thenCompareBy( + (x) => x.attr.size ?? 0, + reversed: reversed, + ) .compare, ); break; @@ -830,3 +859,10 @@ enum _SortType { return files; } } + +class _SortOption { + final _SortType sortBy; + final bool reversed; + + _SortOption({required this.sortBy, required this.reversed}); +} From 52e94e902bbb427ce9d00058c340873b92dcaa6f Mon Sep 17 00:00:00 2001 From: PaperCube Date: Sat, 17 Feb 2024 00:58:57 +0000 Subject: [PATCH 5/6] Option in settings to disable SFTP folders-first --- .dart_tool/flutter_gen/gen_l10n/l10n.dart | 6 ++++++ .dart_tool/flutter_gen/gen_l10n/l10n_de.dart | 3 +++ .dart_tool/flutter_gen/gen_l10n/l10n_en.dart | 3 +++ .dart_tool/flutter_gen/gen_l10n/l10n_fr.dart | 3 +++ .dart_tool/flutter_gen/gen_l10n/l10n_id.dart | 3 +++ .dart_tool/flutter_gen/gen_l10n/l10n_zh.dart | 6 ++++++ lib/data/store/setting.dart | 3 +++ lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_tw.arb | 1 + lib/view/page/setting/entry.dart | 8 ++++++++ lib/view/page/storage/sftp.dart | 14 ++++++++------ 15 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart index 677861a8..a18f7d87 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart @@ -1342,6 +1342,12 @@ abstract class S { /// **'Use `rm -r` to delete a folder in SFTP.'** String get sftpRmrDirSummary; + /// No description provided for @sftpShowFoldersFirst. + /// + /// In en, this message translates to: + /// **'Disply folders first'** + String get sftpShowFoldersFirst; + /// No description provided for @sftpSSHConnected. /// /// 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 26ff855d..4a721363 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart @@ -651,6 +651,9 @@ class SDe extends S { @override String get sftpRmrDirSummary => 'Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.'; + @override + String get sftpShowFoldersFirst => 'Ordner zuerst anzeigen'; + @override String get sftpSSHConnected => 'SFTP Verbunden'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart index af8889f8..abdf03d9 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart @@ -651,6 +651,9 @@ class SEn extends S { @override String get sftpRmrDirSummary => 'Use `rm -r` to delete a folder in SFTP.'; + @override + String get sftpShowFoldersFirst => 'Disply folders first'; + @override String get sftpSSHConnected => 'SFTP Connected'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart index 4a6d77c5..7f64e41f 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart @@ -651,6 +651,9 @@ class SFr extends S { @override String get sftpRmrDirSummary => 'Utilisez `rm -r` pour supprimer un dossier dans SFTP.'; + @override + String get sftpShowFoldersFirst => 'Dossiers d\'abord lors du tri'; + @override String get sftpSSHConnected => 'SFTP connecté'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart index 939e7dd1..1c74e9f7 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart @@ -651,6 +651,9 @@ class SId extends S { @override String get sftpRmrDirSummary => 'Gunakan `rm -r` untuk menghapus dir di SFTP'; + @override + String get sftpShowFoldersFirst => 'Folder ditampilkan lebih dulu'; + @override String get sftpSSHConnected => 'Sftp terhubung'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart index 5b3bfd6c..ec5d0427 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart @@ -651,6 +651,9 @@ class SZh extends S { @override String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹'; + @override + String get sftpShowFoldersFirst => '排序时文件夹显示在前'; + @override String get sftpSSHConnected => 'SFTP 已连接...'; @@ -1502,6 +1505,9 @@ class SZhTw extends SZh { @override String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除文件夾'; + @override + String get sftpShowFoldersFirst => '排序時文件夾顯示在前'; + @override String get sftpSSHConnected => 'SFTP 已連接...'; diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 0691c1c6..8b80e998 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -197,6 +197,9 @@ class SettingStore extends PersistentStore { /// Open SFTP with last viewed path late final sftpOpenLastPath = property('sftpOpenLastPath', true); + /// Show folders first in SFTP file browser + late final sftpShowFoldersFirst = property('sftpShowFoldersFirst', true); + /// Show tip of suspend late final showSuspendTip = property('showSuspendTip', true); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index beeb6819..e54d1ae3 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -207,6 +207,7 @@ "setting": "Einstellungen", "sftpDlPrepare": "Verbindung vorbereiten...", "sftpRmrDirSummary": "Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.", + "sftpShowFoldersFirst": "Ordner zuerst anzeigen", "sftpSSHConnected": "SFTP Verbunden", "showDistLogo": "Distributionslogo anzeigen", "shutdown": "Abschaltung", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 19f9778d..dcfd18de 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -207,6 +207,7 @@ "setting": "Settings", "sftpDlPrepare": "Preparing to connect...", "sftpRmrDirSummary": "Use `rm -r` to delete a folder in SFTP.", + "sftpShowFoldersFirst": "Disply folders first", "sftpSSHConnected": "SFTP Connected", "showDistLogo": "Show distribution logo", "shutdown": "Shutdown", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5cefc344..9591c9f0 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -207,6 +207,7 @@ "setting": "Paramètres", "sftpDlPrepare": "Préparation de la connexion...", "sftpRmrDirSummary": "Utilisez `rm -r` pour supprimer un dossier dans SFTP.", + "sftpShowFoldersFirst": "Dossiers d'abord lors du tri", "sftpSSHConnected": "SFTP connecté", "showDistLogo": "Afficher le logo de la distribution", "shutdown": "Éteindre", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 0debae27..3b436cb6 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -207,6 +207,7 @@ "setting": "Pengaturan", "sftpDlPrepare": "Bersiap untuk terhubung ...", "sftpRmrDirSummary": "Gunakan `rm -r` untuk menghapus dir di SFTP", + "sftpShowFoldersFirst": "Folder ditampilkan lebih dulu", "sftpSSHConnected": "Sftp terhubung", "showDistLogo": "Tampilkan logo distribusi", "shutdown": "Matikan", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 89fb357d..eae5fddb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -207,6 +207,7 @@ "setting": "设置", "sftpDlPrepare": "准备连接至服务器...", "sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹", + "sftpShowFoldersFirst": "排序时文件夹显示在前", "sftpSSHConnected": "SFTP 已连接...", "showDistLogo": "显示发行版 Logo", "shutdown": "关机", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 1e2e1d34..e7dc1200 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -207,6 +207,7 @@ "setting": "設置", "sftpDlPrepare": "準備連接至服務器...", "sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除文件夾", + "sftpShowFoldersFirst": "排序時文件夾顯示在前", "sftpSSHConnected": "SFTP 已連接...", "showDistLogo": "顯示發行版 Logo", "shutdown": "关机", diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index 7405bcc7..f622d262 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -855,6 +855,7 @@ class _SettingPageState extends State { children: [ _buildSftpRmrDir(), _buildSftpOpenLastPath(), + _buildSftpShowFoldersFirst(), ].map((e) => CardX(child: e)).toList(), ); } @@ -867,6 +868,13 @@ class _SettingPageState extends State { ); } + Widget _buildSftpShowFoldersFirst() { + return ListTile( + title: Text(l10n.sftpShowFoldersFirst), + trailing: StoreSwitch(prop: _setting.sftpShowFoldersFirst), + ); + } + Widget _buildNetViewType() { final items = NetViewType.values .map((e) => PopupMenuItem( diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 82952bf3..4f973812 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -15,6 +15,7 @@ import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; +import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/view/widget/omit_start_text.dart'; import 'package:toolbox/view/widget/cardx.dart'; @@ -820,11 +821,14 @@ enum _SortType { ; List sort(List files, {bool reversed = false}) { + var comparator = ChainComparator.create(); + if (Stores.setting.sftpShowFoldersFirst.fetch()) { + comparator = comparator.thenTrueFirst((x) => x.attr.isDirectory); + } switch (this) { case _SortType.name: files.sort( - ChainComparator.create() - .thenTrueFirst((x) => x.attr.isDirectory) + comparator .thenWithComparator( (a, b) => Comparators.compareStringCaseInsensitive()( a.filename, b.filename), @@ -835,8 +839,7 @@ enum _SortType { break; case _SortType.time: files.sort( - ChainComparator.create() - .thenTrueFirst((x) => x.attr.isDirectory) + comparator .thenCompareBy( (x) => x.attr.modifyTime ?? 0, reversed: reversed, @@ -846,8 +849,7 @@ enum _SortType { break; case _SortType.size: files.sort( - ChainComparator.create() - .thenTrueFirst((x) => x.attr.isDirectory) + comparator .thenCompareBy( (x) => x.attr.size ?? 0, reversed: reversed, From 21860daf419a1f0edefce4dc0fac26f362a6ab5a Mon Sep 17 00:00:00 2001 From: PaperCube Date: Sat, 17 Feb 2024 01:46:02 +0000 Subject: [PATCH 6/6] Fix sudo pswd dialog neither re-appearing nor closing session - Fixed that when empty password was provided the dialog should have stopped the session - Fixed that when wrong password was provided the dialog should have appeared again but it didn't --- lib/core/extension/ssh_client.dart | 51 +++++++++++++++++++----------- lib/data/provider/server.dart | 2 +- lib/view/page/storage/sftp.dart | 1 - 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index d963096d..42d66c33 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -9,26 +9,31 @@ import 'package:toolbox/core/extension/uint8list.dart'; import '../../data/res/misc.dart'; -typedef _OnStdout = void Function(String data, StreamSink sink); -typedef _OnStdin = void Function(StreamSink sink); +typedef _OnStdout = void Function(String data, SSHSession session); +typedef _OnStdin = void Function(SSHSession session); typedef PwdRequestFunc = Future Function(String? user); extension SSHClientX on SSHClient { - Future exec( + Future exec( String cmd, { _OnStdout? onStderr, _OnStdout? onStdout, _OnStdin? stdin, + bool redirectToBash = false, // not working yet. do not use }) async { - final session = await execute(cmd); + final session = await execute(redirectToBash ? "head -1 | bash" : cmd); + + if (redirectToBash) { + session.stdin.add("$cmd\n".uint8List); + } final stdoutDone = Completer(); final stderrDone = Completer(); if (onStdout != null) { session.stdout.listen( - (e) => onStdout(e.string, session.stdin), + (e) => onStdout(e.string, session), onDone: stdoutDone.complete, ); } else { @@ -37,7 +42,7 @@ extension SSHClientX on SSHClient { if (onStderr != null) { session.stderr.listen( - (e) => onStderr(e.string, session.stdin), + (e) => onStderr(e.string, session), onDone: stderrDone.complete, ); } else { @@ -45,14 +50,14 @@ extension SSHClientX on SSHClient { } if (stdin != null) { - stdin(session.stdin); + stdin(session); } await stdoutDone.future; await stderrDone.future; session.close(); - return session.exitCode; + return session; } Future execWithPwd( @@ -61,38 +66,48 @@ extension SSHClientX on SSHClient { _OnStdout? onStdout, _OnStdout? onStderr, _OnStdin? stdin, + bool redirectToBash = false, // not working yet. do not use }) async { var isRequestingPwd = false; - return await exec( + final session = await exec( cmd, - onStderr: (data, sink) async { - onStderr?.call(data, sink); + redirectToBash: redirectToBash, + onStderr: (data, session) async { + debugPrint( + " --- execWithPwd stderr (reqPwd = $isRequestingPwd) --- $data"); + onStderr?.call(data, session); if (isRequestingPwd) return; - isRequestingPwd = true; + if (data.contains('[sudo] password for ')) { + isRequestingPwd = true; final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1); if (context == null) return; final pwd = await context.showPwdDialog(user); if (pwd == null || pwd.isEmpty) { - // Add ctrl + c to exit. - sink.add('\x03'.uint8List); + debugPrint("Empty pwd. Exiting"); + session.kill(SSHSignal.INT); } else { - sink.add('$pwd\n'.uint8List); + session.stdin.add('$pwd\n'.uint8List); } + isRequestingPwd = false; } }, - onStdout: onStdout, + onStdout: (data, sink) async { + onStdout?.call(data, sink); + debugPrint(" --- execWithPwd stdout --- $data"); + }, stdin: stdin, ); + return session.exitCode; } - Future runWithSessionAction( + Future runForOutput( String command, { bool runInPty = false, bool stdout = true, bool stderr = true, Map? environment, - Future Function(SSHSession)? action, + Future Function(SSHSession)? action, }) async { final session = await execute( command, diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index ff5c55f4..e3ca23dd 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -272,7 +272,7 @@ class ServerProvider extends ChangeNotifier { ensure(await client.run(ShellFunc.installerMkdirs).string); - ensure(await client.runWithSessionAction(ShellFunc.installerShellWriter, + ensure(await client.runForOutput(ShellFunc.installerShellWriter, action: (session) async { session.stdin.add(utf8.encode(ShellFunc.allScript)); }) diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 4f973812..94da5db4 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -15,7 +15,6 @@ import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; -import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/view/widget/omit_start_text.dart'; import 'package:toolbox/view/widget/cardx.dart';