mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 08:30:29 +00:00
feat: support post-task script hooks (#1265)
This commit is contained in:
1 parent
3eb6df55df
commit
1b1631b41a
20 files changed
+911
No files matched your search
@@ -10,6 +10,7 @@ class DownloaderConfig {
|
||||
ExtraConfig extra = ExtraConfig();
|
||||
ProxyConfig proxy = ProxyConfig();
|
||||
WebhookConfig webhook = WebhookConfig();
|
||||
ScriptConfig script = ScriptConfig();
|
||||
AutoTorrentConfig autoTorrent = AutoTorrentConfig();
|
||||
ArchiveConfig archive = ArchiveConfig();
|
||||
bool autoDeleteMissingFileTasks;
|
||||
@@ -149,6 +150,22 @@ class WebhookConfig {
|
||||
Map<String, dynamic> toJson() => _$WebhookConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ScriptConfig {
|
||||
bool enable;
|
||||
List<String> paths;
|
||||
|
||||
ScriptConfig({
|
||||
this.enable = false,
|
||||
this.paths = const [],
|
||||
});
|
||||
|
||||
factory ScriptConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? ScriptConfig() : _$ScriptConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ScriptConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ProxyConfig {
|
||||
bool enable;
|
||||
|
||||
@@ -19,6 +19,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>)
|
||||
..webhook =
|
||||
WebhookConfig.fromJson(json['webhook'] as Map<String, dynamic>?)
|
||||
..script =
|
||||
ScriptConfig.fromJson(json['script'] as Map<String, dynamic>?)
|
||||
..autoTorrent = AutoTorrentConfig.fromJson(
|
||||
json['autoTorrent'] as Map<String, dynamic>?)
|
||||
..archive =
|
||||
@@ -32,6 +34,7 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
'extra': instance.extra.toJson(),
|
||||
'proxy': instance.proxy.toJson(),
|
||||
'webhook': instance.webhook.toJson(),
|
||||
'script': instance.script.toJson(),
|
||||
'autoTorrent': instance.autoTorrent.toJson(),
|
||||
'archive': instance.archive.toJson(),
|
||||
'autoDeleteMissingFileTasks': instance.autoDeleteMissingFileTasks,
|
||||
@@ -153,6 +156,20 @@ Map<String, dynamic> _$WebhookConfigToJson(WebhookConfig instance) =>
|
||||
'urls': instance.urls,
|
||||
};
|
||||
|
||||
ScriptConfig _$ScriptConfigFromJson(Map<String, dynamic> json) =>
|
||||
ScriptConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
paths:
|
||||
(json['paths'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptConfigToJson(ScriptConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'paths': instance.paths,
|
||||
};
|
||||
|
||||
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
system: json['system'] as bool? ?? false,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -1432,6 +1433,92 @@ class SettingView extends GetView<SettingController> {
|
||||
},
|
||||
);
|
||||
|
||||
// advanced config script items
|
||||
final buildScript = _buildConfigItem(
|
||||
'script',
|
||||
() => downloaderCfg.value.script.enable ? 'on'.tr : 'off'.tr,
|
||||
(Key key) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'scriptEnable'.tr,
|
||||
style: Theme.of(Get.context!).textTheme.bodyMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: downloaderCfg.value.script.enable,
|
||||
onChanged: (value) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.script.enable = value;
|
||||
});
|
||||
debounceSave();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
_padding,
|
||||
Text(
|
||||
'scriptDesc'.tr,
|
||||
style: Theme.of(Get.context!).textTheme.bodySmall,
|
||||
),
|
||||
_padding,
|
||||
// List of existing script paths
|
||||
...downloaderCfg.value.script.paths.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final path = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
path,
|
||||
style: Theme.of(Get.context!).textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
tooltip: 'edit'.tr,
|
||||
onPressed: () {
|
||||
_showScriptDialog(index: index, initialPath: path);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20),
|
||||
tooltip: 'delete'.tr,
|
||||
onPressed: () async {
|
||||
// Create new list to avoid unmodifiable list error
|
||||
final paths =
|
||||
List<String>.from(downloaderCfg.value.script.paths);
|
||||
paths.removeAt(index);
|
||||
downloaderCfg.update((val) {
|
||||
val!.script.paths = paths;
|
||||
});
|
||||
await debounceSave();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
_padding,
|
||||
// Add button
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_showScriptDialog();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text('add'.tr),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// advanced config log items start
|
||||
buildLogsDir() {
|
||||
return ListTile(
|
||||
@@ -1603,6 +1690,7 @@ class SettingView extends GetView<SettingController> {
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildWebhook(),
|
||||
if (Util.isDesktop()) buildScript(),
|
||||
buildLogsDir(),
|
||||
]),
|
||||
)),
|
||||
@@ -1734,6 +1822,104 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showScriptDialog({int? index, String? initialPath}) {
|
||||
final pathController = TextEditingController(text: initialPath ?? '');
|
||||
final saveController = TextButtonLoadingController();
|
||||
final appController = Get.find<AppController>();
|
||||
final downloaderCfg = appController.downloaderConfig;
|
||||
final isEdit = index != null;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(isEdit ? 'edit'.tr : 'add'.tr),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: pathController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'scriptPathHint'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'required'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
if (Util.isDesktop())
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
final filePath = result.files.first.path;
|
||||
if (filePath != null) {
|
||||
pathController.text = filePath;
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButtonLoading(
|
||||
controller: saveController,
|
||||
onPressed: () async {
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
final path = pathController.text.trim();
|
||||
if (path.isEmpty) return;
|
||||
|
||||
saveController.start();
|
||||
try {
|
||||
// Create new list to avoid unmodifiable list error
|
||||
final paths =
|
||||
List<String>.from(downloaderCfg.value.script.paths);
|
||||
if (isEdit) {
|
||||
paths[index] = path;
|
||||
} else {
|
||||
paths.add(path);
|
||||
}
|
||||
downloaderCfg.update((val) {
|
||||
val!.script.paths = paths;
|
||||
});
|
||||
await appController.saveConfig();
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
saveController.stop();
|
||||
}
|
||||
},
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showGithubMirrorDialog({int? index, GithubMirror? initialMirror}) {
|
||||
final isEdit = index != null;
|
||||
GithubMirrorType selectedType =
|
||||
|
||||
@@ -80,6 +80,10 @@ const enUS = {
|
||||
'webhookTest': 'Test',
|
||||
'webhookTestSuccess': 'Webhook test successful',
|
||||
'webhookTestFail': 'Webhook test failed',
|
||||
'script': 'Script',
|
||||
'scriptEnable': 'Enable Script',
|
||||
'scriptDesc': 'Execute custom scripts when downloads complete successfully',
|
||||
'scriptPathHint': 'Enter script file path (e.g., /path/to/script.sh)',
|
||||
'urlInvalid': 'Please enter a valid HTTP or HTTPS URL',
|
||||
'required': 'This field is required',
|
||||
'show': 'Show',
|
||||
|
||||
@@ -78,6 +78,10 @@ const zhCN = {
|
||||
'webhookTest': '测试',
|
||||
'webhookTestSuccess': 'Webhook 测试成功',
|
||||
'webhookTestFail': 'Webhook 测试失败',
|
||||
'script': '脚本执行',
|
||||
'scriptEnable': '启用脚本',
|
||||
'scriptDesc': '在下载成功完成时执行自定义脚本',
|
||||
'scriptPathHint': '请输入脚本文件路径(例如:/path/to/script.sh)',
|
||||
'urlInvalid': '请输入有效的 HTTP 或 HTTPS 链接',
|
||||
'required': '此项为必填项',
|
||||
'show': '显示',
|
||||
|
||||
Reference in new issue
Block a user