mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
Merge branch 'main' of github.com:lollipopkit/flutter_server_box
This commit is contained in:
@@ -1342,6 +1342,12 @@ abstract class S {
|
|||||||
/// **'Use `rm -r` to delete a folder in SFTP.'**
|
/// **'Use `rm -r` to delete a folder in SFTP.'**
|
||||||
String get sftpRmrDirSummary;
|
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.
|
/// No description provided for @sftpSSHConnected.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -651,6 +651,9 @@ class SDe extends S {
|
|||||||
@override
|
@override
|
||||||
String get sftpRmrDirSummary => 'Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.';
|
String get sftpRmrDirSummary => 'Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sftpShowFoldersFirst => 'Ordner zuerst anzeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP Verbunden';
|
String get sftpSSHConnected => 'SFTP Verbunden';
|
||||||
|
|
||||||
|
|||||||
@@ -651,6 +651,9 @@ class SEn extends S {
|
|||||||
@override
|
@override
|
||||||
String get sftpRmrDirSummary => 'Use `rm -r` to delete a folder in SFTP.';
|
String get sftpRmrDirSummary => 'Use `rm -r` to delete a folder in SFTP.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sftpShowFoldersFirst => 'Disply folders first';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP Connected';
|
String get sftpSSHConnected => 'SFTP Connected';
|
||||||
|
|
||||||
|
|||||||
@@ -651,6 +651,9 @@ class SFr extends S {
|
|||||||
@override
|
@override
|
||||||
String get sftpRmrDirSummary => 'Utilisez `rm -r` pour supprimer un dossier dans SFTP.';
|
String get sftpRmrDirSummary => 'Utilisez `rm -r` pour supprimer un dossier dans SFTP.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sftpShowFoldersFirst => 'Dossiers d\'abord lors du tri';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP connecté';
|
String get sftpSSHConnected => 'SFTP connecté';
|
||||||
|
|
||||||
|
|||||||
@@ -651,6 +651,9 @@ class SId extends S {
|
|||||||
@override
|
@override
|
||||||
String get sftpRmrDirSummary => 'Gunakan `rm -r` untuk menghapus dir di SFTP';
|
String get sftpRmrDirSummary => 'Gunakan `rm -r` untuk menghapus dir di SFTP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sftpShowFoldersFirst => 'Folder ditampilkan lebih dulu';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'Sftp terhubung';
|
String get sftpSSHConnected => 'Sftp terhubung';
|
||||||
|
|
||||||
|
|||||||
@@ -651,6 +651,9 @@ class SZh extends S {
|
|||||||
@override
|
@override
|
||||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
|
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 来删除文件夹';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sftpShowFoldersFirst => '排序时文件夹显示在前';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP 已连接...';
|
String get sftpSSHConnected => 'SFTP 已连接...';
|
||||||
|
|
||||||
@@ -1502,6 +1505,9 @@ class SZhTw extends SZh {
|
|||||||
@override
|
@override
|
||||||
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除文件夾';
|
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除文件夾';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sftpShowFoldersFirst => '排序時文件夾顯示在前';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sftpSSHConnected => 'SFTP 已連接...';
|
String get sftpSSHConnected => 'SFTP 已連接...';
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="ServerBox"
|
android:label="ServerBox"
|
||||||
|
|||||||
@@ -9,26 +9,31 @@ import 'package:toolbox/core/extension/uint8list.dart';
|
|||||||
|
|
||||||
import '../../data/res/misc.dart';
|
import '../../data/res/misc.dart';
|
||||||
|
|
||||||
typedef _OnStdout = void Function(String data, StreamSink<Uint8List> sink);
|
typedef _OnStdout = void Function(String data, SSHSession session);
|
||||||
typedef _OnStdin = void Function(StreamSink<Uint8List> sink);
|
typedef _OnStdin = void Function(SSHSession session);
|
||||||
|
|
||||||
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
typedef PwdRequestFunc = Future<String?> Function(String? user);
|
||||||
|
|
||||||
extension SSHClientX on SSHClient {
|
extension SSHClientX on SSHClient {
|
||||||
Future<int?> exec(
|
Future<SSHSession> exec(
|
||||||
String cmd, {
|
String cmd, {
|
||||||
_OnStdout? onStderr,
|
_OnStdout? onStderr,
|
||||||
_OnStdout? onStdout,
|
_OnStdout? onStdout,
|
||||||
_OnStdin? stdin,
|
_OnStdin? stdin,
|
||||||
|
bool redirectToBash = false, // not working yet. do not use
|
||||||
}) async {
|
}) 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<void>();
|
final stdoutDone = Completer<void>();
|
||||||
final stderrDone = Completer<void>();
|
final stderrDone = Completer<void>();
|
||||||
|
|
||||||
if (onStdout != null) {
|
if (onStdout != null) {
|
||||||
session.stdout.listen(
|
session.stdout.listen(
|
||||||
(e) => onStdout(e.string, session.stdin),
|
(e) => onStdout(e.string, session),
|
||||||
onDone: stdoutDone.complete,
|
onDone: stdoutDone.complete,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -37,7 +42,7 @@ extension SSHClientX on SSHClient {
|
|||||||
|
|
||||||
if (onStderr != null) {
|
if (onStderr != null) {
|
||||||
session.stderr.listen(
|
session.stderr.listen(
|
||||||
(e) => onStderr(e.string, session.stdin),
|
(e) => onStderr(e.string, session),
|
||||||
onDone: stderrDone.complete,
|
onDone: stderrDone.complete,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -45,14 +50,14 @@ extension SSHClientX on SSHClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stdin != null) {
|
if (stdin != null) {
|
||||||
stdin(session.stdin);
|
stdin(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
await stdoutDone.future;
|
await stdoutDone.future;
|
||||||
await stderrDone.future;
|
await stderrDone.future;
|
||||||
|
|
||||||
session.close();
|
session.close();
|
||||||
return session.exitCode;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int?> execWithPwd(
|
Future<int?> execWithPwd(
|
||||||
@@ -61,38 +66,48 @@ extension SSHClientX on SSHClient {
|
|||||||
_OnStdout? onStdout,
|
_OnStdout? onStdout,
|
||||||
_OnStdout? onStderr,
|
_OnStdout? onStderr,
|
||||||
_OnStdin? stdin,
|
_OnStdin? stdin,
|
||||||
|
bool redirectToBash = false, // not working yet. do not use
|
||||||
}) async {
|
}) async {
|
||||||
var isRequestingPwd = false;
|
var isRequestingPwd = false;
|
||||||
return await exec(
|
final session = await exec(
|
||||||
cmd,
|
cmd,
|
||||||
onStderr: (data, sink) async {
|
redirectToBash: redirectToBash,
|
||||||
onStderr?.call(data, sink);
|
onStderr: (data, session) async {
|
||||||
|
debugPrint(
|
||||||
|
" --- execWithPwd stderr (reqPwd = $isRequestingPwd) --- $data");
|
||||||
|
onStderr?.call(data, session);
|
||||||
if (isRequestingPwd) return;
|
if (isRequestingPwd) return;
|
||||||
isRequestingPwd = true;
|
|
||||||
if (data.contains('[sudo] password for ')) {
|
if (data.contains('[sudo] password for ')) {
|
||||||
|
isRequestingPwd = true;
|
||||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
final pwd = await context.showPwdDialog(user);
|
final pwd = await context.showPwdDialog(user);
|
||||||
if (pwd == null || pwd.isEmpty) {
|
if (pwd == null || pwd.isEmpty) {
|
||||||
// Add ctrl + c to exit.
|
debugPrint("Empty pwd. Exiting");
|
||||||
sink.add('\x03'.uint8List);
|
session.kill(SSHSignal.INT);
|
||||||
} else {
|
} 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,
|
stdin: stdin,
|
||||||
);
|
);
|
||||||
|
return session.exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> runWithSessionAction(
|
Future<Uint8List> runForOutput(
|
||||||
String command, {
|
String command, {
|
||||||
bool runInPty = false,
|
bool runInPty = false,
|
||||||
bool stdout = true,
|
bool stdout = true,
|
||||||
bool stderr = true,
|
bool stderr = true,
|
||||||
Map<String, String>? environment,
|
Map<String, String>? environment,
|
||||||
Future<void> Function(SSHSession)? action,
|
Future<void> Function(SSHSession)? action,
|
||||||
}) async {
|
}) async {
|
||||||
final session = await execute(
|
final session = await execute(
|
||||||
command,
|
command,
|
||||||
|
|||||||
83
lib/core/utils/comparator.dart
Normal file
83
lib/core/utils/comparator.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
class ChainComparator<T> {
|
||||||
|
final ChainComparator<T>? _parent;
|
||||||
|
final Comparator<T> _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<T> comparing<T, F extends Comparable<F>>(
|
||||||
|
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<T> thenCompareBy<F extends Comparable<F>>(
|
||||||
|
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<T> thenWithComparator(Comparator<T> comparator,
|
||||||
|
{bool reversed = false}) {
|
||||||
|
return ChainComparator._create(
|
||||||
|
this,
|
||||||
|
!reversed ? comparator : (a, b) => comparator(b, a),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChainComparator<T> thenCompareByReversed<F extends Comparable<F>>(
|
||||||
|
F Function(T) extractor) {
|
||||||
|
return ChainComparator._create(
|
||||||
|
this, (a, b) => -extractor(a).compareTo(extractor(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
ChainComparator<T> 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<T> reversed() {
|
||||||
|
return ChainComparator._create(null, (a, b) => this.compare(b, a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Comparators {
|
||||||
|
static Comparator<String> 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, Type2>(Type1::getType2)
|
||||||
|
.thenCompare<Type3>(Type1::getType3)
|
||||||
|
.thenCompare<Type4>(Type1::getType4)
|
||||||
|
.thenCompareReversed<Type5>(Type1::getType5)
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -61,3 +61,4 @@ final isWeb = OS.type == OS.web;
|
|||||||
final isMobile = OS.type == OS.ios || OS.type == OS.android;
|
final isMobile = OS.type == OS.ios || OS.type == OS.android;
|
||||||
final isDesktop =
|
final isDesktop =
|
||||||
OS.type == OS.linux || OS.type == OS.macos || OS.type == OS.windows;
|
OS.type == OS.linux || OS.type == OS.macos || OS.type == OS.windows;
|
||||||
|
const isDebuggingMobileLayoutOnDesktop = kDebugMode;
|
||||||
@@ -272,7 +272,7 @@ class ServerProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
ensure(await client.run(ShellFunc.installerMkdirs).string);
|
ensure(await client.run(ShellFunc.installerMkdirs).string);
|
||||||
|
|
||||||
ensure(await client.runWithSessionAction(ShellFunc.installerShellWriter,
|
ensure(await client.runForOutput(ShellFunc.installerShellWriter,
|
||||||
action: (session) async {
|
action: (session) async {
|
||||||
session.stdin.add(ShellFunc.allScript.uint8List);
|
session.stdin.add(ShellFunc.allScript.uint8List);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ class SettingStore extends PersistentStore {
|
|||||||
/// Open SFTP with last viewed path
|
/// Open SFTP with last viewed path
|
||||||
late final sftpOpenLastPath = property('sftpOpenLastPath', true);
|
late final sftpOpenLastPath = property('sftpOpenLastPath', true);
|
||||||
|
|
||||||
|
/// Show folders first in SFTP file browser
|
||||||
|
late final sftpShowFoldersFirst = property('sftpShowFoldersFirst', true);
|
||||||
|
|
||||||
/// Show tip of suspend
|
/// Show tip of suspend
|
||||||
late final showSuspendTip = property('showSuspendTip', true);
|
late final showSuspendTip = property('showSuspendTip', true);
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"setting": "Einstellungen",
|
"setting": "Einstellungen",
|
||||||
"sftpDlPrepare": "Verbindung vorbereiten...",
|
"sftpDlPrepare": "Verbindung vorbereiten...",
|
||||||
"sftpRmrDirSummary": "Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.",
|
"sftpRmrDirSummary": "Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.",
|
||||||
|
"sftpShowFoldersFirst": "Ordner zuerst anzeigen",
|
||||||
"sftpSSHConnected": "SFTP Verbunden",
|
"sftpSSHConnected": "SFTP Verbunden",
|
||||||
"showDistLogo": "Distributionslogo anzeigen",
|
"showDistLogo": "Distributionslogo anzeigen",
|
||||||
"shutdown": "Abschaltung",
|
"shutdown": "Abschaltung",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"setting": "Settings",
|
"setting": "Settings",
|
||||||
"sftpDlPrepare": "Preparing to connect...",
|
"sftpDlPrepare": "Preparing to connect...",
|
||||||
"sftpRmrDirSummary": "Use `rm -r` to delete a folder in SFTP.",
|
"sftpRmrDirSummary": "Use `rm -r` to delete a folder in SFTP.",
|
||||||
|
"sftpShowFoldersFirst": "Disply folders first",
|
||||||
"sftpSSHConnected": "SFTP Connected",
|
"sftpSSHConnected": "SFTP Connected",
|
||||||
"showDistLogo": "Show distribution logo",
|
"showDistLogo": "Show distribution logo",
|
||||||
"shutdown": "Shutdown",
|
"shutdown": "Shutdown",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"setting": "Paramètres",
|
"setting": "Paramètres",
|
||||||
"sftpDlPrepare": "Préparation de la connexion...",
|
"sftpDlPrepare": "Préparation de la connexion...",
|
||||||
"sftpRmrDirSummary": "Utilisez `rm -r` pour supprimer un dossier dans SFTP.",
|
"sftpRmrDirSummary": "Utilisez `rm -r` pour supprimer un dossier dans SFTP.",
|
||||||
|
"sftpShowFoldersFirst": "Dossiers d'abord lors du tri",
|
||||||
"sftpSSHConnected": "SFTP connecté",
|
"sftpSSHConnected": "SFTP connecté",
|
||||||
"showDistLogo": "Afficher le logo de la distribution",
|
"showDistLogo": "Afficher le logo de la distribution",
|
||||||
"shutdown": "Éteindre",
|
"shutdown": "Éteindre",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"setting": "Pengaturan",
|
"setting": "Pengaturan",
|
||||||
"sftpDlPrepare": "Bersiap untuk terhubung ...",
|
"sftpDlPrepare": "Bersiap untuk terhubung ...",
|
||||||
"sftpRmrDirSummary": "Gunakan `rm -r` untuk menghapus dir di SFTP",
|
"sftpRmrDirSummary": "Gunakan `rm -r` untuk menghapus dir di SFTP",
|
||||||
|
"sftpShowFoldersFirst": "Folder ditampilkan lebih dulu",
|
||||||
"sftpSSHConnected": "Sftp terhubung",
|
"sftpSSHConnected": "Sftp terhubung",
|
||||||
"showDistLogo": "Tampilkan logo distribusi",
|
"showDistLogo": "Tampilkan logo distribusi",
|
||||||
"shutdown": "Matikan",
|
"shutdown": "Matikan",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"setting": "设置",
|
"setting": "设置",
|
||||||
"sftpDlPrepare": "准备连接至服务器...",
|
"sftpDlPrepare": "准备连接至服务器...",
|
||||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
|
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
|
||||||
|
"sftpShowFoldersFirst": "排序时文件夹显示在前",
|
||||||
"sftpSSHConnected": "SFTP 已连接...",
|
"sftpSSHConnected": "SFTP 已连接...",
|
||||||
"showDistLogo": "显示发行版 Logo",
|
"showDistLogo": "显示发行版 Logo",
|
||||||
"shutdown": "关机",
|
"shutdown": "关机",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"setting": "設置",
|
"setting": "設置",
|
||||||
"sftpDlPrepare": "準備連接至服務器...",
|
"sftpDlPrepare": "準備連接至服務器...",
|
||||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除文件夾",
|
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除文件夾",
|
||||||
|
"sftpShowFoldersFirst": "排序時文件夾顯示在前",
|
||||||
"sftpSSHConnected": "SFTP 已連接...",
|
"sftpSSHConnected": "SFTP 已連接...",
|
||||||
"showDistLogo": "顯示發行版 Logo",
|
"showDistLogo": "顯示發行版 Logo",
|
||||||
"shutdown": "关机",
|
"shutdown": "关机",
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
late final List<Widget> children;
|
late final List<Widget> children;
|
||||||
if (srv.state == ServerState.finished) {
|
if (srv.state == ServerState.finished) {
|
||||||
if (cardStatus.value.flip) {
|
if (cardStatus.value.flip) {
|
||||||
children = [title, ..._buildFlipedCard(srv)];
|
children = [title, ..._buildFlippedCard(srv)];
|
||||||
} else {
|
} else {
|
||||||
children = [title, ..._buildNormalCard(srv.status, srv.spi)];
|
children = [title, ..._buildNormalCard(srv.status, srv.spi)];
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildFlipedCard(Server srv) {
|
List<Widget> _buildFlippedCard(Server srv) {
|
||||||
return [
|
return [
|
||||||
UIs.height13,
|
UIs.height13,
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -855,6 +855,7 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildSftpRmrDir(),
|
_buildSftpRmrDir(),
|
||||||
_buildSftpOpenLastPath(),
|
_buildSftpOpenLastPath(),
|
||||||
|
_buildSftpShowFoldersFirst(),
|
||||||
].map((e) => CardX(child: e)).toList(),
|
].map((e) => CardX(child: e)).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -867,6 +868,13 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSftpShowFoldersFirst() {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(l10n.sftpShowFoldersFirst),
|
||||||
|
trailing: StoreSwitch(prop: _setting.sftpShowFoldersFirst),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildNetViewType() {
|
Widget _buildNetViewType() {
|
||||||
final items = NetViewType.values
|
final items = NetViewType.values
|
||||||
.map((e) => PopupMenuItem(
|
.map((e) => PopupMenuItem(
|
||||||
|
|||||||
@@ -33,11 +33,14 @@ class SSHPage extends StatefulWidget {
|
|||||||
final ServerPrivateInfo spi;
|
final ServerPrivateInfo spi;
|
||||||
final String? initCmd;
|
final String? initCmd;
|
||||||
final bool pop;
|
final bool pop;
|
||||||
|
final Function()? onSessionEnd;
|
||||||
|
|
||||||
const SSHPage({
|
const SSHPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.spi,
|
required this.spi,
|
||||||
this.initCmd,
|
this.initCmd,
|
||||||
this.pop = true,
|
this.pop = true,
|
||||||
|
this.onSessionEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -103,7 +106,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
|||||||
_terminalTheme = _isDark ? TerminalThemes.dark : TerminalThemes.light;
|
_terminalTheme = _isDark ? TerminalThemes.dark : TerminalThemes.light;
|
||||||
|
|
||||||
// Because the virtual keyboard only displayed on mobile devices
|
// Because the virtual keyboard only displayed on mobile devices
|
||||||
if (isMobile) {
|
if (isMobile || isDebuggingMobileLayoutOnDesktop) {
|
||||||
_virtKeyWidth = _media.size.width / 7;
|
_virtKeyWidth = _media.size.width / 7;
|
||||||
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
|
_virtKeysHeight = _media.size.height * 0.043 * _virtKeysList.length;
|
||||||
}
|
}
|
||||||
@@ -115,7 +118,9 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
|||||||
Widget child = Scaffold(
|
Widget child = Scaffold(
|
||||||
backgroundColor: _terminalTheme.background,
|
backgroundColor: _terminalTheme.background,
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
bottomNavigationBar: isDesktop ? null : _buildBottom(),
|
bottomNavigationBar: isDesktop && !isDebuggingMobileLayoutOnDesktop
|
||||||
|
? null
|
||||||
|
: _buildBottom(),
|
||||||
);
|
);
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
child = AnnotatedRegion(
|
child = AnnotatedRegion(
|
||||||
@@ -229,10 +234,12 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
void _doVirtualKey(VirtKey item) {
|
void _doVirtualKey(VirtKey item) {
|
||||||
if (item.func != null) {
|
if (item.func != null) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
_doVirtualKeyFunc(item.func!);
|
_doVirtualKeyFunc(item.func!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.key != null) {
|
if (item.key != null) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
_doVirtualKeyInput(item.key!);
|
_doVirtualKeyInput(item.key!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,6 +388,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
|||||||
if (mounted && widget.pop) {
|
if (mounted && widget.pop) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
widget.onSessionEnd?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listen(Stream<Uint8List>? stream) {
|
void _listen(Stream<Uint8List>? stream) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class _SSHTabPageState extends State<SSHTabPage>
|
|||||||
if (confirm != true) {
|
if (confirm != true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// debugPrint("Removing a tab whose tabId = $e");
|
||||||
_tabIds.remove(e);
|
_tabIds.remove(e);
|
||||||
_refreshTabs();
|
_refreshTabs();
|
||||||
},
|
},
|
||||||
@@ -104,6 +105,12 @@ class _SSHTabPageState extends State<SSHTabPage>
|
|||||||
key: key,
|
key: key,
|
||||||
spi: spi,
|
spi: spi,
|
||||||
pop: false,
|
pop: false,
|
||||||
|
onSessionEnd: () {
|
||||||
|
// debugPrint("Session done received on page whose tabId = $name");
|
||||||
|
// debugPrint("key = $key");
|
||||||
|
_tabIds.remove(name);
|
||||||
|
_refreshTabs();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
_refreshTabs();
|
_refreshTabs();
|
||||||
_tabController.animateTo(_tabIds.length - 1);
|
_tabController.animateTo(_tabIds.length - 1);
|
||||||
|
|||||||
@@ -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/locale.dart';
|
||||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||||
import 'package:toolbox/core/extension/sftpfile.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/core/utils/platform/base.dart';
|
||||||
|
import 'package:toolbox/data/res/color.dart';
|
||||||
import 'package:toolbox/data/res/logger.dart';
|
import 'package:toolbox/data/res/logger.dart';
|
||||||
import 'package:toolbox/data/res/misc.dart';
|
import 'package:toolbox/data/res/misc.dart';
|
||||||
import 'package:toolbox/data/res/provider.dart';
|
import 'package:toolbox/data/res/provider.dart';
|
||||||
@@ -50,7 +52,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
final _status = SftpBrowserStatus();
|
final _status = SftpBrowserStatus();
|
||||||
late final _client = widget.spi.server?.client;
|
late final _client = widget.spi.server?.client;
|
||||||
|
|
||||||
final _sortType = ValueNotifier(_SortType.name);
|
final _sortOption =
|
||||||
|
ValueNotifier(_SortOption(sortBy: _SortType.name, reversed: false));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -69,29 +72,47 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
icon: const Icon(Icons.downloading),
|
icon: const Icon(Icons.downloading),
|
||||||
onPressed: () => AppRoute.sftpMission().go(context),
|
onPressed: () => AppRoute.sftpMission().go(context),
|
||||||
),
|
),
|
||||||
ValueListenableBuilder<_SortType>(
|
ValueListenableBuilder(
|
||||||
valueListenable: _sortType,
|
valueListenable: _sortOption,
|
||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
return PopupMenuButton<_SortType>(
|
return PopupMenuButton<_SortType>(
|
||||||
icon: const Icon(Icons.sort),
|
icon: const Icon(Icons.sort),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
final currentSelectedOption = _sortOption.value;
|
||||||
PopupMenuItem(
|
final options = [
|
||||||
value: _SortType.name,
|
(_SortType.name, l10n.name),
|
||||||
child: Text(l10n.name),
|
(_SortType.size, l10n.size),
|
||||||
),
|
(_SortType.time, l10n.time),
|
||||||
PopupMenuItem(
|
|
||||||
value: _SortType.size,
|
|
||||||
child: Text(l10n.size),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: _SortType.time,
|
|
||||||
child: Text(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) {
|
onSelected: (sortBy) {
|
||||||
_sortType.value = value;
|
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<SftpPage> with AfterLayoutMixin {
|
|||||||
child: FadeIn(
|
child: FadeIn(
|
||||||
key: Key(widget.spi.name + _status.path!.path),
|
key: Key(widget.spi.name + _status.path!.path),
|
||||||
child: ValueListenableBuilder(
|
child: ValueListenableBuilder(
|
||||||
valueListenable: _sortType,
|
valueListenable: _sortOption,
|
||||||
builder: (_, sortType, __) {
|
builder: (_, sortOption, __) {
|
||||||
final files = sortType.sort(_status.files!);
|
final files = sortOption.sortBy
|
||||||
|
.sort(_status.files!, reversed: sortOption.reversed);
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: files.length,
|
itemCount: files.length,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||||
@@ -797,20 +819,51 @@ enum _SortType {
|
|||||||
size,
|
size,
|
||||||
;
|
;
|
||||||
|
|
||||||
List<SftpName> sort(List<SftpName> files) {
|
List<SftpName> sort(List<SftpName> files, {bool reversed = false}) {
|
||||||
|
var comparator = ChainComparator<SftpName>.create();
|
||||||
|
if (Stores.setting.sftpShowFoldersFirst.fetch()) {
|
||||||
|
comparator = comparator.thenTrueFirst((x) => x.attr.isDirectory);
|
||||||
|
}
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case _SortType.name:
|
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;
|
break;
|
||||||
case _SortType.time:
|
case _SortType.time:
|
||||||
files.sort(
|
files.sort(
|
||||||
(a, b) => (a.attr.modifyTime ?? 0).compareTo(b.attr.modifyTime ?? 0),
|
comparator
|
||||||
|
.thenCompareBy<num>(
|
||||||
|
(x) => x.attr.modifyTime ?? 0,
|
||||||
|
reversed: reversed,
|
||||||
|
)
|
||||||
|
.compare,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _SortType.size:
|
case _SortType.size:
|
||||||
files.sort((a, b) => (a.attr.size ?? 0).compareTo(b.attr.size ?? 0));
|
files.sort(
|
||||||
|
comparator
|
||||||
|
.thenCompareBy<num>(
|
||||||
|
(x) => x.attr.size ?? 0,
|
||||||
|
reversed: reversed,
|
||||||
|
)
|
||||||
|
.compare,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SortOption {
|
||||||
|
final _SortType sortBy;
|
||||||
|
final bool reversed;
|
||||||
|
|
||||||
|
_SortOption({required this.sortBy, required this.reversed});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user