new: auto run snippet (#67)

This commit is contained in:
lollipopkit
2024-02-18 15:00:41 +08:00
parent 8bfea497a0
commit 61ddb56639
31 changed files with 221 additions and 122 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -80,4 +80,4 @@ Comparator.comparing<Type1, Type2>(Type1::getType2)
.thenCompare<Type4>(Type1::getType4)
.thenCompareReversed<Type5>(Type1::getType5)
*/
*/

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "大小",

View File

@@ -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": "大小",

View File

@@ -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),
);
}

View File

@@ -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!;
}
}
}

View File

@@ -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;

View File

@@ -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: () {