Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
117c488
Initial plan
Copilot Nov 25, 2025
5fe708f
Add download directory placeholder feature with category support
Copilot Nov 25, 2025
79f0c54
Add translations for all languages and fix translation errors
Copilot Nov 25, 2025
c253d60
Merge branch 'main' into copilot/add-download-directory-organization
monkeyWie Nov 26, 2025
f670074
Fix download categories: initialize defaults and fix unmodifiable lis…
Copilot Nov 26, 2025
cad38ae
Allow editing/deleting built-in categories with i18n support and simp…
Copilot Nov 26, 2025
d285517
Move display name logic out of model and implement soft delete for bu…
Copilot Nov 26, 2025
7245c04
Fix built-in category initialization, focus loss issue, and render pl…
Copilot Nov 26, 2025
b396953
Add delete confirmation dialog and fix category list refresh issue
Copilot Nov 26, 2025
9f403d1
Make placeholder rendered path more visible in create task view
Copilot Nov 26, 2025
b38fa82
Show rendered placeholder path directly in input field as inline badge
Copilot Nov 26, 2025
3a930c6
Render placeholders directly in path input on create task page initia…
Copilot Nov 26, 2025
9bb93fc
update
monkeyWie Nov 26, 2025
6f40136
Refactor webhook config from Extra to top-level WebhookConfig struct …
Copilot Nov 26, 2025
e75b923
Update Flutter UI to use new webhook config structure with enable toggle
Copilot Nov 26, 2025
c8afaa5
Fix unmodifiable list error when saving/deleting webhook URLs
Copilot Nov 26, 2025
7205a04
update
monkeyWie Nov 26, 2025
a7a832e
Fix TestDownloaderStoreConfig_Init test to include Webhook field
Copilot Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pkg/base/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ type DownloaderStoreConfig struct {
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"`
Webhook *WebhookConfig `json:"webhook"` // Webhook is the webhook configuration
}

func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
Expand All @@ -194,6 +195,9 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{}
}
if cfg.Webhook == nil {
cfg.Webhook = &WebhookConfig{}
}
return cfg
}

Expand All @@ -216,9 +220,18 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
if cfg.Proxy == nil {
cfg.Proxy = beforeCfg.Proxy
}
if cfg.Webhook == nil {
cfg.Webhook = beforeCfg.Webhook
}
return cfg
}

// WebhookConfig is the webhook configuration
type WebhookConfig struct {
Enable bool `json:"enable"` // Enable is the flag to enable/disable webhooks
URLs []string `json:"urls"` // URLs is the list of webhook URLs
}

type DownloaderProxyConfig struct {
Enable bool `json:"enable"`
// System is the flag that use system proxy
Expand Down
8 changes: 7 additions & 1 deletion pkg/base/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
},
},
{
Expand All @@ -29,6 +30,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
MaxRunning: 10,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
},
},
{
Expand All @@ -43,7 +45,8 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: map[string]any{
"key": "value",
},
Proxy: &DownloaderProxyConfig{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
},
},
{
Expand All @@ -59,6 +62,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
Proxy: &DownloaderProxyConfig{
Enable: true,
},
Webhook: &WebhookConfig{},
},
},
}
Expand All @@ -71,6 +75,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
Webhook: tt.fields.Webhook,
}
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Init() = %v, want %v", got, tt.want)
Expand Down Expand Up @@ -255,6 +260,7 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
Webhook: tt.fields.Webhook,
}
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Merge() = %v, want %v", got, tt.want)
Expand Down
3 changes: 3 additions & 0 deletions pkg/download/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
opts.Path = storeConfig.DownloadDir
}

// Replace placeholders in download path (e.g., %year%, %month%, %day%, %date%)
opts.Path = util.ReplacePathPlaceholders(opts.Path)

