feat: support webhook (#1187)

This commit is contained in:
Copilot authored and GitHub committed 2025-11-25 23:34:45 +08:00
1 parent 5bb8541385
commit 9d9f1caaaa
15 files changed
+1361 -33

No files matched your search

+5
View File
@@ -236,6 +236,11 @@ Future<void> updateExtension(String identity) async {
() => _client.dio.post("api/v1/extensions/$identity/update"), null);
}
Future<void> testWebhook(String url) async {
return _parse(
() => _client.dio.post("api/v1/webhook/test", data: {"url": url}), null);
}
Future<String> login(LoginReq loginReq) async {
return _parse(() => _client.dio.post("api/web/login", data: loginReq),
(data) => data as String);
@@ -82,6 +82,7 @@ class ExtraConfig {
bool defaultDirectDownload;
bool defaultBtClient;
bool notifyWhenNewVersion;
List<String> webhookUrls;
ExtraConfigBt bt = ExtraConfigBt();
@@ -92,6 +93,7 @@ class ExtraConfig {
this.defaultDirectDownload = false,
this.defaultBtClient = true,
this.notifyWhenNewVersion = true,
this.webhookUrls = const [],
});
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
@@ -75,6 +75,10 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
webhookUrls: (json['webhookUrls'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
@@ -85,6 +89,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'defaultDirectDownload': instance.defaultDirectDownload,
'defaultBtClient': instance.defaultBtClient,
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
'webhookUrls': instance.webhookUrls,
'bt': instance.bt.toJson(),
};
@@ -10,6 +10,7 @@ import 'package:intl/intl.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../api/api.dart' as api;
import '../../../../api/model/downloader_config.dart';
import '../../../../i18n/message.dart';
import '../../../../util/input_formatter.dart';
@@ -24,6 +25,7 @@ import '../../../views/check_list_view.dart';
import '../../../views/directory_selector.dart';
import '../../../views/open_in_new.dart';
import '../../../views/outlined_button_loading.dart';
import '../../../views/text_button_loading.dart';
import '../../app/controllers/app_controller.dart';
import '../controllers/setting_controller.dart';
@@ -891,6 +893,72 @@ class SettingView extends GetView<SettingController> {
);
});
// advanced config webhook items
final buildWebhook = _buildConfigItem(
'webhook',
() => 'items'.trParams(
{'count': downloaderCfg.value.extra.webhookUrls.length.toString()}),
(Key key) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'webhookDesc'.tr,
style: Theme.of(Get.context!).textTheme.bodySmall,
),
_padding,
// List of existing webhook URLs
...downloaderCfg.value.extra.webhookUrls
.asMap()
.entries
.map((entry) {
final index = entry.key;
final url = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(
child: Text(
url,
style: Theme.of(Get.context!).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Icons.edit, size: 20),
tooltip: 'edit'.tr,
onPressed: () {
_showWebhookDialog(index: index, initialUrl: url);
},
),
IconButton(
icon: const Icon(Icons.delete, size: 20),
tooltip: 'delete'.tr,
onPressed: () async {
downloaderCfg.value.extra.webhookUrls.removeAt(index);
downloaderCfg.refresh();
await debounceSave();
},
),
],
),
);
}),
_padding,
// Add button
OutlinedButton.icon(
onPressed: () {
_showWebhookDialog();
},
icon: const Icon(Icons.add),
label: Text('add'.tr),
),
],
);
},
);
// advanced config log items start
buildLogsDir() {
return ListTile(
@@ -1035,6 +1103,7 @@ class SettingView extends GetView<SettingController> {
Card(
child: Column(
children: _addDivider([
buildWebhook(),
buildLogsDir(),
]),
)),
@@ -1047,6 +1116,120 @@ class SettingView extends GetView<SettingController> {
});
}
void _showWebhookDialog({int? index, String? initialUrl}) {
final urlController = TextEditingController(text: initialUrl ?? '');
final testController = OutlinedButtonLoadingController();
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: [
TextFormField(
controller: urlController,
decoration: InputDecoration(
hintText: 'webhookUrlHint'.tr,
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'required'.tr;
}
final url = value.trim().toLowerCase();
if (!url.startsWith('http://') &&
!url.startsWith('https://')) {
return 'webhookUrlInvalid'.tr;
}
try {
Uri.parse(value.trim());
} catch (e) {
return 'webhookUrlInvalid'.tr;
}
return null;
},
),
],
),
),
actionsAlignment: MainAxisAlignment.spaceBetween,
actions: [
// Test button on the left
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: OutlinedButtonLoading(
controller: testController,
onPressed: () async {
if (!formKey.currentState!.validate()) {
return;
}
final url = urlController.text.trim();
if (url.isEmpty) return;
testController.start();
try {
await api.testWebhook(url);
showMessage('tip'.tr, 'webhookTestSuccess'.tr);
} catch (e) {
showErrorMessage('webhookTestFail'.tr);
} finally {
testController.stop();
}
},
child: Text('webhookTest'.tr),
),
),
// Cancel and Confirm on the right
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('cancel'.tr),
),
TextButtonLoading(
controller: saveController,
onPressed: () async {
if (!formKey.currentState!.validate()) {
return;
}
final url = urlController.text.trim();
if (url.isEmpty) return;
saveController.start();
try {
if (isEdit) {
downloaderCfg.value.extra.webhookUrls[index] = url;
} else {
downloaderCfg.value.extra.webhookUrls.add(url);
}
downloaderCfg.refresh();
await appController.saveConfig();
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
} catch (e) {
showErrorMessage(e);
} finally {
saveController.stop();
}
},
child: Text('confirm'.tr),
),
],
),
],
),
);
}
void _tapInputWidget(GlobalKey key) {
if (key.currentContext == null) {
return;
+10
View File
@@ -65,6 +65,14 @@ const enUS = {
'effectAfterRestart': 'Effect after restart',
'developer': 'Developer',
'logDirectory': 'Log Directory',
'webhook': 'Webhook',
'webhookDesc': 'Send HTTP POST notifications when tasks complete or fail',
'webhookUrlHint': 'Enter webhook URL',
'webhookTest': 'Test',
'webhookTestSuccess': 'Webhook test successful',
'webhookTestFail': 'Webhook test failed',
'webhookUrlInvalid': 'Please enter a valid HTTP or HTTPS URL',
'required': 'This field is required',
'show': 'Show',
'continue': 'Continue',
'pause': 'Pause',
@@ -73,6 +81,8 @@ const enUS = {
'deleteTask': 'Delete @count tasks',
'deleteTaskTip': 'Keep downloaded files',
'delete': 'Delete',
'add': 'Add',
'edit': 'Edit',
'newVersionTitle': 'Discover new version @version',
'newVersionUpdate': 'Update Now',
'newVersionLater': 'Later',
+10
View File
@@ -64,6 +64,14 @@ const zhCN = {
'effectAfterRestart': '此配置项将在重启应用后生效',
'developer': '开发者',
'logDirectory': '日志目录',
'webhook': 'Webhook 推送',
'webhookDesc': '在任务完成或失败时发送 HTTP POST 通知',
'webhookUrlHint': '请输入 Webhook URL',
'webhookTest': '测试',
'webhookTestSuccess': 'Webhook 测试成功',
'webhookTestFail': 'Webhook 测试失败',
'webhookUrlInvalid': '请输入有效的 HTTP 或 HTTPS 链接',
'required': '此项为必填项',
'show': '显示',
'continue': '继续',
'pause': '暂停',
@@ -72,6 +80,8 @@ const zhCN = {
'deleteTask': '删除 @count 个任务',
'deleteTaskTip': '保留已下载的文件',
'delete': '删除',
'add': '添加',
'edit': '编辑',
'newVersionTitle': '发现新版本 @version',
'newVersionUpdate': '立即更新',
'newVersionLater': '稍后再说',