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/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 @@
+
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/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/core/utils/platform/base.dart b/lib/core/utils/platform/base.dart
index bbc711c0..44cf9fd2 100644
--- a/lib/core/utils/platform/base.dart
+++ b/lib/core/utils/platform/base.dart
@@ -61,3 +61,4 @@ final isWeb = OS.type == OS.web;
final isMobile = OS.type == OS.ios || OS.type == OS.android;
final isDesktop =
OS.type == OS.linux || OS.type == OS.macos || OS.type == OS.windows;
+const isDebuggingMobileLayoutOnDesktop = kDebugMode;
\ No newline at end of file
diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart
index 9aea3c99..f2e7a0de 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(ShellFunc.allScript.uint8List);
})
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/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/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/ssh/page.dart b/lib/view/page/ssh/page.dart
index cdb75ad3..2718e562 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
@@ -103,7 +106,7 @@ class _SSHPageState extends State 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;
}
@@ -115,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(
@@ -229,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!);
}
}
@@ -381,6 +388,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);
diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart
index f6f1c0c9..94da5db4 100644
--- a/lib/view/page/storage/sftp.dart
+++ b/lib/view/page/storage/sftp.dart
@@ -8,7 +8,9 @@ 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/color.dart';
import 'package:toolbox/data/res/logger.dart';
import 'package:toolbox/data/res/misc.dart';
import 'package:toolbox/data/res/provider.dart';
@@ -50,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) {
@@ -69,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);
+ }
},
);
},
@@ -274,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),
@@ -797,20 +819,51 @@ enum _SortType {
size,
;
- List sort(List files) {
+ 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((a, b) => a.filename.compareTo(b.filename));
+ files.sort(
+ comparator
+ .thenWithComparator(
+ (a, b) => Comparators.compareStringCaseInsensitive()(
+ a.filename, b.filename),
+ reversed: reversed,
+ )
+ .compare,
+ );
break;
case _SortType.time:
files.sort(
- (a, b) => (a.attr.modifyTime ?? 0).compareTo(b.attr.modifyTime ?? 0),
+ comparator
+ .thenCompareBy(
+ (x) => x.attr.modifyTime ?? 0,
+ reversed: reversed,
+ )
+ .compare,
);
break;
case _SortType.size:
- files.sort((a, b) => (a.attr.size ?? 0).compareTo(b.attr.size ?? 0));
+ files.sort(
+ comparator
+ .thenCompareBy(
+ (x) => x.attr.size ?? 0,
+ reversed: reversed,
+ )
+ .compare,
+ );
break;
}
return files;
}
}
+
+class _SortOption {
+ final _SortType sortBy;
+ final bool reversed;
+
+ _SortOption({required this.sortBy, required this.reversed});
+}