-
-
-
-
-
-
-
-
+ {activeTab === 'options' && isTorrent &&
+
+
+
{t($ => $.properties.tabs.options)}
+
{t($ => $.properties.torrentPeerOptionsSavedHint)}
+
+
{t($ => $.properties.blankUsesDefault)}
-
-
-
-
-
-
-
{t($ => $.properties.torrentPeerOptionsSavedHint)}
+
+
+
+
+
{t($ => $.properties.liveTorrentPeerOptions)}
+
{t($ => $.properties.speedLimitHint)}
+
+
+
+
+
+
+
+
+
{t($ => $.addDownloads.torrentSeeding)}
+
{t($ => $.addDownloads.seedRatioHint)}
+
+
+
+
+
+
+
+
+
{t($ => $.properties.torrentOptionsBehavior)}
+
{t($ => $.properties.torrentOptionsBehaviorHint)}
+
+
+
+
$.properties.torrentVerifyIntegrity)}
+ hint={t($ => $.properties.torrentVerifyIntegrityHint)}
+ checked={checkIntegrity}
+ onChange={checked => { setCheckIntegrity(checked); setDraftTab('options'); }}
+ disabled={!editingEnabled}
+ />
+ $.properties.torrentRemoveUnselectedFile)}
+ hint={t($ => $.properties.torrentRemoveUnselectedFileHint)}
+ checked={removeUnselectedFile}
+ onChange={checked => { setRemoveUnselectedFile(checked); setDraftTab('options'); }}
+ disabled={!editingEnabled || (!removeUnselectedFile && (!snapshot.torrentFileIndices || snapshot.torrentFileIndices.length === 0))}
+ />
+ $.properties.torrentFileAllocation)}
+ controlId="properties-options-file-allocation"
+ hint={t($ => $.properties.torrentFileAllocationHint)}
+ meta={fileAllocation === '' ? t($ => $.properties.usingDefault) : t($ => $.properties.customPerDownload)}
+ >
+
+
+ $.properties.torrentEncryptionPolicy)}
+ controlId="properties-options-encryption-policy"
+ hint={t($ => $.properties.torrentEncryptionPolicyHint)}
+ meta={encryptionPolicy === '' ? t($ => $.properties.usingDefault) : t($ => $.properties.customPerDownload)}
+ >
+
+
+
+
}
{activeTab === 'advanced' &&
diff --git a/src/components/PropertiesWindowBridgeHost.tsx b/src/components/PropertiesWindowBridgeHost.tsx
index 23641f4..12242f9 100644
--- a/src/components/PropertiesWindowBridgeHost.tsx
+++ b/src/components/PropertiesWindowBridgeHost.tsx
@@ -25,9 +25,11 @@ import {
beginExclusivePropertiesAction,
classifyPropertiesActionRequest,
createFrameCoalescer,
+ decodePropertiesPatchValue,
enqueuePropertiesAction,
getPropertiesLifecycleAction,
propertiesActionRequestKey,
+ PROPERTIES_PATCH_CLEARABLE_KEYS,
sanitizePropertiesSnapshot,
sendPropertiesActionResult,
sendPropertiesRemoved,
@@ -86,6 +88,13 @@ const copyEditablePatch = (rawPatch: PropertiesPatch): Partial =>
'torrentFileAllocation',
] as const) copy(key);
+ for (const key of PROPERTIES_PATCH_CLEARABLE_KEYS) {
+ if (Object.prototype.hasOwnProperty.call(rawPatch, key)) {
+ const value = (rawPatch as Record)[key];
+ (safePatch as Record)[key] = decodePropertiesPatchValue(value);
+ }
+ }
+
if (safePatch.fileName !== undefined && typeof safePatch.fileName !== 'string') {
throw new Error('Invalid file name');
}
diff --git a/src/i18n/catalogs/en.ts b/src/i18n/catalogs/en.ts
index 9260a62..6612da5 100644
--- a/src/i18n/catalogs/en.ts
+++ b/src/i18n/catalogs/en.ts
@@ -247,6 +247,20 @@ const common = {
connectionCountUnknown: '—/{{total}}',
connectionsUnavailable: '—',
speedCap: 'Speed cap',
+ inputFormat: 'Format: {{format}}',
+ inputFormatSpeedLimit: '512K, 2M, or 1G',
+ inputFormatMaxPeers: '0–1000; 0 means unlimited',
+ inputFormatSeedTime: 'minutes, e.g. 60',
+ inputFormatSeedRatio: 'decimal, e.g. 1.5; 0 means time-only',
+ inputFormatStopTimeout: 'whole seconds; 0 disables it',
+ inputFormatPiecePriority: 'head=1M,tail=1M',
+ inputExampleSpeedLimit: 'e.g. 512K',
+ inputExampleMaxPeers: 'e.g. 55',
+ inputExampleSeedTime: 'e.g. 60',
+ inputExampleSeedRatio: 'e.g. 1.5',
+ inputExampleStopTimeout: 'e.g. 300',
+ inputExamplePiecePriority: 'e.g. head=1M,tail=1M',
+ speedLimitHint: 'Leave blank to use the global default; enter a value to set a cap for this download.',
liveSpeedLimit: 'Live speed cap',
liveSpeedLimitHint: 'Applies to active normal downloads only. Media downloads cannot be changed while running.',
liveSpeedLimitPlaceholder: 'e.g. 1024K',
@@ -336,6 +350,7 @@ const common = {
torrentUploaded: 'Uploaded',
torrentRatio: 'Ratio',
torrentSeededDuration: 'Seeded',
+ torrentSeedTimeHint: 'How long this Torrent may continue seeding after its files finish downloading. Leave blank to use the default.',
torrentConnectedPeers: 'Peers',
torrentSeeders: 'Seeders',
torrentUploadSpeed: 'Upload speed',
@@ -354,6 +369,8 @@ const common = {
torrentFileAllocationPrealloc: 'Preallocate files',
torrentFileAllocationNone: 'Allocate as needed',
torrentFileAllocationHint: 'Preallocation reserves the selected files before transfer. Allocation as needed avoids that upfront disk reservation.',
+ torrentOptionsBehavior: 'Torrent behavior',
+ torrentOptionsBehaviorHint: 'Controls verification, storage allocation, encryption, and cleanup for this Torrent.',
torrentDetails: 'Torrent details',
torrentCopyMagnet: 'Copy magnet link',
torrentExportMetadata: 'Export .torrent',
@@ -412,6 +429,9 @@ const common = {
defaultValue: ' (default)',
savedTooltip: 'Saved for this download; Settings changes apply to new downloads.',
defaultTooltip: 'Using the current default for new downloads.',
+ blankUsesDefault: 'Leave blank · use default',
+ usingDefault: 'Using default',
+ customPerDownload: 'Custom for this download',
identityReadOnly: 'File identity is read-only. Transfer settings are saved for redownload.',
transferSettings: 'Transfer settings can be changed after stopping or pausing. Current transfers keep their existing backend options.',
download: 'Download',
diff --git a/src/i18n/catalogs/fa.ts b/src/i18n/catalogs/fa.ts
index 3b2df1d..52a806a 100644
--- a/src/i18n/catalogs/fa.ts
+++ b/src/i18n/catalogs/fa.ts
@@ -247,6 +247,20 @@ const fa = {
connectionCountUnknown: '—/{{total}} فعال',
connectionsUnavailable: '—',
speedCap: 'سقف سرعت',
+ inputFormat: 'قالب: {{format}}',
+ inputFormatSpeedLimit: '512K، 2M یا 1G',
+ inputFormatMaxPeers: '0 تا 1000؛ 0 یعنی نامحدود',
+ inputFormatSeedTime: 'دقیقه، مثلاً 60',
+ inputFormatSeedRatio: 'عدد اعشاری، مثلاً 1.5؛ 0 یعنی فقط بر اساس زمان',
+ inputFormatStopTimeout: 'تعداد ثانیهٔ کامل؛ 0 غیرفعال میکند',
+ inputFormatPiecePriority: 'head=1M,tail=1M',
+ inputExampleSpeedLimit: 'مثلاً 512K',
+ inputExampleMaxPeers: 'مثلاً 55',
+ inputExampleSeedTime: 'مثلاً 60',
+ inputExampleSeedRatio: 'مثلاً 1.5',
+ inputExampleStopTimeout: 'مثلاً 300',
+ inputExamplePiecePriority: 'مثلاً head=1M,tail=1M',
+ speedLimitHint: 'برای استفاده از مقدار سراسری خالی بگذارید؛ برای این دانلود یک سقف مشخص وارد کنید.',
liveSpeedLimit: 'سقف سرعت زنده',
liveSpeedLimitHint: 'فقط برای دانلودهای عادیِ فعال اعمال میشود. سرعت دانلودهای رسانهای هنگام اجرا قابل تغییر نیست.',
liveSpeedLimitPlaceholder: 'مثلاً 1024K',
@@ -336,6 +350,7 @@ const fa = {
torrentUploaded: 'آپلودشده',
torrentRatio: 'نسبت',
torrentSeededDuration: 'مدت سید',
+ torrentSeedTimeHint: 'مدتی که تورنت پس از تکمیل دانلود به سید ادامه میدهد. برای استفاده از پیشفرض خالی بگذارید.',
torrentConnectedPeers: 'همتاها',
torrentSeeders: 'سیدها',
torrentUploadSpeed: 'سرعت آپلود',
@@ -354,6 +369,8 @@ const fa = {
torrentFileAllocationPrealloc: 'تخصیص از پیش',
torrentFileAllocationNone: 'تخصیص هنگام نیاز',
torrentFileAllocationHint: 'تخصیص از پیش فضای فایلهای انتخابشده را قبل از انتقال رزرو میکند؛ تخصیص هنگام نیاز این رزرو اولیه را انجام نمیدهد.',
+ torrentOptionsBehavior: 'رفتار تورنت',
+ torrentOptionsBehaviorHint: 'بررسی صحت، تخصیص فضا، رمزنگاری و پاکسازی این تورنت را کنترل میکند.',
torrentDetails: 'جزئیات تورنت',
torrentCopyMagnet: 'کپی پیوند مگنت',
torrentExportMetadata: 'خروجی .torrent',
@@ -412,6 +429,9 @@ const fa = {
defaultValue: ' (پیشفرض)',
savedTooltip: 'برای این دانلود ذخیرهشده است؛ تغییرات تنظیمات روی دانلودهای جدید اعمال میشود.',
defaultTooltip: 'استفاده از پیشفرض کنونی برای دانلودهای جدید.',
+ blankUsesDefault: 'خالی = استفاده از پیشفرض',
+ usingDefault: 'استفاده از پیشفرض',
+ customPerDownload: 'سفارشی برای این دانلود',
identityReadOnly: 'هویت فایل فقطخواندنی است. تنظیمات انتقال برای دانلود مجدد ذخیره میشوند.',
transferSettings: 'تنظیمات انتقال را میتوان پس از توقف تغییر داد. انتقالهای کنونی گزینههای فعلی خود را حفظ میکنند.',
download: 'دانلود',
diff --git a/src/i18n/catalogs/he.ts b/src/i18n/catalogs/he.ts
index 525ab75..306c4ec 100644
--- a/src/i18n/catalogs/he.ts
+++ b/src/i18n/catalogs/he.ts
@@ -247,6 +247,20 @@ const he = {
connectionCountUnknown: '—/{{total}} פעילות',
connectionsUnavailable: '—',
speedCap: 'הגבלת מהירות',
+ inputFormat: 'תבנית: {{format}}',
+ inputFormatSpeedLimit: '512K, 2M או 1G',
+ inputFormatMaxPeers: '0–1000; 0 פירושו ללא הגבלה',
+ inputFormatSeedTime: 'בדקות, למשל 60',
+ inputFormatSeedRatio: 'מספר עשרוני, למשל 1.5; 0 פירושו לפי זמן בלבד',
+ inputFormatStopTimeout: 'שניות שלמות; 0 משבית',
+ inputFormatPiecePriority: 'head=1M,tail=1M',
+ inputExampleSpeedLimit: 'לדוגמה 512K',
+ inputExampleMaxPeers: 'לדוגמה 55',
+ inputExampleSeedTime: 'לדוגמה 60',
+ inputExampleSeedRatio: 'לדוגמה 1.5',
+ inputExampleStopTimeout: 'לדוגמה 300',
+ inputExamplePiecePriority: 'לדוגמה head=1M,tail=1M',
+ speedLimitHint: 'השאר ריק כדי להשתמש בערך הגלובלי, או הזן הגבלה עבור הורדה זו.',
liveSpeedLimit: 'הגבלת מהירות בזמן אמת',
liveSpeedLimitHint: 'חל על הורדות רגילות פעילות בלבד. אי אפשר לשנות הורדות מדיה בזמן שהן פועלות.',
liveSpeedLimitPlaceholder: 'לדוגמה 1024K',
@@ -336,6 +350,7 @@ const he = {
torrentUploaded: 'הועלה',
torrentRatio: 'יחס',
torrentSeededDuration: 'משך שיתוף',
+ torrentSeedTimeHint: 'משך הזמן שבו הטורנט ימשיך לשתף לאחר סיום ההורדה. השאר ריק כדי להשתמש בברירת המחדל.',
torrentConnectedPeers: 'עמיתים',
torrentSeeders: 'משתפים',
torrentUploadSpeed: 'מהירות העלאה',
@@ -354,6 +369,8 @@ const he = {
torrentFileAllocationPrealloc: 'הקצאה מראש',
torrentFileAllocationNone: 'הקצאה לפי הצורך',
torrentFileAllocationHint: 'הקצאה מראש שומרת מקום לקבצים לפני ההעברה; הקצאה לפי הצורך נמנעת מהשמירה הראשונית.',
+ torrentOptionsBehavior: 'התנהגות Torrent',
+ torrentOptionsBehaviorHint: 'שולט באימות, הקצאת האחסון, ההצפנה והניקוי של Torrent זה.',
torrentDetails: 'פרטי טורנט',
torrentCopyMagnet: 'העתקת קישור מגנט',
torrentExportMetadata: 'ייצוא .torrent',
@@ -412,6 +429,9 @@ const he = {
defaultValue: ' (ברירת מחדל)',
savedTooltip: 'נשמר עבור הורדה זו; שינויים בהגדרות יחולו על הורדות חדשות.',
defaultTooltip: 'שימוש בברירת המחדל הנוכחית להורדות חדשות.',
+ blankUsesDefault: 'ריק · שימוש בברירת המחדל',
+ usingDefault: 'ברירת מחדל',
+ customPerDownload: 'מותאם להורדה זו',
identityReadOnly: 'זהות הקובץ היא לקריאה בלבד. הגדרות ההעברה נשמרות להורדה מחדש.',
transferSettings: 'ניתן לשנות את הגדרות ההעברה לאחר עצירה או השהייה. העברות נוכחיות שומרות על אפשרויות המנוע הקיימות שלהן.',
download: 'הורדה',
diff --git a/src/i18n/catalogs/ru.ts b/src/i18n/catalogs/ru.ts
index 763ca7f..eef79cb 100644
--- a/src/i18n/catalogs/ru.ts
+++ b/src/i18n/catalogs/ru.ts
@@ -247,6 +247,20 @@ const ru = {
connectionCountUnknown: '—/{{total}} активных',
connectionsUnavailable: '—',
speedCap: 'Ограничение скорости',
+ inputFormat: 'Формат: {{format}}',
+ inputFormatSpeedLimit: '512K, 2M или 1G',
+ inputFormatMaxPeers: '0–1000; 0 — без ограничений',
+ inputFormatSeedTime: 'минуты, например 60',
+ inputFormatSeedRatio: 'десятичное число, например 1.5; 0 — только по времени',
+ inputFormatStopTimeout: 'целые секунды; 0 отключает',
+ inputFormatPiecePriority: 'head=1M,tail=1M',
+ inputExampleSpeedLimit: 'например, 512K',
+ inputExampleMaxPeers: 'например, 55',
+ inputExampleSeedTime: 'например, 60',
+ inputExampleSeedRatio: 'например, 1.5',
+ inputExampleStopTimeout: 'например, 300',
+ inputExamplePiecePriority: 'например, head=1M,tail=1M',
+ speedLimitHint: 'Оставьте пустым для глобального значения по умолчанию или задайте ограничение для этой загрузки.',
liveSpeedLimit: 'Текущее ограничение скорости',
liveSpeedLimitHint: 'Применяется только к активным обычным загрузкам. Скорость медиазагрузок нельзя изменить во время работы.',
liveSpeedLimitPlaceholder: 'например, 1024K',
@@ -336,6 +350,7 @@ const ru = {
torrentUploaded: 'Отдано',
torrentRatio: 'Коэффициент',
torrentSeededDuration: 'Время раздачи',
+ torrentSeedTimeHint: 'Как долго Torrent продолжает раздачу после завершения загрузки. Оставьте пустым для значения по умолчанию.',
torrentConnectedPeers: 'Пиры',
torrentSeeders: 'Сиды',
torrentUploadSpeed: 'Скорость отдачи',
@@ -354,6 +369,8 @@ const ru = {
torrentFileAllocationPrealloc: 'Предварительное выделение',
torrentFileAllocationNone: 'Выделять по мере необходимости',
torrentFileAllocationHint: 'Предварительное выделение резервирует место до передачи; выделение по мере необходимости не делает начальное резервирование.',
+ torrentOptionsBehavior: 'Поведение Torrent',
+ torrentOptionsBehaviorHint: 'Управляет проверкой, выделением места, шифрованием и очисткой этого Torrent.',
torrentEncryptionPolicyHint: 'Применяется при запуске или повторной попытке Torrent. Выберите одну политику, чтобы параметры handshake и шифрования payload оставались согласованными.',
torrentDetails: 'Сведения о Torrent',
torrentCopyMagnet: 'Копировать magnet-ссылку',
@@ -412,6 +429,9 @@ const ru = {
defaultValue: ' (по умолчанию)',
savedTooltip: 'Сохранено для этой загрузки. Изменения в Настройках будут применяться к новым загрузкам.',
defaultTooltip: 'Используется текущее значение по умолчанию для новых загрузок.',
+ blankUsesDefault: 'Пусто · значение по умолчанию',
+ usingDefault: 'По умолчанию',
+ customPerDownload: 'Для этой загрузки',
identityReadOnly: 'Идентификация файла доступна только для чтения. Настройки передачи сохранены для повторного скачивания.',
transferSettings: 'Настройки передачи можно изменить после остановки или приостановки. Текущие загрузки сохраняют свои параметры.',
download: 'Загрузка',
diff --git a/src/i18n/catalogs/uk.ts b/src/i18n/catalogs/uk.ts
index 9c8e729..ac6308f 100644
--- a/src/i18n/catalogs/uk.ts
+++ b/src/i18n/catalogs/uk.ts
@@ -247,6 +247,20 @@ const uk = {
connectionCountUnknown: '—/{{total}} активних',
connectionsUnavailable: '—',
speedCap: 'Обмеження швидкості',
+ inputFormat: 'Формат: {{format}}',
+ inputFormatSpeedLimit: '512K, 2M або 1G',
+ inputFormatMaxPeers: '0–1000; 0 означає без обмежень',
+ inputFormatSeedTime: 'хвилини, наприклад 60',
+ inputFormatSeedRatio: 'десяткове число, наприклад 1.5; 0 — лише за часом',
+ inputFormatStopTimeout: 'цілі секунди; 0 вимикає',
+ inputFormatPiecePriority: 'head=1M,tail=1M',
+ inputExampleSpeedLimit: 'наприклад, 512K',
+ inputExampleMaxPeers: 'наприклад, 55',
+ inputExampleSeedTime: 'наприклад, 60',
+ inputExampleSeedRatio: 'наприклад, 1.5',
+ inputExampleStopTimeout: 'наприклад, 300',
+ inputExamplePiecePriority: 'наприклад, head=1M,tail=1M',
+ speedLimitHint: 'Залиште порожнім для глобального значення за замовчуванням або задайте обмеження для цього завантаження.',
liveSpeedLimit: 'Поточне обмеження швидкості',
liveSpeedLimitHint: 'Застосовується лише до активних звичайних завантажень. Швидкість медіазавантажень не можна змінити під час роботи.',
liveSpeedLimitPlaceholder: 'наприклад, 1024K',
@@ -336,6 +350,7 @@ const uk = {
torrentUploaded: 'Віддано',
torrentRatio: 'Коефіцієнт',
torrentSeededDuration: 'Час роздачі',
+ torrentSeedTimeHint: 'Як довго Torrent продовжує роздачу після завершення завантаження. Залиште порожнім для значення за замовчуванням.',
torrentConnectedPeers: 'Піри',
torrentSeeders: 'Сіди',
torrentUploadSpeed: 'Швидкість віддачі',
@@ -354,6 +369,8 @@ const uk = {
torrentFileAllocationPrealloc: 'Попереднє виділення',
torrentFileAllocationNone: 'Виділяти за потреби',
torrentFileAllocationHint: 'Попереднє виділення резервує місце до передачі; виділення за потреби не робить початкового резервування.',
+ torrentOptionsBehavior: 'Поведінка Torrent',
+ torrentOptionsBehaviorHint: 'Керує перевіркою, виділенням місця, шифруванням і очищенням цього Torrent.',
torrentEncryptionPolicyHint: 'Застосовується під час запуску або повторної спроби Torrent. Виберіть одну політику, щоб параметри handshake і шифрування payload залишалися узгодженими.',
torrentDetails: 'Відомості про Torrent',
torrentCopyMagnet: 'Копіювати magnet-посилання',
@@ -412,6 +429,9 @@ const uk = {
defaultValue: ' (за замовчуванням)',
savedTooltip: 'Збережено для цього завантаження; зміни в налаштуваннях застосовуються до нових завантажень.',
defaultTooltip: 'Використовується поточне значення за замовчуванням для нових завантажень.',
+ blankUsesDefault: 'Порожньо · значення за замовчуванням',
+ usingDefault: 'За замовчуванням',
+ customPerDownload: 'Для цього завантаження',
identityReadOnly: 'Ідентифікатор файлу доступний лише для читання. Налаштування передачі збережено для повторного завантаження.',
transferSettings: 'Налаштування передачі можна змінити після зупинки або призупинення. Поточні передачі зберігають свої існуючі налаштування бекенду.',
download: 'Завантаження',
diff --git a/src/i18n/catalogs/zh-CN.ts b/src/i18n/catalogs/zh-CN.ts
index 7233271..25c145a 100644
--- a/src/i18n/catalogs/zh-CN.ts
+++ b/src/i18n/catalogs/zh-CN.ts
@@ -247,6 +247,20 @@ const zhCN = {
connectionCountUnknown: '—/{{total}} 个连接',
connectionsUnavailable: '—',
speedCap: '速度上限',
+ inputFormat: '格式:{{format}}',
+ inputFormatSpeedLimit: '512K、2M 或 1G',
+ inputFormatMaxPeers: '0–1000;0 表示不限',
+ inputFormatSeedTime: '分钟,例如 60',
+ inputFormatSeedRatio: '小数,例如 1.5;0 表示仅按时间',
+ inputFormatStopTimeout: '整数秒;0 表示禁用',
+ inputFormatPiecePriority: 'head=1M,tail=1M',
+ inputExampleSpeedLimit: '例如 512K',
+ inputExampleMaxPeers: '例如 55',
+ inputExampleSeedTime: '例如 60',
+ inputExampleSeedRatio: '例如 1.5',
+ inputExampleStopTimeout: '例如 300',
+ inputExamplePiecePriority: '例如 head=1M,tail=1M',
+ speedLimitHint: '留空以使用全局默认值;输入数值可为此下载设置上限。',
liveSpeedLimit: '实时速度上限',
liveSpeedLimitHint: '仅适用于正在进行的普通下载。媒体下载运行时无法更改速度。',
liveSpeedLimitPlaceholder: '例如 1024K',
@@ -336,6 +350,7 @@ const zhCN = {
torrentUploaded: '已上传',
torrentRatio: '分享率',
torrentSeededDuration: '做种时长',
+ torrentSeedTimeHint: '文件下载完成后继续做种的时长。留空以使用默认值。',
torrentConnectedPeers: '连接数',
torrentSeeders: '种子数',
torrentUploadSpeed: '上传速度',
@@ -354,6 +369,8 @@ const zhCN = {
torrentFileAllocationPrealloc: '预分配文件',
torrentFileAllocationNone: '按需分配',
torrentFileAllocationHint: '预分配会在传输前为选中文件预留空间;按需分配不会进行初始磁盘预留。',
+ torrentOptionsBehavior: 'Torrent 行为',
+ torrentOptionsBehaviorHint: '控制此 Torrent 的校验、存储分配、加密和清理。',
torrentEncryptionPolicyHint: '在 Torrent 启动或重试时应用。选择单一策略,确保握手和 payload 加密设置保持一致。',
torrentDetails: 'Torrent 详细信息',
torrentCopyMagnet: '复制磁力链接',
@@ -412,6 +429,9 @@ const zhCN = {
defaultValue: ' (默认)',
savedTooltip: '已为此下载保存;设置中的更改将应用于新的下载。',
defaultTooltip: '对新的下载使用当前默认值。',
+ blankUsesDefault: '留空 · 使用默认值',
+ usingDefault: '使用默认值',
+ customPerDownload: '此下载的自定义值',
identityReadOnly: '文件标识为只读。传输设置会保存以备重新下载使用。',
transferSettings: '停止或暂停后可以更改传输设置。当前的传输会保留其现有的后端选项。',
download: '下载',
diff --git a/src/i18n/resources.test.ts b/src/i18n/resources.test.ts
index 51d921e..7c2929b 100644
--- a/src/i18n/resources.test.ts
+++ b/src/i18n/resources.test.ts
@@ -112,6 +112,7 @@ describe('translation catalogs', () => {
'settings.network.firefoxMacos',
'settings.network.safariMacos',
'settings.network.torrentExternalIpPlaceholder',
+ 'properties.inputFormatPiecePriority',
]);
const unexpectedDuplicates = duplicates
diff --git a/src/index.css b/src/index.css
index 257efb3..153e347 100644
--- a/src/index.css
+++ b/src/index.css
@@ -572,8 +572,8 @@ html[data-list-density="relaxed"] {
}
.properties-window-shell {
- --properties-header-surface: hsl(var(--surface-raised));
- --properties-card-surface: hsl(var(--bg-input) / 0.42);
+ --properties-header-surface: hsl(var(--main-bg));
+ --properties-card-surface: hsl(var(--surface-raised) / 0.38);
position: relative;
min-height: 100%;
overflow: hidden;
@@ -612,8 +612,10 @@ html[data-list-density="relaxed"] {
}
.properties-window-header {
- background: var(--properties-header-surface);
- box-shadow: inset 0 -1px 0 hsl(0 0% 100% / 0.025);
+ background:
+ radial-gradient(circle at 100% 0%, hsl(var(--accent-color) / 0.055), transparent 38%),
+ var(--properties-header-surface);
+ box-shadow: inset 0 -1px 0 hsl(var(--text-primary) / 0.025);
}
.properties-window-hero-top {
@@ -793,7 +795,8 @@ html[data-list-density="relaxed"] {
padding: 8px 10px;
border: 1px solid hsl(var(--border-modal) / 0.72);
border-radius: 8px;
- background: hsl(var(--bg-input) / 0.28);
+ background: var(--properties-card-surface);
+ box-shadow: inset 0 1px 0 hsl(var(--text-primary) / 0.025);
}
.properties-metric-card > svg {
@@ -856,7 +859,7 @@ html[data-list-density="relaxed"] {
gap: 10px;
padding: 7px 16px;
border-bottom: 1px solid hsl(var(--border-modal));
- background: hsl(var(--surface-raised));
+ background: hsl(var(--surface-raised) / 0.42);
}
.properties-window-tab-navigation--overflow .properties-window-tabs {
@@ -968,6 +971,209 @@ html[data-list-density="relaxed"] {
border-radius: 8px;
}
+ .properties-options {
+ max-width: 940px;
+ }
+
+ .properties-options-intro,
+ .properties-option-group {
+ border: 1px solid hsl(var(--border-modal) / 0.82);
+ border-radius: 12px;
+ background: hsl(var(--surface-raised) / 0.22);
+ }
+
+ .properties-options-intro {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 18px;
+ padding: 14px 16px;
+ box-shadow: inset 0 1px 0 hsl(var(--text-primary) / 0.025);
+ }
+
+ .properties-section-eyebrow,
+ .properties-option-group-heading h2 {
+ color: hsl(var(--text-primary));
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ }
+
+ .properties-default-legend {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: 6px;
+ color: hsl(var(--text-muted));
+ font-size: 11px;
+ white-space: nowrap;
+ }
+
+ .properties-default-legend-dot {
+ width: 7px;
+ height: 7px;
+ border: 1px solid hsl(var(--accent-color) / 0.72);
+ border-radius: 50%;
+ background: hsl(var(--accent-color) / 0.18);
+ }
+
+ .properties-option-group {
+ padding: 16px;
+ }
+
+ .properties-option-group-heading {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+ padding-bottom: 11px;
+ border-bottom: 1px solid hsl(var(--border-modal) / 0.64);
+ }
+
+ .properties-option-group-heading p {
+ max-width: 680px;
+ margin-top: 4px;
+ color: hsl(var(--text-muted));
+ font-size: 11px;
+ line-height: 1.45;
+ }
+
+ .properties-field {
+ min-width: 0;
+ }
+
+ .properties-field-label {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 6px;
+ color: hsl(var(--text-muted));
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.35;
+ }
+
+ .properties-field-meta {
+ overflow: hidden;
+ margin-inline-start: auto;
+ color: hsl(var(--accent-color) / 0.88);
+ font-size: 10px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .properties-help {
+ position: relative;
+ display: inline-flex;
+ flex: 0 0 auto;
+ width: 18px;
+ height: 18px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ border: 0;
+ padding: 0;
+ background: transparent;
+ color: hsl(var(--text-muted));
+ cursor: help;
+ font: inherit;
+ outline: none;
+ }
+
+ .properties-help:hover,
+ .properties-help:focus-visible {
+ color: hsl(var(--accent-color));
+ background: hsl(var(--accent-color) / 0.1);
+ }
+
+ .properties-help:focus-visible {
+ outline: 2px solid hsl(var(--accent-color) / 0.72);
+ outline-offset: 2px;
+ }
+
+ .properties-help-tooltip {
+ position: absolute;
+ z-index: 30;
+ inset-inline-start: 0;
+ top: calc(100% + 7px);
+ width: min(260px, 56vw);
+ padding: 8px 10px;
+ border: 1px solid hsl(var(--border-modal));
+ border-radius: 8px;
+ background: hsl(var(--surface-overlay) / 0.98);
+ box-shadow: 0 10px 26px hsl(var(--shadow-color));
+ color: hsl(var(--text-primary));
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 1.45;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-2px);
+ transition: opacity 120ms ease, transform 120ms ease, visibility 120ms ease;
+ visibility: hidden;
+ }
+
+ .properties-help:hover .properties-help-tooltip,
+ .properties-help:focus-visible .properties-help-tooltip {
+ opacity: 1;
+ transform: translateY(0);
+ visibility: visible;
+ }
+
+ .properties-options .properties-option-group:last-child .properties-help-tooltip {
+ top: auto;
+ bottom: calc(100% + 7px);
+ transform: translateY(2px);
+ }
+
+ .properties-options .properties-option-group:last-child .properties-help:hover .properties-help-tooltip,
+ .properties-options .properties-option-group:last-child .properties-help:focus-visible .properties-help-tooltip {
+ transform: translateY(0);
+ }
+
+ .properties-option-toggle {
+ display: flex !important;
+ min-width: 0;
+ min-height: 56px;
+ align-items: flex-start !important;
+ flex-direction: row !important;
+ gap: 10px !important;
+ padding: 10px 11px;
+ border: 1px solid hsl(var(--border-modal) / 0.82);
+ border-radius: 9px;
+ background: hsl(var(--bg-input) / 0.2);
+ }
+
+ .properties-option-toggle:has(input:not(:disabled)):hover {
+ border-color: hsl(var(--accent-color) / 0.42);
+ background: hsl(var(--item-hover));
+ }
+
+ .properties-option-toggle input {
+ margin-top: 2px !important;
+ }
+
+ .properties-option-toggle-copy {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+ flex: 1 1 auto;
+ }
+
+ .properties-option-toggle-label {
+ color: hsl(var(--text-primary));
+ font-size: 12px;
+ line-height: 1.35;
+ }
+
+ .properties-option-toggle-hint {
+ color: hsl(var(--text-muted));
+ font-size: 10px;
+ line-height: 1.4;
+ }
+
.properties-window-panel textarea.app-control {
min-height: 104px;
resize: vertical;
@@ -997,6 +1203,41 @@ html[data-list-density="relaxed"] {
margin-top: 2px;
}
+ .properties-window-panel .properties-field-label {
+ display: flex;
+ min-height: 20px;
+ align-items: center;
+ flex-direction: row;
+ gap: 6px;
+ line-height: 1.35;
+ }
+
+ .properties-field-label-text {
+ display: inline-flex !important;
+ min-width: 0;
+ align-items: center;
+ color: inherit;
+ font: inherit;
+ line-height: inherit;
+ }
+
+ .properties-field-format {
+ display: block;
+ margin-top: 5px;
+ color: hsl(var(--text-muted) / 0.88);
+ font-size: 10px;
+ line-height: 1.35;
+ }
+
+ .properties-option-toggle-label {
+ display: block !important;
+ margin: 0;
+ color: hsl(var(--text-primary));
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 1.35;
+ }
+
.properties-window-panel > div > p,
.properties-window-panel .text-text-muted {
line-height: 1.45;
diff --git a/src/propertiesBridge.test.ts b/src/propertiesBridge.test.ts
index e09d446..b25aeb2 100644
--- a/src/propertiesBridge.test.ts
+++ b/src/propertiesBridge.test.ts
@@ -17,6 +17,8 @@ import {
beginExclusivePropertiesAction,
classifyPropertiesActionRequest,
createFrameCoalescer,
+ decodePropertiesPatchValue,
+ encodePropertiesPatchValue,
enqueuePropertiesAction,
formatPropertiesQueuePlacement,
getPropertiesLifecycleAction,
@@ -33,6 +35,17 @@ import {
} from './propertiesBridge';
describe('Properties window bridge', () => {
+ it('keeps optional override resets explicit across the JSON IPC boundary', () => {
+ const encoded = encodePropertiesPatchValue(undefined);
+
+ expect(encoded).toBeNull();
+ expect(JSON.parse(JSON.stringify({ torrentEncryptionPolicy: encoded }))).toEqual({
+ torrentEncryptionPolicy: null,
+ });
+ expect(decodePropertiesPatchValue(encoded)).toBeUndefined();
+ expect(decodePropertiesPatchValue('prealloc')).toBe('prealloc');
+ });
+
it('uses a WebviewWindow target for directed child events', () => {
expect(propertiesWindowEventTarget('properties-1')).toEqual({
kind: 'WebviewWindow',
diff --git a/src/propertiesBridge.ts b/src/propertiesBridge.ts
index ad8610b..8652a11 100644
--- a/src/propertiesBridge.ts
+++ b/src/propertiesBridge.ts
@@ -193,13 +193,48 @@ export type SecretPatch =
| { kind: 'replace'; value: string }
| { kind: 'clear' };
-export type PropertiesPatch = Partial> & {
+export const PROPERTIES_PATCH_CLEARABLE_KEYS = [
+ 'destination',
+ 'speedLimit',
+ 'torrentTrackers',
+ 'torrentExcludeTrackers',
+ 'torrentSeedTime',
+ 'torrentSeedRatio',
+ 'torrentUploadLimit',
+ 'torrentMaxPeers',
+ 'torrentPeerSpeedLimit',
+ 'torrentTrackerConnectTimeout',
+ 'torrentTrackerTimeout',
+ 'torrentTrackerInterval',
+ 'torrentStopTimeout',
+ 'torrentPrioritizePiece',
+ 'torrentEncryptionPolicy',
+ 'torrentFileAllocation',
+] as const;
+
+type PropertiesPatchClearableKey = typeof PROPERTIES_PATCH_CLEARABLE_KEYS[number];
+
+type PropertiesPatchClearableValues = {
+ [Key in PropertiesPatchClearableKey]?: DownloadItem[Key] | null;
+};
+
+export type PropertiesPatch = Partial> & PropertiesPatchClearableValues & {
username?: SecretPatch;
password?: SecretPatch;
cookies?: SecretPatch;
headers?: SecretPatch;
};
+// Tauri command arguments cross a JSON boundary. `undefined` object members
+// may be omitted before they reach Rust, so nullable fields are the explicit
+// wire-level sentinel for clearing an optional per-download override.
+export const encodePropertiesPatchValue = (value: T | undefined): T | null => value ?? null;
+
+export const decodePropertiesPatchValue = (value: T | null | undefined): T | undefined =>
+ value === null ? undefined : value;
+
export type PropertiesAction =
| 'apply-properties'
| 'set-torrent-file-selection'