From 49b4d99b5e0e909445e28bff8d2d37da2bab75bd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:39:08 +0800 Subject: [PATCH] feat: add download directory placeholders, category support (#1188) --- pkg/base/model.go | 13 + pkg/base/model_test.go | 8 +- pkg/download/downloader.go | 3 + pkg/download/webhook.go | 23 +- pkg/download/webhook_test.go | 122 +++++--- pkg/util/path.go | 26 ++ pkg/util/path_test.go | 75 +++++ .../lib/api/model/downloader_config.dart | 43 ++- .../lib/api/model/downloader_config.g.dart | 53 +++- .../app/controllers/app_controller.dart | 41 +++ .../app/modules/create/views/create_view.dart | 69 ++++- .../modules/setting/views/setting_view.dart | 276 +++++++++++++++++- .../lib/app/views/directory_selector.dart | 198 +++++++++++-- ui/flutter/lib/i18n/langs/de_de.dart | 31 +- ui/flutter/lib/i18n/langs/en_us.dart | 17 ++ ui/flutter/lib/i18n/langs/es_es.dart | 27 +- ui/flutter/lib/i18n/langs/fa_ir.dart | 18 +- ui/flutter/lib/i18n/langs/fr_fr.dart | 36 ++- ui/flutter/lib/i18n/langs/hu_hu.dart | 28 +- ui/flutter/lib/i18n/langs/id_id.dart | 19 +- ui/flutter/lib/i18n/langs/it_it.dart | 16 + ui/flutter/lib/i18n/langs/ja_jp.dart | 16 + ui/flutter/lib/i18n/langs/pl_pl.dart | 16 + ui/flutter/lib/i18n/langs/pt_br.dart | 19 +- ui/flutter/lib/i18n/langs/ru_ru.dart | 16 + ui/flutter/lib/i18n/langs/ta_ta.dart | 19 +- ui/flutter/lib/i18n/langs/tr_tr.dart | 29 +- ui/flutter/lib/i18n/langs/uk_ua.dart | 16 + ui/flutter/lib/i18n/langs/vi_vn.dart | 16 + ui/flutter/lib/i18n/langs/zh_cn.dart | 17 ++ ui/flutter/lib/i18n/langs/zh_tw.dart | 16 + 31 files changed, 1204 insertions(+), 118 deletions(-) diff --git a/pkg/base/model.go b/pkg/base/model.go index 5b006edd..8c18dc83 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -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 { @@ -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 } @@ -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 diff --git a/pkg/base/model_test.go b/pkg/base/model_test.go index f218162e..c1ba6919 100644 --- a/pkg/base/model_test.go +++ b/pkg/base/model_test.go @@ -18,6 +18,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) { MaxRunning: 5, ProtocolConfig: map[string]any{}, Proxy: &DownloaderProxyConfig{}, + Webhook: &WebhookConfig{}, }, }, { @@ -29,6 +30,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) { MaxRunning: 10, ProtocolConfig: map[string]any{}, Proxy: &DownloaderProxyConfig{}, + Webhook: &WebhookConfig{}, }, }, { @@ -43,7 +45,8 @@ func TestDownloaderStoreConfig_Init(t *testing.T) { ProtocolConfig: map[string]any{ "key": "value", }, - Proxy: &DownloaderProxyConfig{}, + Proxy: &DownloaderProxyConfig{}, + Webhook: &WebhookConfig{}, }, }, { @@ -59,6 +62,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) { Proxy: &DownloaderProxyConfig{ Enable: true, }, + Webhook: &WebhookConfig{}, }, }, } @@ -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) @@ -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) diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 573ffac1..4c5c897e 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -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 diff --git a/pkg/download/webhook.go b/pkg/download/webhook.go index d095b065..d223d213 100644 --- a/pkg/download/webhook.go +++ b/pkg/download/webhook.go @@ -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 } diff --git a/pkg/download/webhook_test.go b/pkg/download/webhook_test.go index 521bb217..dcfcbb7b 100644 --- a/pkg/download/webhook_test.go +++ b/pkg/download/webhook_test.go @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -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) @@ -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) } }) } @@ -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() @@ -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() @@ -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() @@ -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 diff --git a/pkg/util/path.go b/pkg/util/path.go index 9855118e..09397574 100644 --- a/pkg/util/path.go +++ b/pkg/util/path.go @@ -8,6 +8,7 @@ import ( syspath "path" "path/filepath" "strings" + "time" ) func Dir(path string) string { @@ -199,3 +200,28 @@ func ReplaceInvalidFilename(path string) string { } return path } + +// ReplacePathPlaceholders replaces date placeholders in a path with actual values +// Supported placeholders: +// - %year% - Current year (e.g., 2025) +// - %month% - Current month (01-12) +// - %day% - Current day (01-31) +// - %date% - Full date format (2025-01-01) +func ReplacePathPlaceholders(path string) string { + if path == "" { + return "" + } + + now := time.Now() + year := fmt.Sprintf("%d", now.Year()) + month := fmt.Sprintf("%02d", now.Month()) + day := fmt.Sprintf("%02d", now.Day()) + date := fmt.Sprintf("%s-%s-%s", year, month, day) + + path = strings.ReplaceAll(path, "%year%", year) + path = strings.ReplaceAll(path, "%month%", month) + path = strings.ReplaceAll(path, "%day%", day) + path = strings.ReplaceAll(path, "%date%", date) + + return path +} diff --git a/pkg/util/path_test.go b/pkg/util/path_test.go index 3f5cf967..14760fd3 100644 --- a/pkg/util/path_test.go +++ b/pkg/util/path_test.go @@ -1,8 +1,11 @@ package util import ( + "fmt" "os" + "strings" "testing" + "time" ) func TestDir(t *testing.T) { @@ -240,3 +243,75 @@ func TestReplaceInvalidFilename(t *testing.T) { }) } } + +func TestReplacePathPlaceholders(t *testing.T) { + now := time.Now() + year := fmt.Sprintf("%d", now.Year()) + month := fmt.Sprintf("%02d", now.Month()) + day := fmt.Sprintf("%02d", now.Day()) + date := fmt.Sprintf("%s-%s-%s", year, month, day) + + tests := []struct { + name string + path string + want string + }{ + { + name: "empty path", + path: "", + want: "", + }, + { + name: "no placeholders", + path: "/home/user/Downloads", + want: "/home/user/Downloads", + }, + { + name: "year placeholder", + path: "/Downloads/%year%", + want: "/Downloads/" + year, + }, + { + name: "month placeholder", + path: "/Downloads/%month%", + want: "/Downloads/" + month, + }, + { + name: "day placeholder", + path: "/Downloads/%day%", + want: "/Downloads/" + day, + }, + { + name: "date placeholder", + path: "/Downloads/%date%", + want: "/Downloads/" + date, + }, + { + name: "multiple placeholders", + path: "/Downloads/%year%-%month%", + want: "/Downloads/" + year + "-" + month, + }, + { + name: "mixed path with placeholders", + path: "/home/user/Downloads/%year%/%month%/%day%", + want: "/home/user/Downloads/" + year + "/" + month + "/" + day, + }, + { + name: "windows style path", + path: "D:\\Downloads\\%year%-%month%", + want: "D:\\Downloads\\" + year + "-" + month, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ReplacePathPlaceholders(tt.path) + if !strings.Contains(got, year) && tt.path != "" && strings.Contains(tt.path, "%year%") { + t.Errorf("ReplacePathPlaceholders() = %v, want containing year %v", got, year) + } + if got != tt.want { + t.Errorf("ReplacePathPlaceholders() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ui/flutter/lib/api/model/downloader_config.dart b/ui/flutter/lib/api/model/downloader_config.dart index e62c7dd4..8b5bc34c 100644 --- a/ui/flutter/lib/api/model/downloader_config.dart +++ b/ui/flutter/lib/api/model/downloader_config.dart @@ -9,6 +9,7 @@ class DownloaderConfig { ProtocolConfig protocolConfig = ProtocolConfig(); ExtraConfig extra = ExtraConfig(); ProxyConfig proxy = ProxyConfig(); + WebhookConfig webhook = WebhookConfig(); DownloaderConfig({ this.downloadDir = '', @@ -82,7 +83,7 @@ class ExtraConfig { bool defaultDirectDownload; bool defaultBtClient; bool notifyWhenNewVersion; - List webhookUrls; + List downloadCategories; ExtraConfigBt bt = ExtraConfigBt(); @@ -93,7 +94,7 @@ class ExtraConfig { this.defaultDirectDownload = false, this.defaultBtClient = true, this.notifyWhenNewVersion = true, - this.webhookUrls = const [], + this.downloadCategories = const [], }); factory ExtraConfig.fromJson(Map? json) => @@ -102,6 +103,44 @@ class ExtraConfig { Map toJson() => _$ExtraConfigToJson(this); } +@JsonSerializable() +class DownloadCategory { + String name; + String path; + bool isBuiltIn; + String? nameKey; // i18n key for built-in categories (e.g., 'categoryMusic') + bool isDeleted; // Mark built-in categories as deleted instead of removing them + + DownloadCategory({ + required this.name, + required this.path, + this.isBuiltIn = false, + this.nameKey, + this.isDeleted = false, + }); + + factory DownloadCategory.fromJson(Map json) => + _$DownloadCategoryFromJson(json); + + Map toJson() => _$DownloadCategoryToJson(this); +} + +@JsonSerializable() +class WebhookConfig { + bool enable; + List urls; + + WebhookConfig({ + this.enable = false, + this.urls = const [], + }); + + factory WebhookConfig.fromJson(Map? json) => + json == null ? WebhookConfig() : _$WebhookConfigFromJson(json); + + Map toJson() => _$WebhookConfigToJson(this); +} + @JsonSerializable() class ProxyConfig { bool enable; diff --git a/ui/flutter/lib/api/model/downloader_config.g.dart b/ui/flutter/lib/api/model/downloader_config.g.dart index f5a3485e..a4ccb4fc 100644 --- a/ui/flutter/lib/api/model/downloader_config.g.dart +++ b/ui/flutter/lib/api/model/downloader_config.g.dart @@ -14,7 +14,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map json) => ..protocolConfig = ProtocolConfig.fromJson( json['protocolConfig'] as Map?) ..extra = ExtraConfig.fromJson(json['extra'] as Map?) - ..proxy = ProxyConfig.fromJson(json['proxy'] as Map); + ..proxy = ProxyConfig.fromJson(json['proxy'] as Map) + ..webhook = WebhookConfig.fromJson(json['webhook'] as Map?); Map _$DownloaderConfigToJson(DownloaderConfig instance) => { @@ -23,6 +24,7 @@ Map _$DownloaderConfigToJson(DownloaderConfig instance) => 'protocolConfig': instance.protocolConfig.toJson(), 'extra': instance.extra.toJson(), 'proxy': instance.proxy.toJson(), + 'webhook': instance.webhook.toJson(), }; ProtocolConfig _$ProtocolConfigFromJson(Map json) => @@ -75,8 +77,8 @@ ExtraConfig _$ExtraConfigFromJson(Map 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?) - ?.map((e) => e as String) + downloadCategories: (json['downloadCategories'] as List?) + ?.map((e) => DownloadCategory.fromJson(e as Map)) .toList() ?? const [], )..bt = ExtraConfigBt.fromJson(json['bt'] as Map); @@ -89,10 +91,53 @@ Map _$ExtraConfigToJson(ExtraConfig instance) => 'defaultDirectDownload': instance.defaultDirectDownload, 'defaultBtClient': instance.defaultBtClient, 'notifyWhenNewVersion': instance.notifyWhenNewVersion, - 'webhookUrls': instance.webhookUrls, + 'downloadCategories': + instance.downloadCategories.map((e) => e.toJson()).toList(), 'bt': instance.bt.toJson(), }; +DownloadCategory _$DownloadCategoryFromJson(Map json) => + DownloadCategory( + name: json['name'] as String, + path: json['path'] as String, + isBuiltIn: json['isBuiltIn'] as bool? ?? false, + nameKey: json['nameKey'] as String?, + isDeleted: json['isDeleted'] as bool? ?? false, + ); + +Map _$DownloadCategoryToJson(DownloadCategory instance) { + final val = { + 'name': instance.name, + 'path': instance.path, + 'isBuiltIn': instance.isBuiltIn, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('nameKey', instance.nameKey); + val['isDeleted'] = instance.isDeleted; + return val; +} + +WebhookConfig _$WebhookConfigFromJson(Map json) => + WebhookConfig( + enable: json['enable'] as bool? ?? false, + urls: (json['urls'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); + +Map _$WebhookConfigToJson(WebhookConfig instance) => + { + 'enable': instance.enable, + 'urls': instance.urls, + }; + ProxyConfig _$ProxyConfigFromJson(Map json) => ProxyConfig( enable: json['enable'] as bool? ?? false, system: json['system'] as bool? ?? false, diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index 239c0271..5670ef5e 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:share_handler/share_handler.dart'; import 'package:tray_manager/tray_manager.dart'; @@ -504,6 +505,7 @@ class AppController extends GetxController with WindowListener, TrayListener { // default select all tracker subscribe urls extra.bt.trackerSubscribeUrls.addAll(allTrackerSubscribeUrls); } + final proxy = config.proxy; if (proxy.scheme.isEmpty) { proxy.scheme = 'http'; @@ -521,6 +523,45 @@ class AppController extends GetxController with WindowListener, TrayListener { config.downloadDir = './'; } } + + // Initialize default download categories if empty + if (extra.downloadCategories.isEmpty) { + await _initDefaultDownloadCategories(); + } + } + + Future _initDefaultDownloadCategories() async { + final extra = downloaderConfig.value.extra; + final downloadDir = downloaderConfig.value.downloadDir; + + // Add default built-in categories with i18n keys + // No need to set initial name value, it will be retrieved via nameKey + extra.downloadCategories.addAll([ + DownloadCategory( + name: '', + path: path.join(downloadDir, 'Music'), + isBuiltIn: true, + nameKey: 'categoryMusic', + ), + DownloadCategory( + name: '', + path: path.join(downloadDir, 'Video'), + isBuiltIn: true, + nameKey: 'categoryVideo', + ), + DownloadCategory( + name: '', + path: path.join(downloadDir, 'Document'), + isBuiltIn: true, + nameKey: 'categoryDocument', + ), + DownloadCategory( + name: '', + path: path.join(downloadDir, 'Program'), + isBuiltIn: true, + nameKey: 'categoryProgram', + ), + ]); } Future _initLaunchAtStartup() async { diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index 72e8b300..67cdca8f 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -12,6 +12,7 @@ import 'package:rounded_loading_button_plus/rounded_loading_button.dart'; import '../../../../api/api.dart'; import '../../../../api/model/create_task.dart'; import '../../../../api/model/create_task_batch.dart'; +import '../../../../api/model/downloader_config.dart'; import '../../../../api/model/options.dart'; import '../../../../api/model/request.dart'; import '../../../../api/model/resolve_result.dart'; @@ -72,7 +73,9 @@ class CreateView extends GetView { .toString(); } if (_pathController.text.isEmpty) { - _pathController.text = appController.downloaderConfig.value.downloadDir; + // Render placeholders when initializing the path + final downloadDir = appController.downloaderConfig.value.downloadDir; + _pathController.text = renderPathPlaceholders(downloadDir); } final CreateTask? routerParams = Get.rootDelegate.arguments(); @@ -328,6 +331,8 @@ class CreateView extends GetView { DirectorySelector( controller: _pathController, ), + // Category selector + _buildCategorySelector(appController), Obx( () => Visibility( visible: controller.showAdvanced.value, @@ -912,4 +917,66 @@ class CreateView extends GetView { ], )); } + + Widget _buildCategorySelector(AppController appController) { + final categories = + appController.downloaderConfig.value.extra.downloadCategories + .where((c) => !c.isDeleted) // Filter out deleted categories + .toList(); + if (categories.isEmpty) { + return const SizedBox.shrink(); + } + + // Helper to get display name + String getCategoryDisplayName(DownloadCategory category) { + if (category.nameKey != null && category.nameKey!.isNotEmpty) { + return category.nameKey!.tr; + } + return category.name; + } + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Text( + 'selectCategory'.tr, + style: TextStyle( + color: Get.theme.hintColor, + fontSize: 12, + ), + ), + const SizedBox(width: 8), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: categories.map((category) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: OutlinedButton( + onPressed: () { + _pathController.text = renderPathPlaceholders(category.path); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + minimumSize: Size.zero, + ), + child: Text( + getCategoryDisplayName(category), + style: const TextStyle(fontSize: 12), + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ); + } } diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index f0d2abf9..f50e798b 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -35,6 +35,14 @@ final _divider = const Divider().paddingOnly(left: 10, right: 10); class SettingView extends GetView { const SettingView({Key? key}) : super(key: key); + // Helper function to get display name for a category + static String _getCategoryDisplayName(DownloadCategory category) { + if (category.nameKey != null && category.nameKey!.isNotEmpty) { + return category.nameKey!.tr; + } + return category.name; + } + @override Widget build(BuildContext context) { final appController = Get.find(); @@ -71,20 +79,25 @@ class SettingView extends GetView { 'downloadDir', () => downloaderCfg.value.downloadDir, (Key key) { final downloadDirController = TextEditingController(text: downloaderCfg.value.downloadDir); - downloadDirController.addListener(() async { + + // Update config only when editing is done (on focus lost or submit) + void onEditComplete() { if (downloadDirController.text != downloaderCfg.value.downloadDir) { downloaderCfg.value.downloadDir = downloadDirController.text; if (Util.isDesktop()) { controller.clearTap(); } - - await debounceSave(); + debounceSave(); } - }); + } + return DirectorySelector( controller: downloadDirController, showLabel: false, showAndoirdToggle: true, + allowEdit: true, + showPlaceholderButton: true, + onEditComplete: onEditComplete, ); }); final buildMaxRunning = _buildConfigItem( @@ -135,6 +148,133 @@ class SettingView extends GetView { ); }); + // Download categories configuration + buildDownloadCategories() { + final categories = downloaderCfg.value.extra.downloadCategories + .where((c) => !c.isDeleted) // Filter out deleted categories + .toList(); + return ListTile( + title: Text('downloadCategories'.tr), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (categories.isEmpty) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'add'.tr, + style: TextStyle(color: Theme.of(context).hintColor), + ), + ), + ...categories.map((category) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _getCategoryDisplayName(category), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text( + category.path, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).hintColor, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + icon: Icon( + Icons.edit, + size: 20, + color: Theme.of(context).hintColor, + ), + onPressed: () { + _showCategoryDialog( + context, + debounceSave, + downloaderCfg, + category: category, + ); + }, + ), + IconButton( + icon: Icon( + Icons.delete, + size: 20, + color: Theme.of(context).hintColor, + ), + onPressed: () async { + // Show confirmation dialog + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('tip'.tr), + content: Text('confirmDelete'.tr), + actions: [ + TextButton( + onPressed: () => + Navigator.of(context).pop(false), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: () => + Navigator.of(context).pop(true), + child: Text('confirm'.tr), + ), + ], + ), + ); + + if (confirmed == true) { + if (category.isBuiltIn) { + // Mark built-in category as deleted instead of removing it + downloaderCfg.update((val) { + category.isDeleted = true; + }); + } else { + // Remove custom categories completely + downloaderCfg.update((val) { + val!.extra.downloadCategories = val + .extra.downloadCategories + .where((c) => c != category) + .toList(); + }); + } + debounceSave(); + } + }, + ), + ], + ), + ); + }), + Padding( + padding: const EdgeInsets.only(top: 8), + child: OutlinedButton.icon( + icon: const Icon(Icons.add, size: 18), + label: Text('add'.tr), + onPressed: () { + _showCategoryDialog( + context, + debounceSave, + downloaderCfg, + ); + }, + ), + ), + ], + ), + ); + } + buildBrowserExtension() { return ListTile( title: Text('browserExtension'.tr), @@ -896,22 +1036,37 @@ class SettingView extends GetView { // advanced config webhook items final buildWebhook = _buildConfigItem( 'webhook', - () => 'items'.trParams( - {'count': downloaderCfg.value.extra.webhookUrls.length.toString()}), + () => downloaderCfg.value.webhook.enable ? 'on'.tr : 'off'.tr, (Key key) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + Text( + 'webhookEnable'.tr, + style: Theme.of(Get.context!).textTheme.bodyMedium, + ), + const Spacer(), + Switch( + value: downloaderCfg.value.webhook.enable, + onChanged: (value) { + downloaderCfg.update((val) { + val!.webhook.enable = value; + }); + debounceSave(); + }, + ), + ], + ), + _padding, Text( 'webhookDesc'.tr, style: Theme.of(Get.context!).textTheme.bodySmall, ), _padding, // List of existing webhook URLs - ...downloaderCfg.value.extra.webhookUrls - .asMap() - .entries - .map((entry) { + ...downloaderCfg.value.webhook.urls.asMap().entries.map((entry) { final index = entry.key; final url = entry.value; return Padding( @@ -936,8 +1091,12 @@ class SettingView extends GetView { icon: const Icon(Icons.delete, size: 20), tooltip: 'delete'.tr, onPressed: () async { - downloaderCfg.value.extra.webhookUrls.removeAt(index); - downloaderCfg.refresh(); + // Create new list to avoid unmodifiable list error + final urls = List.from(downloaderCfg.value.webhook.urls); + urls.removeAt(index); + downloaderCfg.update((val) { + val!.webhook.urls = urls; + }); await debounceSave(); }, ), @@ -1018,6 +1177,7 @@ class SettingView extends GetView { child: Column( children: _addDivider([ buildDownloadDir(), + buildDownloadCategories(), buildMaxRunning(), buildDefaultDirectDownload(), buildBrowserExtension(), @@ -1205,12 +1365,16 @@ class SettingView extends GetView { saveController.start(); try { + // Create new list to avoid unmodifiable list error + final urls = List.from(downloaderCfg.value.webhook.urls); if (isEdit) { - downloaderCfg.value.extra.webhookUrls[index] = url; + urls[index] = url; } else { - downloaderCfg.value.extra.webhookUrls.add(url); + urls.add(url); } - downloaderCfg.refresh(); + downloaderCfg.update((val) { + val!.webhook.urls = urls; + }); await appController.saveConfig(); if (dialogContext.mounted) { Navigator.of(dialogContext).pop(); @@ -1302,6 +1466,88 @@ class SettingView extends GetView { return 'themeSystem'.tr; } } + + void _showCategoryDialog( + BuildContext context, + Future Function() debounceSave, + Rx downloaderCfg, { + DownloadCategory? category, + }) { + final isEdit = category != null; + final nameController = TextEditingController( + text: isEdit ? _getCategoryDisplayName(category) : '', + ); + final pathController = TextEditingController(text: category?.path ?? ''); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(isEdit ? 'edit'.tr : 'add'.tr), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: 'categoryName'.tr, + ), + ), + const SizedBox(height: 16), + DirectorySelector( + controller: pathController, + showLabel: true, + allowEdit: true, + showPlaceholderButton: true, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: () { + if (nameController.text.isEmpty || pathController.text.isEmpty) { + return; + } + + if (isEdit) { + // Trigger UI update by wrapping changes in update() + downloaderCfg.update((val) { + // If name changed, clear nameKey so it won't be re-translated + final nameChanged = + nameController.text != _getCategoryDisplayName(category); + category.name = nameController.text; + category.path = pathController.text; + if (nameChanged) { + category.nameKey = null; + } + // If editing a deleted built-in category, unmark it as deleted + if (category.isBuiltIn && category.isDeleted) { + category.isDeleted = false; + } + }); + } else { + downloaderCfg.update((val) { + val!.extra.downloadCategories = [ + ...val.extra.downloadCategories, + DownloadCategory( + name: nameController.text, + path: pathController.text, + ), + ]; + }); + } + debounceSave(); + Navigator.of(context).pop(); + }, + child: Text('confirm'.tr), + ), + ], + ), + ); + } } enum ProxyModeEnum { diff --git a/ui/flutter/lib/app/views/directory_selector.dart b/ui/flutter/lib/app/views/directory_selector.dart index d10f68c5..43fef851 100644 --- a/ui/flutter/lib/app/views/directory_selector.dart +++ b/ui/flutter/lib/app/views/directory_selector.dart @@ -14,17 +14,86 @@ import '../../util/util.dart'; final deviceInfo = DeviceInfoPlugin(); +// Placeholder information for download directory +class PathPlaceholder { + final String placeholder; + final String description; + final String example; + + const PathPlaceholder({ + required this.placeholder, + required this.description, + required this.example, + }); +} + +// Available placeholders for download directory +List getPathPlaceholders() { + final now = DateTime.now(); + final year = now.year.toString(); + final month = now.month.toString().padLeft(2, '0'); + final day = now.day.toString().padLeft(2, '0'); + + return [ + PathPlaceholder( + placeholder: '%year%', + description: 'placeholderYear'.tr, + example: year, + ), + PathPlaceholder( + placeholder: '%month%', + description: 'placeholderMonth'.tr, + example: month, + ), + PathPlaceholder( + placeholder: '%day%', + description: 'placeholderDay'.tr, + example: day, + ), + PathPlaceholder( + placeholder: '%date%', + description: 'placeholderDate'.tr, + example: '$year-$month-$day', + ), + ]; +} + +// Render placeholders in a path with actual values +String renderPathPlaceholders(String path) { + if (path.isEmpty) return path; + + final now = DateTime.now(); + final year = now.year.toString(); + final month = now.month.toString().padLeft(2, '0'); + final day = now.day.toString().padLeft(2, '0'); + final date = '$year-$month-$day'; + + return path + .replaceAll('%year%', year) + .replaceAll('%month%', month) + .replaceAll('%day%', day) + .replaceAll('%date%', date); +} + class DirectorySelector extends StatefulWidget { final TextEditingController controller; final bool showLabel; final bool showAndoirdToggle; + final bool allowEdit; + final bool showPlaceholderButton; + final VoidCallback? onEditComplete; + final bool showRenderedPlaceholders; - const DirectorySelector( - {Key? key, - required this.controller, - this.showLabel = true, - this.showAndoirdToggle = false}) - : super(key: key); + const DirectorySelector({ + Key? key, + required this.controller, + this.showLabel = true, + this.showAndoirdToggle = false, + this.allowEdit = false, + this.showPlaceholderButton = false, + this.onEditComplete, + this.showRenderedPlaceholders = false, + }) : super(key: key); @override State createState() => _DirectorySelectorState(); @@ -108,22 +177,117 @@ class _DirectorySelectorState extends State { return null; } + Widget? buildPlaceholderButton() { + if (!widget.showPlaceholderButton) return null; + + return PopupMenuButton( + icon: const Icon(Icons.data_object), + tooltip: 'insertPlaceholder'.tr, + onSelected: (String placeholder) { + final currentText = widget.controller.text; + final selection = widget.controller.selection; + final cursorPosition = selection.baseOffset >= 0 + ? selection.baseOffset + : currentText.length; + + final newText = currentText.substring(0, cursorPosition) + + placeholder + + currentText.substring(cursorPosition); + widget.controller.text = newText; + widget.controller.selection = TextSelection.fromPosition( + TextPosition(offset: cursorPosition + placeholder.length), + ); + }, + itemBuilder: (BuildContext context) { + final placeholders = getPathPlaceholders(); + return placeholders.map((p) { + return PopupMenuItem( + value: p.placeholder, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${p.placeholder} - ${p.description}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text( + 'example'.trParams({'value': p.example}), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).hintColor, + ), + ), + ], + ), + ); + }).toList(); + }, + ); + } + return Row( children: [ Expanded( - child: TextFormField( - readOnly: Util.isWeb() ? false : true, - controller: widget.controller, - decoration: widget.showLabel - ? InputDecoration( - labelText: 'downloadDir'.tr, - ) - : null, - validator: (v) { - return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr; + child: ValueListenableBuilder( + valueListenable: widget.controller, + builder: (context, value, child) { + Widget? suffix; + if (widget.showRenderedPlaceholders && value.text.contains('%')) { + final renderedPath = renderPathPlaceholders(value.text); + // Show rendered path as a chip/badge in the input field + suffix = Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.blue[200]!), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.arrow_forward, size: 14, color: Colors.blue[700]), + const SizedBox(width: 4), + Flexible( + child: Text( + renderedPath, + style: TextStyle( + color: Colors.blue[700], + fontSize: 12, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + return TextFormField( + readOnly: widget.allowEdit ? false : (Util.isWeb() ? false : true), + controller: widget.controller, + decoration: widget.showLabel + ? InputDecoration( + labelText: 'downloadDir'.tr, + suffix: suffix, + ) + : InputDecoration( + suffix: suffix, + ), + validator: (v) { + return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr; + }, + onEditingComplete: widget.onEditComplete, + onTapOutside: (event) { + // Call onEditComplete when user taps outside the field + widget.onEditComplete?.call(); + }, + ); }, )), - buildSelectWidget() + buildSelectWidget(), + buildPlaceholderButton(), ].where((e) => e != null).map((e) => e!).toList(), ); } diff --git a/ui/flutter/lib/i18n/langs/de_de.dart b/ui/flutter/lib/i18n/langs/de_de.dart index cc2b648b..ae977e84 100644 --- a/ui/flutter/lib/i18n/langs/de_de.dart +++ b/ui/flutter/lib/i18n/langs/de_de.dart @@ -4,6 +4,7 @@ const deDE = { 'error': 'Fehler', 'tip': 'Tipp', 'confirm': 'Bestätigen', + 'confirmDelete': 'Möchten Sie wirklich löschen?', 'cancel': 'Abbrechen', 'on': 'An', 'off': 'Aus', @@ -21,10 +22,13 @@ const deDE = { 'followSettings': 'Einstellungen Folgen', 'downloadLink': 'Download Link', 'downloadLinkValid': 'Bitte geben Sie den Download-Link ein', - 'downloadLinkHit': 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append', - 'downloadLinkHitDesktop': ', oder ziehen Sie die Torrent-Datei direkt hierher', + 'downloadLinkHit': + 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append', + 'downloadLinkHitDesktop': + ', oder ziehen Sie die Torrent-Datei direkt hierher', 'download': 'Download', - 'noFileSelected': 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.', + 'noFileSelected': + 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.', 'noStoragePermission': 'Speicherberechtigung erforderlich', 'selectFile': 'Datei auswählen', 'rename': 'Umbenennen', @@ -45,7 +49,8 @@ const deDE = { 'updateDaily': 'Tägliche Aktualisierung', 'lastUpdate': 'Letzte Aktualisierung: @time', 'addTracker': 'Tracker hinzufügen', - 'addTrackerHit': 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile', + 'addTrackerHit': + 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile', 'ui': 'Benutzeroberfläche', 'theme': 'Thema', 'themeSystem': 'System', @@ -122,8 +127,24 @@ const deDE = { 'username_required': 'Bitte geben Sie Ihren Benutzernamen ein', 'password_required': 'Bitte geben Sie Ihr Passwort ein', 'login_success': 'Anmeldung erfolgreich', - 'login_failed': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort', + 'login_failed': + 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort', 'login_failed_network': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung', + 'insertPlaceholder': 'Platzhalter einfügen', + 'placeholderYear': 'Aktuelles Jahr', + 'placeholderMonth': 'Aktueller Monat (01-12)', + 'placeholderDay': 'Aktueller Tag (01-31)', + 'placeholderDate': 'Vollständiges Datum (JJJJ-MM-TT)', + 'example': 'z.B. @value', + 'downloadCategories': 'Download-Kategorien', + 'categoryMusic': 'Musik', + 'categoryVideo': 'Video', + 'categoryDocument': 'Dokument', + 'categoryProgram': 'Programm', + 'categoryName': 'Kategoriename', + 'categoryPath': 'Kategoriepfad', + 'builtInCategory': 'Integrierte Kategorie kann nicht gelöscht werden', + 'selectCategory': 'Kategorie auswählen', }, }; diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index f18a6e84..1fb99012 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -4,6 +4,7 @@ const enUS = { 'error': 'Error', 'tip': 'Tip', 'confirm': 'Confirm', + 'confirmDelete': 'Are you sure you want to delete?', 'cancel': 'Cancel', 'on': 'On', 'off': 'Off', @@ -66,6 +67,7 @@ const enUS = { 'developer': 'Developer', 'logDirectory': 'Log Directory', 'webhook': 'Webhook', + 'webhookEnable': 'Enable Webhook', 'webhookDesc': 'Send HTTP POST notifications when tasks complete or fail', 'webhookUrlHint': 'Enter webhook URL', 'webhookTest': 'Test', @@ -136,5 +138,20 @@ const enUS = { 'login_failed': 'Login failed, please check your username and password', 'login_failed_network': 'Login failed, please check your network connection', + 'insertPlaceholder': 'Insert Placeholder', + 'placeholderYear': 'Current year', + 'placeholderMonth': 'Current month (01-12)', + 'placeholderDay': 'Current day (01-31)', + 'placeholderDate': 'Full date (YYYY-MM-DD)', + 'example': 'e.g. @value', + 'downloadCategories': 'Download Categories', + 'categoryMusic': 'Music', + 'categoryVideo': 'Video', + 'categoryDocument': 'Document', + 'categoryProgram': 'Program', + 'categoryName': 'Category Name', + 'categoryPath': 'Category Path', + 'builtInCategory': 'Built-in category cannot be deleted', + 'selectCategory': 'Select Category', }, }; diff --git a/ui/flutter/lib/i18n/langs/es_es.dart b/ui/flutter/lib/i18n/langs/es_es.dart index 8fa89615..2de5c012 100644 --- a/ui/flutter/lib/i18n/langs/es_es.dart +++ b/ui/flutter/lib/i18n/langs/es_es.dart @@ -4,6 +4,7 @@ const esES = { 'error': 'Error', 'tip': 'Consejo', 'confirm': 'Confirmar', + 'confirmDelete': '¿Está seguro de que desea eliminar?', 'cancel': 'Cancelar', 'on': 'Activado', 'off': 'Desactivado', @@ -21,9 +22,11 @@ const esES = { 'downloadLinkValid': 'Por favor, introduce el enlace de descarga', 'downloadLinkHit': 'Por favor, introduce el enlace de descarga, se admite HTTP/HTTPS/MAGNET@append', - 'downloadLinkHitDesktop': ', o arrastra el archivo torrent aquí directamente', + 'downloadLinkHitDesktop': + ', o arrastra el archivo torrent aquí directamente', 'download': 'Descargar', - 'noFileSelected': 'Por favor, selecciona al menos un archivo para continuar.', + 'noFileSelected': + 'Por favor, selecciona al menos un archivo para continuar.', 'noStoragePermission': 'Se requiere permiso de almacenamiento', 'selectFile': 'Seleccionar Archivo', 'rename': 'Renombrar', @@ -43,7 +46,8 @@ const esES = { 'updateDaily': 'Actualizar diariamente', 'lastUpdate': 'Última actualización: @time', 'addTracker': 'Añadir Tracker', - 'addTrackerHit': 'Por favor, introduce la URL del servidor tracker, una por línea', + 'addTrackerHit': + 'Por favor, introduce la URL del servidor tracker, una por línea', 'ui': 'Interfaz', 'theme': 'Tema', 'themeSystem': 'Sistema', @@ -104,5 +108,20 @@ const esES = { 'taskUrl': 'URL de la Tarea', 'downloadPath': 'Ruta de Descarga', 'skipVerifyCert': 'Omitir Verificación de Certificado', + 'insertPlaceholder': 'Insertar marcador de posición', + 'placeholderYear': 'Año actual', + 'placeholderMonth': 'Mes actual (01-12)', + 'placeholderDay': 'Día actual (01-31)', + 'placeholderDate': 'Fecha completa (AAAA-MM-DD)', + 'example': 'ej. @value', + 'downloadCategories': 'Categorías de descarga', + 'categoryMusic': 'Música', + 'categoryVideo': 'Video', + 'categoryDocument': 'Documento', + 'categoryProgram': 'Programa', + 'categoryName': 'Nombre de categoría', + 'categoryPath': 'Ruta de categoría', + 'builtInCategory': 'La categoría incorporada no se puede eliminar', + 'selectCategory': 'Seleccionar categoría', }, -}; \ No newline at end of file +}; diff --git a/ui/flutter/lib/i18n/langs/fa_ir.dart b/ui/flutter/lib/i18n/langs/fa_ir.dart index 96815ef2..06898eab 100644 --- a/ui/flutter/lib/i18n/langs/fa_ir.dart +++ b/ui/flutter/lib/i18n/langs/fa_ir.dart @@ -4,6 +4,7 @@ const faIR = { 'error': 'اررور', 'tip': 'Tip', 'confirm': 'تایید', + 'confirmDelete': 'آیا مطمئن هستید که می خواهید حذف کنید؟', 'cancel': 'انصراف', 'selectAll': 'انتخاب همه', 'task': 'کار', @@ -60,7 +61,22 @@ const faIR = { 'thanks': 'تشکر', 'thanksDesc': 'از تمامی افرادی که در ساخت و توسعه‌ی جامعه‌ی Gopeed مشارکت کرده‌اند، سپاسگزاری می‌کنیم!', - 'browserExtension': 'فایل های دانلود شده را نگه دارد', + 'browserExtension': 'افزونه مرورگر', 'skipVerifyCert': 'رد کردن تأیید گواهینامه', + 'insertPlaceholder': 'درج نگهدارنده', + 'placeholderYear': 'سال جاری', + 'placeholderMonth': 'ماه جاری (01-12)', + 'placeholderDay': 'روز جاری (01-31)', + 'placeholderDate': 'تاریخ کامل (YYYY-MM-DD)', + 'example': 'مثلا @value', + 'downloadCategories': 'دسته‌بندی‌های دانلود', + 'categoryMusic': 'موسیقی', + 'categoryVideo': 'ویدیو', + 'categoryDocument': 'سند', + 'categoryProgram': 'برنامه', + 'categoryName': 'نام دسته‌بندی', + 'categoryPath': 'مسیر دسته‌بندی', + 'builtInCategory': 'دسته‌بندی داخلی قابل حذف نیست', + 'selectCategory': 'انتخاب دسته‌بندی', } }; diff --git a/ui/flutter/lib/i18n/langs/fr_fr.dart b/ui/flutter/lib/i18n/langs/fr_fr.dart index 75f630ff..d20cde67 100644 --- a/ui/flutter/lib/i18n/langs/fr_fr.dart +++ b/ui/flutter/lib/i18n/langs/fr_fr.dart @@ -4,6 +4,7 @@ const frFR = { 'error': 'Erreur', 'tip': 'Conseil', 'confirm': 'Confirmer', + 'confirmDelete': 'Êtes-vous sûr de vouloir supprimer ?', 'cancel': 'Annuler', 'on': 'Activé', 'off': 'Désactivé', @@ -19,10 +20,12 @@ const frFR = { 'advancedOptions': 'Options avancées', 'downloadLink': 'Lien de téléchargement', 'downloadLinkValid': 'Veuillez entrer le lien de téléchargement', - 'downloadLinkHit': 'Veuillez entrer le lien de téléchargement, HTTP/HTTPS/MAGNET pris en charge', + 'downloadLinkHit': + 'Veuillez entrer le lien de téléchargement, HTTP/HTTPS/MAGNET pris en charge', 'downloadLinkHitDesktop': ', ou glissez directement le fichier torrent ici', 'download': 'Télécharger', - 'noFileSelected': 'Veuillez sélectionner au moins un fichier pour continuer.', + 'noFileSelected': + 'Veuillez sélectionner au moins un fichier pour continuer.', 'noStoragePermission': 'Permission de stockage requise', 'selectFile': 'Sélectionner un fichier', 'rename': 'Renommer', @@ -32,16 +35,19 @@ const frFR = { 'downloadDir': 'Répertoire de téléchargement', 'downloadDirValid': 'Veuillez sélectionner le répertoire de téléchargement', 'connections': 'Connexions', - 'useServerCtime': 'Utiliser l\'heure du serveur pour la création de fichier', + 'useServerCtime': + 'Utiliser l\'heure du serveur pour la création de fichier', 'maxRunning': 'Tâches en cours maximum', 'items': '@count éléments', 'subscribeTracker': 'S\'abonner au tracker', - 'subscribeFail': 'Abonnement échoué, veuillez vérifier le réseau ou réessayer plus tard', + 'subscribeFail': + 'Abonnement échoué, veuillez vérifier le réseau ou réessayer plus tard', 'update': 'Mettre à jour', 'updateDaily': 'Mettre à jour quotidiennement', 'lastUpdate': 'Dernière mise à jour : @time', 'addTracker': 'Ajouter un tracker', - 'addTrackerHit': 'Veuillez entrer l\'URL du serveur de tracker, une par ligne', + 'addTrackerHit': + 'Veuillez entrer l\'URL du serveur de tracker, une par ligne', 'ui': 'Interface utilisateur', 'theme': 'Thème', 'themeSystem': 'Système', @@ -89,9 +95,25 @@ const frFR = { 'username': 'Nom d\'utilisateur', 'password': 'Mot de passe', 'thanks': 'Merci', - 'thanksDesc': 'Merci à tous les contributeurs qui ont aidé à construire et développer la communauté Gopeed !', + 'thanksDesc': + 'Merci à tous les contributeurs qui ont aidé à construire et développer la communauté Gopeed !', 'browserExtension': 'Extension du navigateur', 'launchAtStartup': 'Lancer au démarrage', - 'skipVerifyCert': 'Bypass Verifikasi Sertifikat', + 'skipVerifyCert': 'Ignorer la vérification du certificat', + 'insertPlaceholder': 'Insérer un espace réservé', + 'placeholderYear': 'Année en cours', + 'placeholderMonth': 'Mois en cours (01-12)', + 'placeholderDay': 'Jour en cours (01-31)', + 'placeholderDate': 'Date complète (AAAA-MM-JJ)', + 'example': 'ex. @value', + 'downloadCategories': 'Catégories de téléchargement', + 'categoryMusic': 'Musique', + 'categoryVideo': 'Vidéo', + 'categoryDocument': 'Document', + 'categoryProgram': 'Programme', + 'categoryName': 'Nom de la catégorie', + 'categoryPath': 'Chemin de la catégorie', + 'builtInCategory': 'La catégorie intégrée ne peut pas être supprimée', + 'selectCategory': 'Sélectionner une catégorie', }, }; diff --git a/ui/flutter/lib/i18n/langs/hu_hu.dart b/ui/flutter/lib/i18n/langs/hu_hu.dart index 7f5cc565..84c95f61 100644 --- a/ui/flutter/lib/i18n/langs/hu_hu.dart +++ b/ui/flutter/lib/i18n/langs/hu_hu.dart @@ -4,6 +4,7 @@ const huHU = { 'error': 'Hiba', 'tip': 'Tipp', 'confirm': 'Megerősít', + 'confirmDelete': 'Biztosan törölni szeretné?', 'cancel': 'Mégsem', 'on': 'Bekapcsolva', 'off': 'Kikapcsolva', @@ -21,7 +22,8 @@ const huHU = { 'followSettings': 'Beállítások követése', 'downloadLink': 'Letöltési hivatkozás', 'downloadLinkValid': 'Kérjük, adja meg a letöltési hivatkozást', - 'downloadLinkHit': 'Kérjük, adja meg a letöltési hivatkozást, soronként egyet@append', + 'downloadLinkHit': + 'Kérjük, adja meg a letöltési hivatkozást, soronként egyet@append', 'downloadLinkHitDesktop': ', vagy húzza ide a torrent fájlt közvetlenül', 'download': 'Letöltés', 'noFileSelected': 'Kérjük, válasszon ki legalább egy fájlt a folytatáshoz.', @@ -36,7 +38,8 @@ const huHU = { 'connections': 'Kapcsolatok', 'useServerCtime': 'Használja a szerveridőt a fájl létrehozásához', 'maxRunning': 'Maximális futó feladatok', - 'defaultDirectDownload': 'Alapértelmezettként ellenőrizze a közvetlen letöltést', + 'defaultDirectDownload': + 'Alapértelmezettként ellenőrizze a közvetlen letöltést', 'items': '@count elem', 'subscribeTracker': 'Nyomkövető előfizetése', 'subscribeFail': @@ -45,7 +48,8 @@ const huHU = { 'updateDaily': 'Napi frissítés', 'lastUpdate': 'Utolsó frissítés: @time', 'addTracker': 'Nyomkövető hozzáadása', - 'addTrackerHit': 'Kérjük, adja meg a nyomkövető szerver URL-jét, soronként egyet', + 'addTrackerHit': + 'Kérjük, adja meg a nyomkövető szerver URL-jét, soronként egyet', 'ui': 'Felhasználói felület', 'theme': 'Téma', 'themeSystem': 'Rendszer', @@ -60,7 +64,8 @@ const huHU = { 'apiToken': 'API token', 'notSet': 'NS', 'set': 'BEÁLLÍT', - 'portInUse': 'A [@port] port használatban van, kérjük, változtassa meg a portot', + 'portInUse': + 'A [@port] port használatban van, kérjük, változtassa meg a portot', 'effectAfterRestart': 'Kihatás indítás után', 'developer': 'Fejlesztő', 'logDirectory': 'Napló könyvtár', @@ -118,5 +123,20 @@ const huHU = { 'fileSelectedSize': 'Méret: ', 'httpHeaderName': 'Fejléc neve', 'httpHeaderValue': 'Fejléc értéke', + 'insertPlaceholder': 'Helyőrző beszúrása', + 'placeholderYear': 'Aktuális év', + 'placeholderMonth': 'Aktuális hónap (01-12)', + 'placeholderDay': 'Aktuális nap (01-31)', + 'placeholderDate': 'Teljes dátum (ÉÉÉÉ-HH-NN)', + 'example': 'pl. @value', + 'downloadCategories': 'Letöltési kategóriák', + 'categoryMusic': 'Zene', + 'categoryVideo': 'Videó', + 'categoryDocument': 'Dokumentum', + 'categoryProgram': 'Program', + 'categoryName': 'Kategória neve', + 'categoryPath': 'Kategória útvonala', + 'builtInCategory': 'A beépített kategória nem törölhető', + 'selectCategory': 'Kategória kiválasztása', }, }; diff --git a/ui/flutter/lib/i18n/langs/id_id.dart b/ui/flutter/lib/i18n/langs/id_id.dart index 3b11b3a0..4af06c61 100644 --- a/ui/flutter/lib/i18n/langs/id_id.dart +++ b/ui/flutter/lib/i18n/langs/id_id.dart @@ -4,6 +4,7 @@ const idID = { 'error': 'Kesalahan', 'tip': 'Tips', 'confirm': 'Konfirmasi', + 'confirmDelete': 'Apakah Anda yakin ingin menghapus?', 'cancel': 'Batal', 'on': 'Aktif', 'off': 'Nonaktif', @@ -22,7 +23,8 @@ const idID = { 'downloadLinkValid': 'Silakan masukkan tautan unduhan', 'downloadLinkHit': 'Tolong masukkan tautan unduhan, HTTP/HTTPS/MAGNET didukung@append', - 'downloadLinkHitDesktop': ', atau seret file torrent ke sini secara langsung', + 'downloadLinkHitDesktop': + ', atau seret file torrent ke sini secara langsung', 'download': 'Download', 'noFileSelected': 'Silakan pilih setidaknya satu file untuk melanjutkan.', 'noStoragePermission': 'Izin penyimpanan diperlukan', @@ -108,5 +110,20 @@ const idID = { 'taskUrl': 'URL Tugas', 'downloadPath': 'Direktori Unduhan', 'skipVerifyCert': 'Lupakan verifikasi sertifikat', + 'insertPlaceholder': 'Sisipkan placeholder', + 'placeholderYear': 'Tahun saat ini', + 'placeholderMonth': 'Bulan saat ini (01-12)', + 'placeholderDay': 'Hari saat ini (01-31)', + 'placeholderDate': 'Tanggal lengkap (YYYY-MM-DD)', + 'example': 'cth. @value', + 'downloadCategories': 'Kategori Unduhan', + 'categoryMusic': 'Musik', + 'categoryVideo': 'Video', + 'categoryDocument': 'Dokumen', + 'categoryProgram': 'Program', + 'categoryName': 'Nama Kategori', + 'categoryPath': 'Path Kategori', + 'builtInCategory': 'Kategori bawaan tidak dapat dihapus', + 'selectCategory': 'Pilih Kategori', }, }; diff --git a/ui/flutter/lib/i18n/langs/it_it.dart b/ui/flutter/lib/i18n/langs/it_it.dart index a6e9ffb5..7905c812 100644 --- a/ui/flutter/lib/i18n/langs/it_it.dart +++ b/ui/flutter/lib/i18n/langs/it_it.dart @@ -4,6 +4,7 @@ const itIT = { 'error': 'Errore', 'tip': 'Suggerimento', 'confirm': 'Conferma', + 'confirmDelete': 'Sei sicuro di voler eliminare?', 'cancel': 'Annullla', 'on': 'Acceso', 'off': 'Spento', @@ -97,5 +98,20 @@ const itIT = { 'browserExtension': 'Estensione del browser', 'launchAtStartup': "Lancia all'avvio", 'skipVerifyCert': 'Salta la verifica del certificato', + 'insertPlaceholder': 'Inserisci segnaposto', + 'placeholderYear': 'Anno corrente', + 'placeholderMonth': 'Mese corrente (01-12)', + 'placeholderDay': 'Giorno corrente (01-31)', + 'placeholderDate': 'Data completa (AAAA-MM-GG)', + 'example': 'es. @value', + 'downloadCategories': 'Categorie di download', + 'categoryMusic': 'Musica', + 'categoryVideo': 'Video', + 'categoryDocument': 'Documento', + 'categoryProgram': 'Programma', + 'categoryName': 'Nome categoria', + 'categoryPath': 'Percorso categoria', + 'builtInCategory': 'La categoria predefinita non può essere eliminata', + 'selectCategory': 'Seleziona categoria', }, }; diff --git a/ui/flutter/lib/i18n/langs/ja_jp.dart b/ui/flutter/lib/i18n/langs/ja_jp.dart index 26d999da..008902c3 100644 --- a/ui/flutter/lib/i18n/langs/ja_jp.dart +++ b/ui/flutter/lib/i18n/langs/ja_jp.dart @@ -4,6 +4,7 @@ const jaJP = { 'error': 'エラー', 'tip': 'ヒント', 'confirm': '確認', + 'confirmDelete': '削除してもよろしいですか?', 'cancel': 'キャンセル', 'selectAll': 'すべてを選択', 'task': 'タスク', @@ -63,5 +64,20 @@ const jaJP = { 'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!', 'browserExtension': 'ブラウザ拡張機能', 'skipVerifyCert': '証明書の検証をスキップ', + 'insertPlaceholder': 'プレースホルダーを挿入', + 'placeholderYear': '現在の年', + 'placeholderMonth': '現在の月 (01-12)', + 'placeholderDay': '現在の日 (01-31)', + 'placeholderDate': '完全な日付 (年-月-日)', + 'example': '例: @value', + 'downloadCategories': 'ダウンロードカテゴリ', + 'categoryMusic': '音楽', + 'categoryVideo': '動画', + 'categoryDocument': 'ドキュメント', + 'categoryProgram': 'プログラム', + 'categoryName': 'カテゴリ名', + 'categoryPath': 'カテゴリパス', + 'builtInCategory': '内蔵カテゴリは削除できません', + 'selectCategory': 'カテゴリを選択', } }; diff --git a/ui/flutter/lib/i18n/langs/pl_pl.dart b/ui/flutter/lib/i18n/langs/pl_pl.dart index c346dde8..bfb5d20c 100644 --- a/ui/flutter/lib/i18n/langs/pl_pl.dart +++ b/ui/flutter/lib/i18n/langs/pl_pl.dart @@ -4,6 +4,7 @@ const plPL = { 'error': 'Błąd', 'tip': 'Tip', 'confirm': 'Potwierdź', + 'confirmDelete': 'Czy na pewno chcesz usunąć?', 'cancel': 'Anuluj', 'on': 'Włącz', 'off': 'Wyłącz', @@ -95,5 +96,20 @@ const plPL = { 'browserExtension': 'Wtyczka dla przeglądarki', 'launchAtStartup': 'Uruchom na starcie', 'skipVerifyCert': 'Pomiń weryfikację certyfikatu', + 'insertPlaceholder': 'Wstaw placeholder', + 'placeholderYear': 'Bieżący rok', + 'placeholderMonth': 'Bieżący miesiąc (01-12)', + 'placeholderDay': 'Bieżący dzień (01-31)', + 'placeholderDate': 'Pełna data (RRRR-MM-DD)', + 'example': 'np. @value', + 'downloadCategories': 'Kategorie pobierania', + 'categoryMusic': 'Muzyka', + 'categoryVideo': 'Wideo', + 'categoryDocument': 'Dokument', + 'categoryProgram': 'Program', + 'categoryName': 'Nazwa kategorii', + 'categoryPath': 'Ścieżka kategorii', + 'builtInCategory': 'Wbudowanej kategorii nie można usunąć', + 'selectCategory': 'Wybierz kategorię', }, }; diff --git a/ui/flutter/lib/i18n/langs/pt_br.dart b/ui/flutter/lib/i18n/langs/pt_br.dart index f5383561..765126b3 100644 --- a/ui/flutter/lib/i18n/langs/pt_br.dart +++ b/ui/flutter/lib/i18n/langs/pt_br.dart @@ -4,6 +4,7 @@ const ptBR = { 'error': 'Erro', 'tip': 'Dica', 'confirm': 'Confirmar', + 'confirmDelete': 'Tem certeza de que deseja excluir?', 'cancel': 'Cancelar', 'on': 'Ligado', 'off': 'Desligado', @@ -21,7 +22,8 @@ const ptBR = { 'followSettings': 'Follow Settings', 'downloadLink': 'Link de Download', 'downloadLinkValid': 'Por favor, insira o link de download', - 'downloadLinkHit': 'Por favor, insira o link de download, um por linha@append', + 'downloadLinkHit': + 'Por favor, insira o link de download, um por linha@append', 'downloadLinkHitDesktop': ', ou arraste o arquivo torrent aqui', 'download': 'Download', 'noFileSelected': 'Por favor, selecione ao menos um arquivo para continuar', @@ -118,5 +120,20 @@ const ptBR = { 'fileSelectedSize': 'Tamanho: ', 'httpHeaderName': 'Nome do Header', 'httpHeaderValue': 'Valor do Header', + 'insertPlaceholder': 'Inserir marcador', + 'placeholderYear': 'Ano atual', + 'placeholderMonth': 'Mês atual (01-12)', + 'placeholderDay': 'Dia atual (01-31)', + 'placeholderDate': 'Data completa (AAAA-MM-DD)', + 'example': 'ex. @value', + 'downloadCategories': 'Categorias de Download', + 'categoryMusic': 'Música', + 'categoryVideo': 'Vídeo', + 'categoryDocument': 'Documento', + 'categoryProgram': 'Programa', + 'categoryName': 'Nome da Categoria', + 'categoryPath': 'Caminho da Categoria', + 'builtInCategory': 'Categoria integrada não pode ser excluída', + 'selectCategory': 'Selecionar Categoria', }, }; diff --git a/ui/flutter/lib/i18n/langs/ru_ru.dart b/ui/flutter/lib/i18n/langs/ru_ru.dart index 6bb59ce5..a80d2945 100644 --- a/ui/flutter/lib/i18n/langs/ru_ru.dart +++ b/ui/flutter/lib/i18n/langs/ru_ru.dart @@ -4,6 +4,7 @@ const ruRU = { 'error': 'Ошибка', 'tip': 'Подсказка', 'confirm': 'Подтвердить', + 'confirmDelete': 'Вы уверены, что хотите удалить?', 'cancel': 'Отмена', 'on': 'Вкл.', 'off': 'Откл.', @@ -119,5 +120,20 @@ const ruRU = { 'fileSelectedSize': 'Размер: ', 'httpHeaderName': 'Название заголовка HTTP', 'httpHeaderValue': 'Значение заголовка HTTP', + 'insertPlaceholder': 'Вставить заполнитель', + 'placeholderYear': 'Текущий год', + 'placeholderMonth': 'Текущий месяц (01-12)', + 'placeholderDay': 'Текущий день (01-31)', + 'placeholderDate': 'Полная дата (ГГГГ-ММ-ДД)', + 'example': 'напр. @value', + 'downloadCategories': 'Категории загрузки', + 'categoryMusic': 'Музыка', + 'categoryVideo': 'Видео', + 'categoryDocument': 'Документ', + 'categoryProgram': 'Программа', + 'categoryName': 'Название категории', + 'categoryPath': 'Путь категории', + 'builtInCategory': 'Встроенную категорию нельзя удалить', + 'selectCategory': 'Выбрать категорию', } }; diff --git a/ui/flutter/lib/i18n/langs/ta_ta.dart b/ui/flutter/lib/i18n/langs/ta_ta.dart index b720f910..7ea6bc49 100644 --- a/ui/flutter/lib/i18n/langs/ta_ta.dart +++ b/ui/flutter/lib/i18n/langs/ta_ta.dart @@ -4,6 +4,7 @@ const taTA = { 'error': 'பிழை', 'tip': 'குறிப்பு', 'confirm': 'சரி', + 'confirmDelete': 'நிச்சயமாக நீக்க விரும்புகிறீர்களா?', 'cancel': 'ரத்துசெய்', 'on': 'On', 'off': 'Off', @@ -21,7 +22,8 @@ const taTA = { 'downloadLinkValid': 'பதிவிறக்க இணைப்பை உள்ளிடவும்', 'downloadLinkHit': 'பதிவிறக்க இணைப்பை உள்ளிடவும், HTTP/HTTPS/MAGNET ஆகியவற்றை ஏற்கும்@append', - 'downloadLinkHitDesktop': ', அல்லது torrent கோப்பை நேரடியாக இங்கே இழுக்கவும்', + 'downloadLinkHitDesktop': + ', அல்லது torrent கோப்பை நேரடியாக இங்கே இழுக்கவும்', 'download': 'பதிவிறக்கு', 'noFileSelected': 'குறைந்தபட்சம் ஒரு கோப்பைத் தேர்ந்தெடுக்கவும்.', 'noStoragePermission': 'சேமிப்பக அனுமதி தேவை', @@ -96,5 +98,20 @@ const taTA = { 'browserExtension': 'உலாவி நீட்டிப்பு', 'launchAtStartup': 'தொடக்கத்தின் போது இயங்கு', 'skipVerifyCert': 'திருத்தி சான்று சரிபார்க்கவும்', + 'insertPlaceholder': 'இடங்காட்டி செருக', + 'placeholderYear': 'தற்போதைய ஆண்டு', + 'placeholderMonth': 'தற்போதைய மாதம் (01-12)', + 'placeholderDay': 'தற்போதைய நாள் (01-31)', + 'placeholderDate': 'முழு தேதி (YYYY-MM-DD)', + 'example': 'எ.கா. @value', + 'downloadCategories': 'பதிவிறக்க வகைகள்', + 'categoryMusic': 'இசை', + 'categoryVideo': 'காணொளி', + 'categoryDocument': 'ஆவணம்', + 'categoryProgram': 'நிரல்', + 'categoryName': 'வகை பெயர்', + 'categoryPath': 'வகை பாதை', + 'builtInCategory': 'உள்ளமைக்கப்பட்ட வகையை நீக்க முடியாது', + 'selectCategory': 'வகையைத் தேர்ந்தெடுக்கவும்', }, }; diff --git a/ui/flutter/lib/i18n/langs/tr_tr.dart b/ui/flutter/lib/i18n/langs/tr_tr.dart index 3bd994cb..a5b0bc42 100644 --- a/ui/flutter/lib/i18n/langs/tr_tr.dart +++ b/ui/flutter/lib/i18n/langs/tr_tr.dart @@ -4,6 +4,7 @@ const trTR = { 'error': 'Hata', 'tip': 'İpucu', 'confirm': 'Onayla', + 'confirmDelete': 'Silmek istediğinizden emin misiniz?', 'cancel': 'İptal', 'on': 'Açık', 'off': 'Kapalı', @@ -22,7 +23,8 @@ const trTR = { 'downloadLink': 'İndirme Bağlantısı', 'downloadLinkValid': 'Lütfen indirme bağlantısını girin', 'downloadLinkHit': 'Lütfen her satıra bir indirme bağlantısı girin@append', - 'downloadLinkHitDesktop': ', veya doğrudan torrent dosyasını buraya sürükleyin', + 'downloadLinkHitDesktop': + ', veya doğrudan torrent dosyasını buraya sürükleyin', 'download': 'İndir', 'noFileSelected': 'Devam etmek için en az bir dosya seçin.', 'noStoragePermission': 'Depolama izni gerekli', @@ -39,12 +41,14 @@ const trTR = { 'defaultDirectDownload': 'Varsayılan olarak doğrudan indirmeyi seç', 'items': '@count öğe', 'subscribeTracker': 'Tracker Aboneliği', - 'subscribeFail': 'Abonelik başarısız oldu, lütfen ağı kontrol edin veya daha sonra tekrar deneyin', + 'subscribeFail': + 'Abonelik başarısız oldu, lütfen ağı kontrol edin veya daha sonra tekrar deneyin', 'update': 'Güncelle', 'updateDaily': 'Günlük güncelle', 'lastUpdate': 'Son güncelleme: @time', 'addTracker': 'Tracker Ekle', - 'addTrackerHit': 'Lütfen tracker sunucu URL’sini girin, her satıra bir tane', + 'addTrackerHit': + 'Lütfen tracker sunucu URL’sini girin, her satıra bir tane', 'ui': 'Arayüz', 'theme': 'Tema', 'themeSystem': 'Sistem', @@ -96,8 +100,8 @@ const trTR = { 'username': 'Kullanıcı Adı', 'password': 'Şifre', 'thanks': 'Teşekkürler', - 'thanksDesc': - 'Gopeed topluluğunun inşasında ve geliştirilmesinde katkıda bulunan herkese teşekkürler!', + 'thanksDesc': + 'Gopeed topluluğunun inşasında ve geliştirilmesinde katkıda bulunan herkese teşekkürler!', 'browserExtension': 'Tarayıcı Eklentisi', 'launchAtStartup': 'Başlangıçta Çalıştır', 'seedConfig': 'Seed Ayarları', @@ -117,5 +121,20 @@ const trTR = { 'fileSelectedSize': 'Boyut: ', 'httpHeaderName': 'Başlık Adı', 'httpHeaderValue': 'Başlık Değeri', + 'insertPlaceholder': 'Yer tutucu ekle', + 'placeholderYear': 'Geçerli yıl', + 'placeholderMonth': 'Geçerli ay (01-12)', + 'placeholderDay': 'Geçerli gün (01-31)', + 'placeholderDate': 'Tam tarih (YYYY-AA-GG)', + 'example': 'örn. @value', + 'downloadCategories': 'İndirme Kategorileri', + 'categoryMusic': 'Müzik', + 'categoryVideo': 'Video', + 'categoryDocument': 'Belge', + 'categoryProgram': 'Program', + 'categoryName': 'Kategori Adı', + 'categoryPath': 'Kategori Yolu', + 'builtInCategory': 'Yerleşik kategori silinemez', + 'selectCategory': 'Kategori Seç', }, }; diff --git a/ui/flutter/lib/i18n/langs/uk_ua.dart b/ui/flutter/lib/i18n/langs/uk_ua.dart index ee1b525a..09bd3875 100644 --- a/ui/flutter/lib/i18n/langs/uk_ua.dart +++ b/ui/flutter/lib/i18n/langs/uk_ua.dart @@ -4,6 +4,7 @@ const ukUA = { 'error': 'Помилка', 'tip': 'Підказка', 'confirm': 'Підтвердити', + 'confirmDelete': 'Ви впевнені, що хочете видалити?', 'cancel': 'Скасувати', 'on': 'Увімк.', 'off': 'Вимк.', @@ -119,5 +120,20 @@ const ukUA = { 'fileSelectedSize': 'Розмір: ', 'httpHeaderName': 'Назва заголовка HTTP', 'httpHeaderValue': 'Значення заголовка HTTP', + 'insertPlaceholder': 'Вставити заповнювач', + 'placeholderYear': 'Поточний рік', + 'placeholderMonth': 'Поточний місяць (01-12)', + 'placeholderDay': 'Поточний день (01-31)', + 'placeholderDate': 'Повна дата (РРРР-ММ-ДД)', + 'example': 'напр. @value', + 'downloadCategories': 'Категорії завантажень', + 'categoryMusic': 'Музика', + 'categoryVideo': 'Відео', + 'categoryDocument': 'Документ', + 'categoryProgram': 'Програма', + 'categoryName': 'Назва категорії', + 'categoryPath': 'Шлях категорії', + 'builtInCategory': 'Вбудовану категорію неможливо видалити', + 'selectCategory': 'Вибрати категорію', }, }; diff --git a/ui/flutter/lib/i18n/langs/vi_vn.dart b/ui/flutter/lib/i18n/langs/vi_vn.dart index 309fdf69..4eae4e0e 100644 --- a/ui/flutter/lib/i18n/langs/vi_vn.dart +++ b/ui/flutter/lib/i18n/langs/vi_vn.dart @@ -4,6 +4,7 @@ const viVN = { 'error': 'Lỗi', 'tip': 'Mẹo', 'confirm': 'Xác nhận', + 'confirmDelete': 'Bạn có chắc chắn muốn xóa?', 'cancel': 'Hủy', 'on': 'Bật', 'off': 'Tắt', @@ -90,5 +91,20 @@ const viVN = { 'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!', 'browserExtension': 'Tiện ích mở rộng trình duyệt', 'skipVerifyCert': 'Bỏ qua xác thực chứng chỉ', + 'insertPlaceholder': 'Chèn placeholder', + 'placeholderYear': 'Năm hiện tại', + 'placeholderMonth': 'Tháng hiện tại (01-12)', + 'placeholderDay': 'Ngày hiện tại (01-31)', + 'placeholderDate': 'Ngày đầy đủ (YYYY-MM-DD)', + 'example': 'vd. @value', + 'downloadCategories': 'Danh mục tải xuống', + 'categoryMusic': 'Nhạc', + 'categoryVideo': 'Video', + 'categoryDocument': 'Tài liệu', + 'categoryProgram': 'Chương trình', + 'categoryName': 'Tên danh mục', + 'categoryPath': 'Đường dẫn danh mục', + 'builtInCategory': 'Không thể xóa danh mục tích hợp', + 'selectCategory': 'Chọn danh mục', }, }; diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index 252ddc00..e778add9 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -4,6 +4,7 @@ const zhCN = { 'error': '错误', 'tip': '提示', 'confirm': '确认', + 'confirmDelete': '确定要删除吗?', 'cancel': '取消', 'on': '开启', 'off': '关闭', @@ -65,6 +66,7 @@ const zhCN = { 'developer': '开发者', 'logDirectory': '日志目录', 'webhook': 'Webhook 推送', + 'webhookEnable': '启用 Webhook', 'webhookDesc': '在任务完成或失败时发送 HTTP POST 通知', 'webhookUrlHint': '请输入 Webhook URL', 'webhookTest': '测试', @@ -133,5 +135,20 @@ const zhCN = { 'login_success': '登录成功', 'login_failed': '登录失败,请检查用户名和密码', 'login_failed_network': '登录失败,请检查网络连接', + 'insertPlaceholder': '插入占位符', + 'placeholderYear': '当前年份', + 'placeholderMonth': '当前月份 (01-12)', + 'placeholderDay': '当前日期 (01-31)', + 'placeholderDate': '完整日期 (年-月-日)', + 'example': '例如 @value', + 'downloadCategories': '下载目录分类', + 'categoryMusic': '音乐', + 'categoryVideo': '视频', + 'categoryDocument': '文档', + 'categoryProgram': '程序', + 'categoryName': '分类名称', + 'categoryPath': '分类路径', + 'builtInCategory': '内置分类不能删除', + 'selectCategory': '选择分类', } }; diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 35701c18..51d18a17 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -4,6 +4,7 @@ const zhTW = { 'error': '錯誤', 'tip': '提示', 'confirm': '確認', + 'confirmDelete': '確定要刪除嗎?', 'cancel': '取消', 'on': '開啟', 'off': '關閉', @@ -116,5 +117,20 @@ const zhTW = { 'fileSelectedSize': '大小:', 'httpHeaderName': '標頭名稱', 'httpHeaderValue': '標頭值', + 'insertPlaceholder': '插入佔位符', + 'placeholderYear': '當前年份', + 'placeholderMonth': '當前月份 (01-12)', + 'placeholderDay': '當前日期 (01-31)', + 'placeholderDate': '完整日期 (年-月-日)', + 'example': '例如 @value', + 'downloadCategories': '下載目錄分類', + 'categoryMusic': '音樂', + 'categoryVideo': '影片', + 'categoryDocument': '文件', + 'categoryProgram': '程式', + 'categoryName': '分類名稱', + 'categoryPath': '分類路徑', + 'builtInCategory': '內建分類無法刪除', + 'selectCategory': '選擇分類', } };