feat: add download directory placeholders, category support (#1188)

This commit is contained in:
Copilot authored and GitHub committed 2025-11-27 18:39:08 +08:00
1 parent 9d9f1caaaa
commit 49b4d99b5e
31 files changed
+1204 -118

No files matched your search

+13
View File
@@ -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
+7 -1
View File
@@ -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)
+3
View File
@@ -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
+21 -2
View File
@@ -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
}
+75 -47
View File
@@ -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
+26
View File
@@ -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
}
+75
View File
@@ -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)
}
})
}
}
@@ -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<String> webhookUrls;
List<DownloadCategory> 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<String, dynamic>? json) =>
@@ -102,6 +103,44 @@ class ExtraConfig {
Map<String, dynamic> 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<String, dynamic> json) =>
_$DownloadCategoryFromJson(json);
Map<String, dynamic> toJson() => _$DownloadCategoryToJson(this);
}
@JsonSerializable()
class WebhookConfig {
bool enable;
List<String> urls;
WebhookConfig({
this.enable = false,
this.urls = const [],
});
factory WebhookConfig.fromJson(Map<String, dynamic>? json) =>
json == null ? WebhookConfig() : _$WebhookConfigFromJson(json);
Map<String, dynamic> toJson() => _$WebhookConfigToJson(this);
}
@JsonSerializable()
class ProxyConfig {
bool enable;
@@ -14,7 +14,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>?)
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?)
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>);
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>)
..webhook = WebhookConfig.fromJson(json['webhook'] as Map<String, dynamic>?);
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
<String, dynamic>{
@@ -23,6 +24,7 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
'protocolConfig': instance.protocolConfig.toJson(),
'extra': instance.extra.toJson(),
'proxy': instance.proxy.toJson(),
'webhook': instance.webhook.toJson(),
};
ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) =>
@@ -75,8 +77,8 @@ 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)
downloadCategories: (json['downloadCategories'] as List<dynamic>?)
?.map((e) => DownloadCategory.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
@@ -89,10 +91,53 @@ Map<String, dynamic> _$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<String, dynamic> 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<String, dynamic> _$DownloadCategoryToJson(DownloadCategory instance) {
final val = <String, dynamic>{
'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<String, dynamic> json) =>
WebhookConfig(
enable: json['enable'] as bool? ?? false,
urls: (json['urls'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$WebhookConfigToJson(WebhookConfig instance) =>
<String, dynamic>{
'enable': instance.enable,
'urls': instance.urls,
};
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
enable: json['enable'] as bool? ?? false,
system: json['system'] as bool? ?? false,
@@ -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<void> _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<void> _initLaunchAtStartup() async {
@@ -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<CreateController> {
.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<CreateController> {
DirectorySelector(
controller: _pathController,
),
// Category selector
_buildCategorySelector(appController),
Obx(
() => Visibility(
visible: controller.showAdvanced.value,
@@ -912,4 +917,66 @@ class CreateView extends GetView<CreateController> {
],
));
}
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(),
),
),
),
],
),
);
}
}
@@ -35,6 +35,14 @@ final _divider = const Divider().paddingOnly(left: 10, right: 10);
class SettingView extends GetView<SettingController> {
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<AppController>();
@@ -71,20 +79,25 @@ class SettingView extends GetView<SettingController> {
'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<SettingController> {
);
});
// 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<bool>(
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<SettingController> {
// 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<SettingController> {
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<String>.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<SettingController> {
child: Column(
children: _addDivider([
buildDownloadDir(),
buildDownloadCategories(),
buildMaxRunning(),
buildDefaultDirectDownload(),
buildBrowserExtension(),
@@ -1205,12 +1365,16 @@ class SettingView extends GetView<SettingController> {
saveController.start();
try {
// Create new list to avoid unmodifiable list error
final urls = List<String>.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<SettingController> {
return 'themeSystem'.tr;
}
}
void _showCategoryDialog(
BuildContext context,
Future<bool> Function() debounceSave,
Rx<DownloaderConfig> 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 {
+181 -17
View File
@@ -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<PathPlaceholder> 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<DirectorySelector> createState() => _DirectorySelectorState();
@@ -108,22 +177,117 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return null;
}
Widget? buildPlaceholderButton() {
if (!widget.showPlaceholderButton) return null;
return PopupMenuButton<String>(
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<String>(
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<TextEditingValue>(
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(),
);
}
+26 -5
View File
@@ -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',
},
};
+17
View File
@@ -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',
},
};
+23 -4
View File
@@ -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',
},
};
};
+17 -1
View File
@@ -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': 'انتخاب دسته‌بندی',
}
};
+29 -7
View File
@@ -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',
},
};
+24 -4
View File
@@ -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',
},
};
+18 -1
View File
@@ -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',
},
};
+16
View File
@@ -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',
},
};
+16
View File
@@ -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': 'カテゴリを選択',
}
};
+16
View File
@@ -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ę',
},
};
+18 -1
View File
@@ -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',
},
};
+16
View File
@@ -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': 'Выбрать категорию',
}
};
+18 -1
View File
@@ -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': 'வகையைத் தேர்ந்தெடுக்கவும்',
},
};
+24 -5
View File
@@ -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ç',
},
};
+16
View File
@@ -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': 'Вибрати категорію',
},
};
+16
View File
@@ -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',
},
};
+17
View File
@@ -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': '选择分类',
}
};
+16
View File
@@ -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': '選擇分類',
}
};