mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
Lollipopkit/issue382 (#383)
This commit is contained in:
@@ -102,12 +102,14 @@ class AppRoutes {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required ServerPrivateInfo spi,
|
required ServerPrivateInfo spi,
|
||||||
String? initCmd,
|
String? initCmd,
|
||||||
|
Snippet? initSnippet,
|
||||||
}) {
|
}) {
|
||||||
return AppRoutes(
|
return AppRoutes(
|
||||||
SSHPage(
|
SSHPage(
|
||||||
key: key,
|
key: key,
|
||||||
spi: spi,
|
spi: spi,
|
||||||
initCmd: initCmd,
|
initCmd: initCmd,
|
||||||
|
initSnippet: initSnippet,
|
||||||
),
|
),
|
||||||
'ssh_term',
|
'ssh_term',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:xterm/core.dart';
|
||||||
|
|
||||||
import '../app/tag_pickable.dart';
|
import '../app/tag_pickable.dart';
|
||||||
|
|
||||||
@@ -53,19 +57,116 @@ class Snippet implements TagPickable {
|
|||||||
@override
|
@override
|
||||||
String get tagName => name;
|
String get tagName => name;
|
||||||
|
|
||||||
String fmtWith(ServerPrivateInfo spi) {
|
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
||||||
final fmted = script.replaceAllMapped(
|
|
||||||
RegExp(r'\${.+?}'),
|
String fmtWithSpi(ServerPrivateInfo spi) {
|
||||||
|
return script.replaceAllMapped(
|
||||||
|
fmtFinder,
|
||||||
(match) {
|
(match) {
|
||||||
final key = match.group(0);
|
final key = match.group(0);
|
||||||
final func = fmtArgs[key];
|
final func = fmtArgs[key];
|
||||||
if (func == null) {
|
if (func != null) return func(spi);
|
||||||
return key!;
|
// If not found, return the original content for further processing
|
||||||
}
|
return key ?? '';
|
||||||
return func(spi);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return fmted;
|
}
|
||||||
|
|
||||||
|
Future<void> runInTerm(
|
||||||
|
Terminal terminal,
|
||||||
|
ServerPrivateInfo spi, {
|
||||||
|
bool autoEnter = false,
|
||||||
|
}) async {
|
||||||
|
final argsFmted = fmtWithSpi(spi);
|
||||||
|
final matches = fmtFinder.allMatches(argsFmted);
|
||||||
|
|
||||||
|
/// There is no [TerminalKey] in the script
|
||||||
|
if (matches.isEmpty) {
|
||||||
|
terminal.textInput(argsFmted);
|
||||||
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records all start and end indexes of the matches
|
||||||
|
final (starts, ends) = matches.fold((<int>[], <int>[]), (pre, e) {
|
||||||
|
pre.$1.add(e.start);
|
||||||
|
pre.$2.add(e.end);
|
||||||
|
return pre;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check all indexes, `(idx + 1).start` must >= `idx.end`
|
||||||
|
for (var i = 0; i < starts.length - 1; i++) {
|
||||||
|
final lastEnd = ends[i];
|
||||||
|
final nextStart = starts[i + 1];
|
||||||
|
if (nextStart < lastEnd) {
|
||||||
|
throw 'Invalid format: $nextStart < $lastEnd';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start term input
|
||||||
|
if (starts.first > 0) {
|
||||||
|
terminal.textInput(argsFmted.substring(0, starts.first));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process matched
|
||||||
|
for (var idx = 0; idx < starts.length; idx++) {
|
||||||
|
final start = starts[idx];
|
||||||
|
final end = ends[idx];
|
||||||
|
final key = argsFmted.substring(start, end).toLowerCase();
|
||||||
|
|
||||||
|
// Special funcs
|
||||||
|
final special = _find(SnippetFuncs.specialCtrl, key);
|
||||||
|
if (special != null) {
|
||||||
|
final raw = key.substring(special.key.length + 1, key.length - 1);
|
||||||
|
await special.value((term: terminal, raw: raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Term keys
|
||||||
|
final termKey = _find(fmtTermKeys, key);
|
||||||
|
if (termKey != null) await _doTermKeys(terminal, termKey, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// End term input
|
||||||
|
if (ends.last < argsFmted.length) {
|
||||||
|
terminal.textInput(argsFmted.substring(ends.last));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoEnter) terminal.keyInput(TerminalKey.enter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _doTermKeys(
|
||||||
|
Terminal terminal,
|
||||||
|
MapEntry<String, TerminalKey> termKey,
|
||||||
|
String key,
|
||||||
|
) async {
|
||||||
|
if (termKey.value == TerminalKey.enter) {
|
||||||
|
terminal.keyInput(TerminalKey.enter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ctrlAlt = switch (termKey.value) {
|
||||||
|
TerminalKey.control => (ctrl: true, alt: false),
|
||||||
|
TerminalKey.alt => (ctrl: false, alt: true),
|
||||||
|
_ => (ctrl: false, alt: false),
|
||||||
|
};
|
||||||
|
|
||||||
|
// `${ctrl+ad}` -> `ctrla + d`
|
||||||
|
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
||||||
|
if (chars.isEmpty) return;
|
||||||
|
final ok = terminal.charInput(
|
||||||
|
chars.codeUnitAt(0),
|
||||||
|
ctrl: ctrlAlt.ctrl,
|
||||||
|
alt: ctrlAlt.alt,
|
||||||
|
);
|
||||||
|
if (!ok) {
|
||||||
|
Loggers.app.warning('Failed to input: $key');
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.textInput(chars.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
MapEntry<String, T>? _find<T>(Map<String, T> map, String key) {
|
||||||
|
return map.entries.firstWhereOrNull((e) => key.startsWith(e.key));
|
||||||
}
|
}
|
||||||
|
|
||||||
static final fmtArgs = {
|
static final fmtArgs = {
|
||||||
@@ -76,6 +177,12 @@ class Snippet implements TagPickable {
|
|||||||
r'${id}': (ServerPrivateInfo spi) => spi.id,
|
r'${id}': (ServerPrivateInfo spi) => spi.id,
|
||||||
r'${name}': (ServerPrivateInfo spi) => spi.name,
|
r'${name}': (ServerPrivateInfo spi) => spi.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// r'${ctrl+ad}' -> TerminalKey.control, a, d
|
||||||
|
static final fmtTermKeys = {
|
||||||
|
r'${ctrl': TerminalKey.control,
|
||||||
|
r'${alt': TerminalKey.alt,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SnippetResult {
|
class SnippetResult {
|
||||||
@@ -89,3 +196,32 @@ class SnippetResult {
|
|||||||
required this.time,
|
required this.time,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef SnippetFuncCtx = ({Terminal term, String raw});
|
||||||
|
|
||||||
|
abstract final class SnippetFuncs {
|
||||||
|
static final specialCtrl = {
|
||||||
|
// `${sleep 3}` -> sleep 3 seconds
|
||||||
|
r'${sleep': SnippetFuncs.sleep,
|
||||||
|
r'${enter': SnippetFuncs.enter,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const help = {
|
||||||
|
'sleep': 'Sleep for a few seconds',
|
||||||
|
'enter': 'Enter a few times',
|
||||||
|
};
|
||||||
|
|
||||||
|
static FutureOr<void> sleep(SnippetFuncCtx ctx) async {
|
||||||
|
final seconds = int.tryParse(ctx.raw);
|
||||||
|
if (seconds == null) return;
|
||||||
|
final duration = Duration(seconds: seconds);
|
||||||
|
await Future.delayed(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FutureOr<void> enter(SnippetFuncCtx ctx) async {
|
||||||
|
final times = int.tryParse(ctx.raw) ?? 1;
|
||||||
|
for (var i = 0; i < times; i++) {
|
||||||
|
ctx.term.keyInput(TerminalKey.enter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import '../../core/utils/server.dart';
|
|||||||
import '../model/server/server.dart';
|
import '../model/server/server.dart';
|
||||||
import '../model/server/server_private_info.dart';
|
import '../model/server/server_private_info.dart';
|
||||||
import '../model/server/server_status_update_req.dart';
|
import '../model/server/server_status_update_req.dart';
|
||||||
import '../model/server/snippet.dart';
|
|
||||||
import '../model/server/try_limiter.dart';
|
import '../model/server/try_limiter.dart';
|
||||||
import '../res/status.dart';
|
import '../res/status.dart';
|
||||||
|
|
||||||
@@ -460,20 +459,20 @@ class ServerProvider extends ChangeNotifier {
|
|||||||
TryLimiter.reset(sid);
|
TryLimiter.reset(sid);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnippetResult?> runSnippet(String id, Snippet snippet) async {
|
// Future<SnippetResult?> runSnippet(String id, Snippet snippet) async {
|
||||||
final server = _servers[id];
|
// final server = _servers[id];
|
||||||
if (server == null) return null;
|
// if (server == null) return null;
|
||||||
final watch = Stopwatch()..start();
|
// final watch = Stopwatch()..start();
|
||||||
final result = await server.client?.run(snippet.fmtWith(server.spi)).string;
|
// final result = await server.client?.run(snippet.fmtWithArgs(server.spi)).string;
|
||||||
final time = watch.elapsed;
|
// final time = watch.elapsed;
|
||||||
watch.stop();
|
// watch.stop();
|
||||||
if (result == null) return null;
|
// if (result == null) return null;
|
||||||
return SnippetResult(
|
// return SnippetResult(
|
||||||
dest: _servers[id]?.spi.name,
|
// dest: _servers[id]?.spi.name,
|
||||||
result: result,
|
// result: result,
|
||||||
time: time,
|
// time: time,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Future<List<SnippetResult?>> runSnippetsMulti(
|
// Future<List<SnippetResult?>> runSnippetsMulti(
|
||||||
// List<String> ids,
|
// List<String> ids,
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "System verfolgen",
|
"followSystem": "System verfolgen",
|
||||||
"font": "Schriftarten",
|
"font": "Schriftarten",
|
||||||
"fontSize": "Schriftgröße",
|
"fontSize": "Schriftgröße",
|
||||||
|
"forExample": "Zum Beispiel",
|
||||||
"force": "freiwillig",
|
"force": "freiwillig",
|
||||||
"foundNUpdate": "Update {count} gefunden",
|
"foundNUpdate": "Update {count} gefunden",
|
||||||
"fullScreen": "Vollbildmodus",
|
"fullScreen": "Vollbildmodus",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "Follow system",
|
"followSystem": "Follow system",
|
||||||
"font": "Font",
|
"font": "Font",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
|
"forExample": "For example",
|
||||||
"force": "Force",
|
"force": "Force",
|
||||||
"foundNUpdate": "Found {count} update",
|
"foundNUpdate": "Found {count} update",
|
||||||
"fullScreen": "Full screen mode",
|
"fullScreen": "Full screen mode",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "Seguir al sistema",
|
"followSystem": "Seguir al sistema",
|
||||||
"font": "Fuente",
|
"font": "Fuente",
|
||||||
"fontSize": "Tamaño de fuente",
|
"fontSize": "Tamaño de fuente",
|
||||||
|
"forExample": "Por ejemplo",
|
||||||
"force": "Forzar",
|
"force": "Forzar",
|
||||||
"foundNUpdate": "Encontradas {count} actualizaciones",
|
"foundNUpdate": "Encontradas {count} actualizaciones",
|
||||||
"fullScreen": "Modo pantalla completa",
|
"fullScreen": "Modo pantalla completa",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "Suivre le système",
|
"followSystem": "Suivre le système",
|
||||||
"font": "Police",
|
"font": "Police",
|
||||||
"fontSize": "Taille de la police",
|
"fontSize": "Taille de la police",
|
||||||
|
"forExample": "Par exemple",
|
||||||
"force": "Forcer",
|
"force": "Forcer",
|
||||||
"foundNUpdate": "{count} mise à jour trouvée",
|
"foundNUpdate": "{count} mise à jour trouvée",
|
||||||
"fullScreen": "Mode plein écran",
|
"fullScreen": "Mode plein écran",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "Ikuti sistem",
|
"followSystem": "Ikuti sistem",
|
||||||
"font": "Font",
|
"font": "Font",
|
||||||
"fontSize": "Ukuran huruf",
|
"fontSize": "Ukuran huruf",
|
||||||
|
"forExample": "Sebagai contoh",
|
||||||
"force": "sukarela",
|
"force": "sukarela",
|
||||||
"foundNUpdate": "Menemukan {count} pembaruan",
|
"foundNUpdate": "Menemukan {count} pembaruan",
|
||||||
"fullScreen": "Mode Layar Penuh",
|
"fullScreen": "Mode Layar Penuh",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "システムに従う",
|
"followSystem": "システムに従う",
|
||||||
"font": "フォント",
|
"font": "フォント",
|
||||||
"fontSize": "フォントサイズ",
|
"fontSize": "フォントサイズ",
|
||||||
|
"forExample": "例えば",
|
||||||
"force": "強制",
|
"force": "強制",
|
||||||
"foundNUpdate": "{count}個の更新が見つかりました",
|
"foundNUpdate": "{count}個の更新が見つかりました",
|
||||||
"fullScreen": "フルスクリーンモード",
|
"fullScreen": "フルスクリーンモード",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "Volg systeem",
|
"followSystem": "Volg systeem",
|
||||||
"font": "Lettertype",
|
"font": "Lettertype",
|
||||||
"fontSize": "Lettergrootte",
|
"fontSize": "Lettergrootte",
|
||||||
|
"forExample": "Bijvoorbeeld",
|
||||||
"force": "Forceer",
|
"force": "Forceer",
|
||||||
"foundNUpdate": "{count} update gevonden",
|
"foundNUpdate": "{count} update gevonden",
|
||||||
"fullScreen": "Volledig schermmodus",
|
"fullScreen": "Volledig schermmodus",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "Seguir sistema",
|
"followSystem": "Seguir sistema",
|
||||||
"font": "Fonte",
|
"font": "Fonte",
|
||||||
"fontSize": "Tamanho da fonte",
|
"fontSize": "Tamanho da fonte",
|
||||||
|
"forExample": "Por exemplo",
|
||||||
"force": "Forçar",
|
"force": "Forçar",
|
||||||
"foundNUpdate": "Encontradas {count} atualizações",
|
"foundNUpdate": "Encontradas {count} atualizações",
|
||||||
"fullScreen": "Modo tela cheia",
|
"fullScreen": "Modo tela cheia",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "следовать за системой",
|
"followSystem": "следовать за системой",
|
||||||
"font": "шрифт",
|
"font": "шрифт",
|
||||||
"fontSize": "размер шрифта",
|
"fontSize": "размер шрифта",
|
||||||
|
"forExample": "Например",
|
||||||
"force": "принудительно",
|
"force": "принудительно",
|
||||||
"foundNUpdate": "Найдено {count} обновлений",
|
"foundNUpdate": "Найдено {count} обновлений",
|
||||||
"fullScreen": "полноэкранный режим",
|
"fullScreen": "полноэкранный режим",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "跟随系统",
|
"followSystem": "跟随系统",
|
||||||
"font": "字体",
|
"font": "字体",
|
||||||
"fontSize": "字体大小",
|
"fontSize": "字体大小",
|
||||||
|
"forExample": "例如",
|
||||||
"force": "强制",
|
"force": "强制",
|
||||||
"foundNUpdate": "找到 {count} 个更新",
|
"foundNUpdate": "找到 {count} 个更新",
|
||||||
"fullScreen": "全屏模式",
|
"fullScreen": "全屏模式",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"followSystem": "跟隨系統",
|
"followSystem": "跟隨系統",
|
||||||
"font": "字體",
|
"font": "字體",
|
||||||
"fontSize": "字體大小",
|
"fontSize": "字體大小",
|
||||||
|
"forExample": "例如",
|
||||||
"force": "強制",
|
"force": "強制",
|
||||||
"foundNUpdate": "找到 {count} 個更新",
|
"foundNUpdate": "找到 {count} 個更新",
|
||||||
"fullScreen": "全屏模式",
|
"fullScreen": "全屏模式",
|
||||||
|
|||||||
@@ -562,7 +562,10 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
case ContainerMenu.terminal:
|
case ContainerMenu.terminal:
|
||||||
AppRoutes.ssh(
|
AppRoutes.ssh(
|
||||||
spi: widget.spi,
|
spi: widget.spi,
|
||||||
initCmd: 'docker exec -it ${dItem.id} sh',
|
initCmd: '${switch (_container.type) {
|
||||||
|
ContainerType.podman => 'podman',
|
||||||
|
ContainerType.docker => 'docker',
|
||||||
|
}} exec -it ${dItem.id} sh',
|
||||||
).go(context);
|
).go(context);
|
||||||
break;
|
break;
|
||||||
// case DockerMenuType.stats:
|
// case DockerMenuType.stats:
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
),
|
),
|
||||||
floatingActionButton: AutoHide(
|
floatingActionButton: AutoHide(
|
||||||
key: _autoHideKey,
|
key: _autoHideKey,
|
||||||
direction: AxisDirection.right,
|
direction: AxisDirection.down,
|
||||||
|
offset: 75,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
heroTag: 'addServer',
|
heroTag: 'addServer',
|
||||||
|
|||||||
@@ -200,9 +200,13 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
|||||||
padding: const EdgeInsets.all(13),
|
padding: const EdgeInsets.all(13),
|
||||||
child: SimpleMarkdown(
|
child: SimpleMarkdown(
|
||||||
data: '''
|
data: '''
|
||||||
📌 ${l10n.supportFmtArgs}
|
📌 ${l10n.supportFmtArgs}\n
|
||||||
|
${Snippet.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
|
||||||
|
|
||||||
${Snippet.fmtArgs.keys.map((e) => '`$e`').join(', ')}
|
${Snippet.fmtTermKeys.keys.map((e) => '`$e+?}`').join(', ')}\n
|
||||||
|
${l10n.forExample}:
|
||||||
|
- `\${ctrl+c}` (Control + C)
|
||||||
|
- `\${ctrl+b}d` (Tmux Detach)
|
||||||
''',
|
''',
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
codeblockDecoration: const BoxDecoration(
|
codeblockDecoration: const BoxDecoration(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const _echoPWD = 'echo \$PWD';
|
|||||||
class SSHPage extends StatefulWidget {
|
class SSHPage extends StatefulWidget {
|
||||||
final ServerPrivateInfo spi;
|
final ServerPrivateInfo spi;
|
||||||
final String? initCmd;
|
final String? initCmd;
|
||||||
|
final Snippet? initSnippet;
|
||||||
final bool notFromTab;
|
final bool notFromTab;
|
||||||
final Function()? onSessionEnd;
|
final Function()? onSessionEnd;
|
||||||
final GlobalKey<TerminalViewState>? terminalKey;
|
final GlobalKey<TerminalViewState>? terminalKey;
|
||||||
@@ -37,6 +38,7 @@ class SSHPage extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.spi,
|
required this.spi,
|
||||||
this.initCmd,
|
this.initCmd,
|
||||||
|
this.initSnippet,
|
||||||
this.notFromTab = true,
|
this.notFromTab = true,
|
||||||
this.onSessionEnd,
|
this.onSessionEnd,
|
||||||
this.terminalKey,
|
this.terminalKey,
|
||||||
@@ -309,8 +311,7 @@ class SSHPageState extends State<SSHPage>
|
|||||||
|
|
||||||
final snippet = snippets.firstOrNull;
|
final snippet = snippets.firstOrNull;
|
||||||
if (snippet == null) return;
|
if (snippet == null) return;
|
||||||
_terminal.textInput(snippet.script);
|
snippet.runInTerm(_terminal, widget.spi);
|
||||||
_terminal.keyInput(TerminalKey.enter);
|
|
||||||
break;
|
break;
|
||||||
case VirtualKeyFunc.file:
|
case VirtualKeyFunc.file:
|
||||||
// get $PWD from SSH session
|
// get $PWD from SSH session
|
||||||
@@ -415,16 +416,19 @@ class SSHPageState extends State<SSHPage>
|
|||||||
|
|
||||||
_initService();
|
_initService();
|
||||||
|
|
||||||
|
for (final snippet in Pros.snippet.snippets) {
|
||||||
|
if (snippet.autoRunOn?.contains(widget.spi.id) == true) {
|
||||||
|
snippet.runInTerm(_terminal, widget.spi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.initCmd != null) {
|
if (widget.initCmd != null) {
|
||||||
_terminal.textInput(widget.initCmd!);
|
_terminal.textInput(widget.initCmd!);
|
||||||
_terminal.keyInput(TerminalKey.enter);
|
_terminal.keyInput(TerminalKey.enter);
|
||||||
} else {
|
}
|
||||||
for (final snippet in Pros.snippet.snippets) {
|
|
||||||
if (snippet.autoRunOn?.contains(widget.spi.id) == true) {
|
if (widget.initSnippet != null) {
|
||||||
_terminal.textInput(snippet.script);
|
widget.initSnippet!.runInTerm(_terminal, widget.spi);
|
||||||
_terminal.keyInput(TerminalKey.enter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SSHPage.focusNode.requestFocus();
|
SSHPage.focusNode.requestFocus();
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class _SSHTabPageState extends State<SSHTabPage>
|
|||||||
|
|
||||||
Widget _buildAddPage() {
|
Widget _buildAddPage() {
|
||||||
return Center(
|
return Center(
|
||||||
|
key: const Key('sshTabAddServer'),
|
||||||
child: Consumer<ServerProvider>(builder: (_, pro, __) {
|
child: Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||||
if (pro.serverOrder.isEmpty) {
|
if (pro.serverOrder.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@@ -105,21 +106,27 @@ class _SSHTabPageState extends State<SSHTabPage>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return GridView.builder(
|
||||||
padding: const EdgeInsets.all(7),
|
padding: const EdgeInsets.all(7),
|
||||||
itemBuilder: (_, idx) {
|
itemBuilder: (_, idx) {
|
||||||
final spi = Pros.server.pick(id: pro.serverOrder[idx])?.spi;
|
final spi = Pros.server.pick(id: pro.serverOrder[idx])?.spi;
|
||||||
if (spi == null) return UIs.placeholder;
|
if (spi == null) return UIs.placeholder;
|
||||||
return CardX(
|
return CardX(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.only(left: 17, right: 7),
|
||||||
title: Text(spi.name),
|
title: Text(spi.name),
|
||||||
subtitle: Text(spi.id, style: UIs.textGrey),
|
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _onTapInitCard(spi),
|
onTap: () => _onTapInitCard(spi),
|
||||||
),
|
).center(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: pro.servers.length,
|
itemCount: pro.servers.length,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
childAspectRatio: 3,
|
||||||
|
crossAxisSpacing: 3,
|
||||||
|
mainAxisSpacing: 3,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ void _onTapMoreBtns(
|
|||||||
if (snippets == null || snippets.isEmpty) return;
|
if (snippets == null || snippets.isEmpty) return;
|
||||||
final snippet = snippets.firstOrNull;
|
final snippet = snippets.firstOrNull;
|
||||||
if (snippet == null) return;
|
if (snippet == null) return;
|
||||||
final fmted = snippet.fmtWith(spi);
|
final fmted = snippet.fmtWithSpi(spi);
|
||||||
final sure = await context.showRoundDialog<bool>(
|
final sure = await context.showRoundDialog<bool>(
|
||||||
title: l10n.attention,
|
title: l10n.attention,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -141,7 +141,7 @@ void _onTapMoreBtns(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (sure != true) return;
|
if (sure != true) return;
|
||||||
AppRoutes.ssh(spi: spi, initCmd: fmted).checkGo(
|
AppRoutes.ssh(spi: spi, initSnippet: snippet).checkGo(
|
||||||
context: context,
|
context: context,
|
||||||
check: () => _checkClient(context, spi.id),
|
check: () => _checkClient(context, spi.id),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user