mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 10:00:27 +00:00
feat: support webhook (#1187)
This commit is contained in:
1 parent
5bb8541385
commit
9d9f1caaaa
15 files changed
+1361
-33
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '稍后再说',
|
||||
|
||||
Reference in new issue
Block a user