// if enable white download directory, check if the download directory is in the white list
if len(d.cfg.WhiteDownloadDirs) > 0 {
inWhiteList := false
Expand Down
23 changes: 21 additions & 2 deletions pkg/download/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,29 @@ type WebhookPayload struct {
Task *Task `json:"task"`
}

// getWebhookUrls extracts and converts webhook URLs from config
// getWebhookUrls extracts webhook URLs from config
// Supports both new webhook config format and legacy extra field for backward compatibility
func (d *Downloader) getWebhookUrls() []string {
cfg := d.cfg.DownloaderStoreConfig
if cfg == nil || cfg.Extra == nil {
if cfg == nil {
return nil
}

// Try new webhook config first
if cfg.Webhook != nil && cfg.Webhook.Enable && len(cfg.Webhook.URLs) > 0 {
urls := make([]string, 0, len(cfg.Webhook.URLs))
for _, url := range cfg.Webhook.URLs {
if url != "" {
urls = append(urls, url)
}
}
if len(urls) > 0 {
return urls
}
}

// Fall back to legacy extra field for backward compatibility
if cfg.Extra == nil {
return nil
}

Expand Down
122 changes: 75 additions & 47 deletions pkg/download/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ func TestWebhook_TriggerOnDone(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
// Configure webhook URLs
cfg, _ := downloader.GetConfig()
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
cfg.Extra["webhookUrls"] = []string{server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL},
}
downloader.PutConfig(cfg)

// Create a mock task
Expand Down Expand Up @@ -98,10 +98,10 @@ func TestWebhook_TriggerOnError(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
// Configure webhook URLs
cfg, _ := downloader.GetConfig()
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
cfg.Extra["webhookUrls"] = []string{server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL},
}
downloader.PutConfig(cfg)

// Create a mock task
Expand Down Expand Up @@ -143,10 +143,10 @@ func TestWebhook_SendTestWebhook(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
// Configure webhook URLs
cfg, _ := downloader.GetConfig()
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
cfg.Extra["webhookUrls"] = []string{server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL},
}
downloader.PutConfig(cfg)

// Send test webhook
Expand Down Expand Up @@ -206,10 +206,10 @@ func TestWebhook_MultipleUrls(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
// Configure multiple webhook URLs
cfg, _ := downloader.GetConfig()
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
cfg.Extra["webhookUrls"] = []string{server1.URL, server2.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server1.URL, server2.URL},
}
downloader.PutConfig(cfg)

// Create a mock task
Expand Down Expand Up @@ -239,10 +239,10 @@ func TestWebhook_TestWebhookFailsOnNon200(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
// Configure webhook URLs
cfg, _ := downloader.GetConfig()
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
cfg.Extra["webhookUrls"] = []string{server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL},
}
downloader.PutConfig(cfg)

// Send test webhook - should fail with non-200 status
Expand All @@ -263,10 +263,10 @@ func TestWebhook_TestWebhookFailsOn201(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
// Configure webhook URLs
cfg, _ := downloader.GetConfig()
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
cfg.Extra["webhookUrls"] = []string{server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL},
}
downloader.PutConfig(cfg)

// Send test webhook - should fail with 201 status (only 200 is success)
Expand Down Expand Up @@ -333,7 +333,7 @@ func TestWebhook_GetWebhookUrls_EmptyConfig(t *testing.T) {
func TestWebhook_GetWebhookUrls_NoExtraField(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = nil
cfg.Webhook = nil
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
Expand All @@ -346,8 +346,7 @@ func TestWebhook_GetWebhookUrls_NoExtraField(t *testing.T) {
func TestWebhook_GetWebhookUrls_NoWebhookUrlsKey(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["otherKey"] = "value"
cfg.Webhook = &base.WebhookConfig{Enable: true} // No URLs set
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
Expand All @@ -360,8 +359,10 @@ func TestWebhook_GetWebhookUrls_NoWebhookUrlsKey(t *testing.T) {
func TestWebhook_GetWebhookUrls_StringSlice(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []string{"http://example.com", "http://example2.com"}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{"http://example.com", "http://example2.com"},
}
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
Expand All @@ -377,8 +378,10 @@ func TestWebhook_GetWebhookUrls_StringSlice(t *testing.T) {
func TestWebhook_GetWebhookUrls_InterfaceSlice(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []interface{}{"http://example.com", "http://example2.com"}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{"http://example.com", "http://example2.com"},
}
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
Expand All @@ -395,7 +398,7 @@ func TestWebhook_GetWebhookUrls_EmptyStringSlice(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []string{}
cfg.Webhook.URLs = []string{}
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
Expand All @@ -408,8 +411,10 @@ func TestWebhook_GetWebhookUrls_EmptyStringSlice(t *testing.T) {
func TestWebhook_GetWebhookUrls_InterfaceSliceWithEmptyStrings(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []interface{}{"", "", ""}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{"", "", ""},
}
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
Expand All @@ -422,13 +427,15 @@ func TestWebhook_GetWebhookUrls_InterfaceSliceWithEmptyStrings(t *testing.T) {
func TestWebhook_GetWebhookUrls_InterfaceSliceMixedTypes(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []interface{}{"http://example.com", 123, "http://example2.com", nil, ""}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{"http://example.com", "", "http://example2.com", ""},
}
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
if len(urls) != 2 {
t.Errorf("Expected 2 valid URLs (ignoring non-strings and empty), got %d: %v", len(urls), urls)
t.Errorf("Expected 2 valid URLs (ignoring empty strings), got %d: %v", len(urls), urls)
}
if urls[0] != "http://example.com" || urls[1] != "http://example2.com" {
t.Errorf("URLs don't match expected values: %v", urls)
Expand All @@ -439,13 +446,28 @@ func TestWebhook_GetWebhookUrls_InterfaceSliceMixedTypes(t *testing.T) {
func TestWebhook_GetWebhookUrls_InvalidType(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = "not-a-slice"
cfg.Webhook = &base.WebhookConfig{Enable: false} // Disabled webhook
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
if urls != nil {
t.Errorf("Expected nil for invalid type, got %v", urls)
t.Errorf("Expected nil for disabled webhook, got %v", urls)
}
})
}

func TestWebhook_GetWebhookUrls_DisabledWebhook(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Webhook = &base.WebhookConfig{
Enable: false,
URLs: []string{"http://example.com"},
}
downloader.PutConfig(cfg)

urls := downloader.getWebhookUrls()
if urls != nil {
t.Errorf("Expected nil for disabled webhook even with URLs, got %v", urls)
}
})
}
Expand Down Expand Up @@ -617,8 +639,10 @@ func TestWebhook_TriggerWebhooks_EmptyUrlSkipped(t *testing.T) {

setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []string{server.URL, "", server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL, "", server.URL},
}
downloader.PutConfig(cfg)

task := NewTask()
Expand Down Expand Up @@ -650,8 +674,10 @@ func TestWebhook_WebhookDataStructure(t *testing.T) {

setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []string{server.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server.URL},
}
downloader.PutConfig(cfg)

task := NewTask()
Expand Down Expand Up @@ -692,7 +718,7 @@ func TestWebhook_SendTestWebhook_EmptyUrls(t *testing.T) {
setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []string{}
cfg.Webhook.URLs = []string{}
downloader.PutConfig(cfg)

err := downloader.SendTestWebhook()
Expand All @@ -717,8 +743,10 @@ func TestWebhook_SendTestWebhook_MixedResults(t *testing.T) {

setupWebhookTest(t, func(downloader *Downloader) {
cfg, _ := downloader.GetConfig()
cfg.Extra = make(map[string]any)
cfg.Extra["webhookUrls"] = []string{server1.URL, server2.URL}
cfg.Webhook = &base.WebhookConfig{
Enable: true,
URLs: []string{server1.URL, server2.URL},
}
downloader.PutConfig(cfg)

// Should fail because server2 returns 500
Expand Down
Loading
Loading