new: auto run snippet (#67)
This commit is contained in:
@@ -53,34 +53,41 @@ extension DialogX on BuildContext {
|
||||
|
||||
Future<List<T>?> showPickDialog<T>({
|
||||
required List<T?> items,
|
||||
required String Function(T) name,
|
||||
String Function(T)? name,
|
||||
bool multi = true,
|
||||
List<T>? initial,
|
||||
bool clearable = false,
|
||||
List<Widget>? actions,
|
||||
}) async {
|
||||
var vals = <T>[];
|
||||
var vals = initial ?? <T>[];
|
||||
final sure = await showRoundDialog<bool>(
|
||||
title: Text(l10n.choose),
|
||||
child: Choice<T>(
|
||||
onChanged: (value) => vals = value,
|
||||
multiple: multi,
|
||||
clearable: true,
|
||||
builder: (state, _) {
|
||||
return Wrap(
|
||||
children: List<Widget>.generate(
|
||||
items.length,
|
||||
(index) {
|
||||
final item = items[index];
|
||||
if (item == null) return UIs.placeholder;
|
||||
return ChoiceChipX<T>(
|
||||
label: name(item),
|
||||
state: state,
|
||||
value: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Choice<T>(
|
||||
onChanged: (value) => vals = value,
|
||||
multiple: multi,
|
||||
clearable: clearable,
|
||||
value: vals,
|
||||
builder: (state, _) {
|
||||
return Wrap(
|
||||
children: List<Widget>.generate(
|
||||
items.length,
|
||||
(index) {
|
||||
final item = items[index];
|
||||
if (item == null) return UIs.placeholder;
|
||||
return ChoiceChipX<T>(
|
||||
label: name?.call(item) ?? item.toString(),
|
||||
state: state,
|
||||
value: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (actions != null) ...actions,
|
||||
TextButton(
|
||||
onPressed: () => pop(true),
|
||||
child: Text(l10n.ok),
|
||||
@@ -95,12 +102,17 @@ extension DialogX on BuildContext {
|
||||
|
||||
Future<T?> showPickSingleDialog<T>({
|
||||
required List<T?> items,
|
||||
required String Function(T) name,
|
||||
String Function(T)? name,
|
||||
T? initial,
|
||||
bool clearable = false,
|
||||
List<Widget>? actions,
|
||||
}) async {
|
||||
final vals = await showPickDialog<T>(
|
||||
items: items,
|
||||
name: name,
|
||||
multi: false,
|
||||
initial: initial == null ? null : [initial],
|
||||
actions: actions,
|
||||
);
|
||||
if (vals != null && vals.isNotEmpty) {
|
||||
return vals.first;
|
||||
|
||||
@@ -107,7 +107,7 @@ extension SSHClientX on SSHClient {
|
||||
bool stdout = true,
|
||||
bool stderr = true,
|
||||
Map<String, String>? environment,
|
||||
Future<void> Function(SSHSession)? action,
|
||||
Future<void> Function(SSHSession)? action,
|
||||
}) async {
|
||||
final session = await execute(
|
||||
command,
|
||||
|
||||
@@ -80,4 +80,4 @@ Comparator.comparing<Type1, Type2>(Type1::getType2)
|
||||
.thenCompare<Type4>(Type1::getType4)
|
||||
.thenCompareReversed<Type5>(Type1::getType5)
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
@@ -61,4 +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;
|
||||
const isDebuggingMobileLayoutOnDesktop = kDebugMode;
|
||||
|
||||
@@ -16,18 +16,24 @@ class Snippet implements TagPickable {
|
||||
@HiveField(3)
|
||||
final String? note;
|
||||
|
||||
/// List of server id that this snippet should be auto run on
|
||||
@HiveField(4)
|
||||
final List<String>? autoRunOn;
|
||||
|
||||
const Snippet({
|
||||
required this.name,
|
||||
required this.script,
|
||||
this.tags,
|
||||
this.note,
|
||||
this.autoRunOn,
|
||||
});
|
||||
|
||||
Snippet.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'].toString(),
|
||||
script = json['script'].toString(),
|
||||
tags = json['tags']?.cast<String>(),
|
||||
note = json['note']?.toString();
|
||||
note = json['note']?.toString(),
|
||||
autoRunOn = json['autoRunOn']?.cast<String>();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final data = <String, dynamic>{};
|
||||
@@ -35,6 +41,7 @@ class Snippet implements TagPickable {
|
||||
data['script'] = script;
|
||||
data['tags'] = tags;
|
||||
data['note'] = note;
|
||||
data['autoRunOn'] = autoRunOn;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,14 @@ class SnippetAdapter extends TypeAdapter<Snippet> {
|
||||
script: fields[1] as String,
|
||||
tags: (fields[2] as List?)?.cast<String>(),
|
||||
note: fields[3] as String?,
|
||||
autoRunOn: (fields[4] as List?)?.cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Snippet obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(5)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -35,7 +36,9 @@ class SnippetAdapter extends TypeAdapter<Snippet> {
|
||||
..writeByte(2)
|
||||
..write(obj.tags)
|
||||
..writeByte(3)
|
||||
..write(obj.note);
|
||||
..write(obj.note)
|
||||
..writeByte(4)
|
||||
..write(obj.autoRunOn);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -273,10 +273,9 @@ class ServerProvider extends ChangeNotifier {
|
||||
ensure(await client.run(ShellFunc.installerMkdirs).string);
|
||||
|
||||
ensure(await client.runForOutput(ShellFunc.installerShellWriter,
|
||||
action: (session) async {
|
||||
session.stdin.add(ShellFunc.allScript.uint8List);
|
||||
})
|
||||
.string);
|
||||
action: (session) async {
|
||||
session.stdin.add(ShellFunc.allScript.uint8List);
|
||||
}).string);
|
||||
|
||||
ensure(await client.run(ShellFunc.installerPermissionModifier).string);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 746;
|
||||
static const String engine = "3.16.9";
|
||||
static const String buildAt = "2024-02-15 16:45:05";
|
||||
static const int modifications = 2;
|
||||
static const int script = 37;
|
||||
static const int build = 761;
|
||||
static const String engine = "3.19.0";
|
||||
static const String buildAt = "2024-02-18 12:48:41";
|
||||
static const int modifications = 10;
|
||||
static const int script = 38;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"autoBackupConflict": "Es kann nur eine automatische Sicherung gleichzeitig aktiviert werden.",
|
||||
"autoCheckUpdate": "Aktualisierung automatisch prüfen",
|
||||
"autoConnect": "Automatisch verbinden",
|
||||
"autoRun": "Automatischer Start",
|
||||
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
|
||||
"backup": "Backup",
|
||||
"backupTip": "Das Backup wird nur einfach verschlüsselt.\nBitte bewahre die Datei sicher auf.",
|
||||
@@ -207,8 +208,8 @@
|
||||
"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",
|
||||
"sftpShowFoldersFirst": "Ordner zuerst anzeigen",
|
||||
"showDistLogo": "Distributionslogo anzeigen",
|
||||
"shutdown": "Abschaltung",
|
||||
"size": "Größe",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"autoBackupConflict": "Only one automatic backup can be turned on at the same time.",
|
||||
"autoCheckUpdate": "Auto check update",
|
||||
"autoConnect": "Auto connect",
|
||||
"autoRun": "Automatic Run",
|
||||
"autoUpdateHomeWidget": "Auto update home widget",
|
||||
"backup": "Backup",
|
||||
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.",
|
||||
@@ -207,8 +208,8 @@
|
||||
"setting": "Settings",
|
||||
"sftpDlPrepare": "Preparing to connect...",
|
||||
"sftpRmrDirSummary": "Use `rm -r` to delete a folder in SFTP.",
|
||||
"sftpShowFoldersFirst": "Disply folders first",
|
||||
"sftpSSHConnected": "SFTP Connected",
|
||||
"sftpShowFoldersFirst": "Disply folders first",
|
||||
"showDistLogo": "Show distribution logo",
|
||||
"shutdown": "Shutdown",
|
||||
"size": "Size",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"autoBackupConflict": "Une seule sauvegarde automatique peut être activée à la fois.",
|
||||
"autoCheckUpdate": "Vérification automatique des mises à jour",
|
||||
"autoConnect": "Connexion automatique",
|
||||
"autoRun": "Exécution automatique",
|
||||
"autoUpdateHomeWidget": "Mise à jour automatique du widget d'accueil",
|
||||
"backup": "Sauvegarder",
|
||||
"backupTip": "Les données exportées sont simplement chiffrées. \nVeuillez les conserver en lieu sûr.",
|
||||
@@ -207,8 +208,8 @@
|
||||
"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é",
|
||||
"sftpShowFoldersFirst": "Dossiers d'abord lors du tri",
|
||||
"showDistLogo": "Afficher le logo de la distribution",
|
||||
"shutdown": "Éteindre",
|
||||
"size": "Taille",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"autoBackupConflict": "Hanya satu pencadangan otomatis yang dapat diaktifkan pada saat yang bersamaan.",
|
||||
"autoCheckUpdate": "Periksa pembaruan otomatis",
|
||||
"autoConnect": "Hubungkan otomatis",
|
||||
"autoRun": "Berjalan Otomatis",
|
||||
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
|
||||
"backup": "Cadangan",
|
||||
"backupTip": "Data yang diekspor hanya dienkripsi.\nTolong jaga keamanannya.",
|
||||
@@ -207,8 +208,8 @@
|
||||
"setting": "Pengaturan",
|
||||
"sftpDlPrepare": "Bersiap untuk terhubung ...",
|
||||
"sftpRmrDirSummary": "Gunakan `rm -r` untuk menghapus dir di SFTP",
|
||||
"sftpShowFoldersFirst": "Folder ditampilkan lebih dulu",
|
||||
"sftpSSHConnected": "Sftp terhubung",
|
||||
"sftpShowFoldersFirst": "Folder ditampilkan lebih dulu",
|
||||
"showDistLogo": "Tampilkan logo distribusi",
|
||||
"shutdown": "Matikan",
|
||||
"size": "Ukuran",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"autoBackupConflict": "只能同时开启一个自动备份",
|
||||
"autoCheckUpdate": "自动检查更新",
|
||||
"autoConnect": "自动连接",
|
||||
"autoRun": "自动运行",
|
||||
"autoUpdateHomeWidget": "自动更新桌面小部件",
|
||||
"backup": "备份",
|
||||
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。",
|
||||
@@ -207,8 +208,8 @@
|
||||
"setting": "设置",
|
||||
"sftpDlPrepare": "准备连接至服务器...",
|
||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 来删除文件夹",
|
||||
"sftpShowFoldersFirst": "排序时文件夹显示在前",
|
||||
"sftpSSHConnected": "SFTP 已连接...",
|
||||
"sftpShowFoldersFirst": "排序时文件夹显示在前",
|
||||
"showDistLogo": "显示发行版 Logo",
|
||||
"shutdown": "关机",
|
||||
"size": "大小",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"autoBackupConflict": "只能同時開啓壹個自動備份",
|
||||
"autoCheckUpdate": "自動檢查更新",
|
||||
"autoConnect": "自動連接",
|
||||
"autoRun": "自動運行",
|
||||
"autoUpdateHomeWidget": "自動更新桌面小部件",
|
||||
"backup": "備份",
|
||||
"backupTip": "導出的數據僅進行了簡單加密,請妥善保管。",
|
||||
@@ -207,8 +208,8 @@
|
||||
"setting": "設置",
|
||||
"sftpDlPrepare": "準備連接至服務器...",
|
||||
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除文件夾",
|
||||
"sftpShowFoldersFirst": "排序時文件夾顯示在前",
|
||||
"sftpSSHConnected": "SFTP 已連接...",
|
||||
"sftpShowFoldersFirst": "排序時文件夾顯示在前",
|
||||
"showDistLogo": "顯示發行版 Logo",
|
||||
"shutdown": "关机",
|
||||
"size": "大小",
|
||||
|
||||
@@ -855,7 +855,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
children: [
|
||||
_buildSftpRmrDir(),
|
||||
_buildSftpOpenLastPath(),
|
||||
_buildSftpShowFoldersFirst(),
|
||||
_buildSftpShowFoldersFirst(),
|
||||
].map((e) => CardX(child: e)).toList(),
|
||||
);
|
||||
}
|
||||
@@ -870,7 +870,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
|
||||
Widget _buildSftpShowFoldersFirst() {
|
||||
return ListTile(
|
||||
title: Text(l10n.sftpShowFoldersFirst),
|
||||
title: Text(l10n.sftpShowFoldersFirst),
|
||||
trailing: StoreSwitch(prop: _setting.sftpShowFoldersFirst),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
final _scriptController = TextEditingController();
|
||||
final _noteController = TextEditingController();
|
||||
final _scriptNode = FocusNode();
|
||||
|
||||
List<String> _tags = [];
|
||||
final _autoRunOn = ValueNotifier(<String>[]);
|
||||
final _tags = ValueNotifier(<String>[]);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -98,8 +98,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
final snippet = Snippet(
|
||||
name: name,
|
||||
script: script,
|
||||
tags: _tags.isEmpty ? null : _tags,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value,
|
||||
note: note.isEmpty ? null : note,
|
||||
autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value,
|
||||
);
|
||||
if (widget.snippet != null) {
|
||||
Pros.snippet.update(widget.snippet!, snippet);
|
||||
@@ -131,15 +132,20 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
label: l10n.note,
|
||||
icon: Icons.note,
|
||||
),
|
||||
TagEditor(
|
||||
tags: _tags,
|
||||
onChanged: (p0) => setState(() {
|
||||
_tags = p0;
|
||||
}),
|
||||
allTags: [...Pros.snippet.tags],
|
||||
onRenameTag: (old, n) => setState(() {
|
||||
Pros.snippet.renameTag(old, n);
|
||||
}),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _tags,
|
||||
builder: (_, vals, __) {
|
||||
return TagEditor(
|
||||
tags: _tags.value,
|
||||
onChanged: (p0) => setState(() {
|
||||
_tags.value = p0;
|
||||
}),
|
||||
allTags: [...Pros.snippet.tags],
|
||||
onRenameTag: (old, n) => setState(() {
|
||||
Pros.snippet.renameTag(old, n);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
Input(
|
||||
controller: _scriptController,
|
||||
@@ -150,11 +156,41 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
label: l10n.snippet,
|
||||
icon: Icons.code,
|
||||
),
|
||||
_buildAutoRunOn(),
|
||||
_buildTip(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAutoRunOn() {
|
||||
return CardX(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _autoRunOn,
|
||||
builder: (_, vals, __) {
|
||||
return ListTile(
|
||||
title: Text(l10n.autoRun),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
subtitle: vals.isEmpty
|
||||
? null
|
||||
: Text(
|
||||
vals.join(', '),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () async {
|
||||
final serverIds = await context.showPickDialog(
|
||||
items: Pros.server.serverOrder,
|
||||
initial: vals,
|
||||
);
|
||||
if (serverIds != null) {
|
||||
_autoRunOn.value = serverIds;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildTip() {
|
||||
return CardX(
|
||||
child: MarkdownBody(
|
||||
@@ -174,16 +210,20 @@ ${Snippet.fmtArgs.keys.map((e) => '`$e`').join(', ')}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (widget.snippet != null) {
|
||||
_nameController.text = widget.snippet!.name;
|
||||
_scriptController.text = widget.snippet!.script;
|
||||
if (widget.snippet!.note != null) {
|
||||
_noteController.text = widget.snippet!.note!;
|
||||
final snippet = widget.snippet;
|
||||
if (snippet != null) {
|
||||
_nameController.text = snippet.name;
|
||||
_scriptController.text = snippet.script;
|
||||
if (snippet.note != null) {
|
||||
_noteController.text = snippet.note!;
|
||||
}
|
||||
|
||||
if (widget.snippet!.tags != null) {
|
||||
_tags = widget.snippet!.tags!;
|
||||
setState(() {});
|
||||
if (snippet.tags != null) {
|
||||
_tags.value = snippet.tags!;
|
||||
}
|
||||
|
||||
if (snippet.autoRunOn != null) {
|
||||
_autoRunOn.value = snippet.autoRunOn!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +382,13 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
if (widget.initCmd != null) {
|
||||
_terminal.textInput(widget.initCmd!);
|
||||
_terminal.keyInput(TerminalKey.enter);
|
||||
} else {
|
||||
for (final snippet in Pros.snippet.snippets) {
|
||||
if (snippet.autoRunOn?.contains(widget.spi.id) == true) {
|
||||
_terminal.textInput(snippet.script);
|
||||
_terminal.keyInput(TerminalKey.enter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await session.done;
|
||||
|
||||
@@ -59,7 +59,8 @@ class _TagEditorState extends State<TagEditor> {
|
||||
Widget build(BuildContext context) {
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.tag),
|
||||
// Align the place of TextField.prefixIcon
|
||||
leading: const Icon(Icons.tag).padding(const EdgeInsets.only(left: 6)),
|
||||
title: _buildTags(widget.tags),
|
||||
trailing: const Icon(Icons.add).tap(
|
||||
onTap: () {
|
||||
|
||||
Reference in New Issue
Block a user