mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 18:03:23 +00:00
feat(torrents): add aggregate upload limit control
This commit is contained in:
@@ -11,4 +11,4 @@ import type { SiteLogin } from "./SiteLogin";
|
||||
import type { Theme } from "./Theme";
|
||||
import type { WindowControlStyle } from "./WindowControlStyle";
|
||||
|
||||
export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, torrentEnableDht: boolean, torrentEnableDht6: boolean, torrentEnablePex: boolean, torrentEnableLpd: boolean, torrentMaxOpenFiles: number, torrentListenPort: string, torrentDhtListenPort: string, torrentExternalIp: string, torrentDhtEntryPoint: string, torrentDhtEntryPoint6: string, torrentDhtListenAddr6: string, torrentLpdInterface: string, torrentPeerIdPrefix: string, torrentPeerAgent: string, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
|
||||
export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, torrentOverallUploadLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, torrentEnableDht: boolean, torrentEnableDht6: boolean, torrentEnablePex: boolean, torrentEnableLpd: boolean, torrentMaxOpenFiles: number, torrentListenPort: string, torrentDhtListenPort: string, torrentExternalIp: string, torrentDhtEntryPoint: string, torrentDhtEntryPoint6: string, torrentDhtListenAddr6: string, torrentLpdInterface: string, torrentPeerIdPrefix: string, torrentPeerAgent: string, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
|
||||
|
||||
@@ -37,6 +37,7 @@ import { normalizeCustomProxy } from '../store/useDownloadStore';
|
||||
import {
|
||||
MAX_TORRENT_MAX_OPEN_FILES,
|
||||
MIN_TORRENT_MAX_OPEN_FILES,
|
||||
normalizeSpeedLimitForBackend,
|
||||
normalizeTorrentMaxOpenFiles
|
||||
} from '../utils/downloads';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -326,7 +327,11 @@ const engineRunId = useRef(0);
|
||||
const [torrentMaxOpenFilesInput, setTorrentMaxOpenFilesInput] = useState(
|
||||
() => String(settings.torrentMaxOpenFiles)
|
||||
);
|
||||
const [torrentOverallUploadLimitInput, setTorrentOverallUploadLimitInput] = useState(
|
||||
() => settings.torrentOverallUploadLimit
|
||||
);
|
||||
const torrentMaxOpenFilesCommitRef = useRef(0);
|
||||
const torrentOverallUploadLimitCommitRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
setPerServerConnectionsInput(String(settings.perServerConnections));
|
||||
@@ -344,6 +349,10 @@ const engineRunId = useRef(0);
|
||||
setTorrentMaxOpenFilesInput(String(settings.torrentMaxOpenFiles));
|
||||
}, [settings.torrentMaxOpenFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
setTorrentOverallUploadLimitInput(settings.torrentOverallUploadLimit);
|
||||
}, [settings.torrentOverallUploadLimit]);
|
||||
|
||||
// Local state for adding site login
|
||||
const [loginPattern, setLoginPattern] = useState('');
|
||||
const [loginUser, setLoginUser] = useState('');
|
||||
@@ -375,6 +384,32 @@ const engineRunId = useRef(0);
|
||||
});
|
||||
});
|
||||
};
|
||||
const commitTorrentOverallUploadLimit = (raw: string) => {
|
||||
const trimmed = raw.trim();
|
||||
const normalized = trimmed ? (normalizeSpeedLimitForBackend(trimmed) ?? '') : '';
|
||||
if (trimmed && !normalized) {
|
||||
setTorrentOverallUploadLimitInput(settings.torrentOverallUploadLimit);
|
||||
addToast({
|
||||
message: t($ => $.settings.network.torrentOverallUploadLimitInvalid),
|
||||
variant: 'error',
|
||||
isActionable: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
const requestId = ++torrentOverallUploadLimitCommitRef.current;
|
||||
setTorrentOverallUploadLimitInput(normalized);
|
||||
void settings.setTorrentOverallUploadLimit(normalized).catch(error => {
|
||||
if (requestId !== torrentOverallUploadLimitCommitRef.current) return;
|
||||
setTorrentOverallUploadLimitInput(settings.torrentOverallUploadLimit);
|
||||
addToast({
|
||||
message: t($ => $.settings.network.torrentOverallUploadLimitUpdateFailed, {
|
||||
detail: error instanceof Error ? error.message : String(error)
|
||||
}),
|
||||
variant: 'error',
|
||||
isActionable: true
|
||||
});
|
||||
});
|
||||
};
|
||||
const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false);
|
||||
const [manualUpdateStatus, setManualUpdateStatus] = useState<ManualUpdateStatus>({ type: 'idle' });
|
||||
|
||||
@@ -1395,6 +1430,21 @@ runEngineChecks(false);
|
||||
aria-label={t($ => $.settings.network.torrentMaxOpenFiles)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentOverallUploadLimit)}</span>
|
||||
<small>{t($ => $.settings.network.torrentOverallUploadLimitDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={torrentOverallUploadLimitInput}
|
||||
onChange={(event) => setTorrentOverallUploadLimitInput(event.target.value)}
|
||||
onBlur={(event) => commitTorrentOverallUploadLimit(event.target.value)}
|
||||
placeholder="1M"
|
||||
className="app-control settings-network-input text-center"
|
||||
aria-label={t($ => $.settings.network.torrentOverallUploadLimit)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="settings-section-title">{t($ => $.settings.network.identity)}</h2>
|
||||
|
||||
@@ -824,6 +824,10 @@ const common = {
|
||||
torrentMaxOpenFiles: 'Maximum open Torrent files',
|
||||
torrentMaxOpenFilesDescription: 'Global Aria2 limit for files open at once in multi-file Torrents. Lower values reduce file-descriptor use; the default is 100. Changes apply to new Torrents without restarting Aria2, and this does not raise your operating system limit.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Could not apply the Torrent open-file limit: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Overall Aria2 upload limit',
|
||||
torrentOverallUploadLimitDescription: 'Caps combined Aria2 upload traffic, primarily active Torrent seeding in Firelink. Leave blank for unlimited; the value is applied live and restored when Firelink restarts.',
|
||||
torrentOverallUploadLimitInvalid: 'Enter a valid upload limit, such as 512K or 2M.',
|
||||
torrentOverallUploadLimitUpdateFailed: 'Could not apply the overall Aria2 upload limit: {{detail}}',
|
||||
identity: 'Identity',
|
||||
customUserAgent: 'Custom User-Agent',
|
||||
userAgentDescription: 'Applied to metadata fetches and download engines.',
|
||||
|
||||
@@ -824,6 +824,10 @@ const fa = {
|
||||
torrentMaxOpenFiles: 'حداکثر فایلهای باز تورنت',
|
||||
torrentMaxOpenFilesDescription: 'حداکثر سراسری Aria2 برای تعداد فایلهای همزمان باز در تورنتهای چندفایلی. مقدار کمتر مصرف file descriptor را کم میکند؛ پیشفرض ۱۰۰ است. تغییرات برای تورنتهای جدید و بدون راهاندازی مجدد Aria2 اعمال میشوند و محدودیت سیستمعامل را افزایش نمیدهند.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'اعمال محدودیت فایلهای باز تورنت ممکن نشد: {{detail}}',
|
||||
torrentOverallUploadLimit: 'محدودیت کلی آپلود Aria2',
|
||||
torrentOverallUploadLimitDescription: 'سرعت کلی آپلود Aria2 را محدود میکند؛ در Firelink این مقدار عمدتاً برای سیدینگ تورنتهاست. برای نامحدود بودن خالی بگذارید؛ مقدار جدید زنده اعمال میشود و پس از راهاندازی مجدد Firelink برمیگردد.',
|
||||
torrentOverallUploadLimitInvalid: 'یک محدودیت معتبر مثل 512K یا 2M برای آپلود وارد کنید.',
|
||||
torrentOverallUploadLimitUpdateFailed: 'اعمال محدودیت کلی آپلود Aria2 ممکن نشد: {{detail}}',
|
||||
identity: 'هویت',
|
||||
customUserAgent: 'User-Agent سفارشی',
|
||||
userAgentDescription: 'در دریافتهای متادیتا و موتورهای دانلود اعمال میشود.',
|
||||
|
||||
@@ -824,6 +824,10 @@ const he = {
|
||||
torrentMaxOpenFiles: 'מספר קובצי Torrent פתוחים מרבי',
|
||||
torrentMaxOpenFilesDescription: 'מגבלה כללית של Aria2 על מספר הקבצים הפתוחים בו-זמנית בטורנטים מרובי קבצים. ערך נמוך יותר מפחית שימוש ב-file descriptors; ברירת המחדל היא 100. השינויים חלים על טורנטים חדשים ללא הפעלה מחדש של Aria2, ואינם מגדילים את מגבלת מערכת ההפעלה.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'לא ניתן להחיל את מגבלת הקבצים הפתוחים של Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'מגבלת העלאה כוללת של Aria2',
|
||||
torrentOverallUploadLimitDescription: 'מגבילה את מהירות ההעלאה המשולבת של Aria2, בעיקר עבור העלאת טורנטים פעילים ב-Firelink. השאר ריק ללא הגבלה; הערך מוחל מיד ומשוחזר לאחר הפעלה מחדש של Firelink.',
|
||||
torrentOverallUploadLimitInvalid: 'הזן מגבלת העלאה תקפה, למשל 512K או 2M.',
|
||||
torrentOverallUploadLimitUpdateFailed: 'לא ניתן להחיל את מגבלת ההעלאה הכוללת של Aria2: {{detail}}',
|
||||
identity: 'זהות',
|
||||
customUserAgent: 'User-Agent מותאם אישית',
|
||||
userAgentDescription: 'מוחל על משיכות מטא נתונים ומנועי הורדה.',
|
||||
|
||||
@@ -824,6 +824,10 @@ const ru = {
|
||||
torrentMaxOpenFiles: 'Максимум открытых файлов Torrent',
|
||||
torrentMaxOpenFilesDescription: 'Глобальный лимит Aria2 на одновременно открытые файлы в многофайловых торрентах. Меньшие значения снижают расход дескрипторов; по умолчанию 100. Изменения применяются к новым торрентам без перезапуска Aria2 и не повышают лимит операционной системы.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Не удалось применить лимит открытых файлов Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Общий лимит отдачи Aria2',
|
||||
torrentOverallUploadLimitDescription: 'Ограничивает суммарную скорость отдачи Aria2; в Firelink это в основном раздача активных торрентов. Оставьте поле пустым для снятия ограничения; значение применяется сразу и восстанавливается после перезапуска Firelink.',
|
||||
torrentOverallUploadLimitInvalid: 'Введите корректный лимит отдачи, например 512K или 2M.',
|
||||
torrentOverallUploadLimitUpdateFailed: 'Не удалось применить общий лимит отдачи Aria2: {{detail}}',
|
||||
identity: 'Идентификация',
|
||||
customUserAgent: 'Собственный User-Agent',
|
||||
userAgentDescription: 'Применяется при получении метаданных и работе движков загрузки.',
|
||||
|
||||
@@ -824,6 +824,10 @@ const uk = {
|
||||
torrentMaxOpenFiles: 'Максимум відкритих файлів Torrent',
|
||||
torrentMaxOpenFilesDescription: 'Глобальне обмеження Aria2 на одночасно відкриті файли в багатофайлових торрентах. Менші значення зменшують використання дескрипторів; типове значення — 100. Зміни застосовуються до нових торрентів без перезапуску Aria2 і не підвищують обмеження операційної системи.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Не вдалося застосувати обмеження відкритих файлів Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Загальне обмеження віддачі Aria2',
|
||||
torrentOverallUploadLimitDescription: 'Обмежує сумарну швидкість віддачі Aria2; у Firelink це переважно роздача активних торрентів. Залиште поле порожнім без обмеження; значення застосовується одразу й відновлюється після перезапуску Firelink.',
|
||||
torrentOverallUploadLimitInvalid: 'Введіть коректне обмеження віддачі, наприклад 512K або 2M.',
|
||||
torrentOverallUploadLimitUpdateFailed: 'Не вдалося застосувати загальне обмеження віддачі Aria2: {{detail}}',
|
||||
identity: 'Ідентифікація',
|
||||
customUserAgent: 'Власний User-Agent',
|
||||
userAgentDescription: 'Застосовується до запитів метаданих та рушіїв завантаження.',
|
||||
|
||||
@@ -824,6 +824,10 @@ const zhCN = {
|
||||
torrentMaxOpenFiles: 'Torrent 最大打开文件数',
|
||||
torrentMaxOpenFilesDescription: 'Aria2 对多文件 Torrent 同时打开文件数的全局限制。较低的值可减少文件描述符占用;默认值为 100。修改会在不重启 Aria2 的情况下应用于新 Torrent,且不会提高操作系统的限制。',
|
||||
torrentMaxOpenFilesUpdateFailed: '无法应用 Torrent 打开文件数限制:{{detail}}',
|
||||
torrentOverallUploadLimit: 'Aria2 总上传限制',
|
||||
torrentOverallUploadLimitDescription: '限制 Aria2 的总上传速度,在 Firelink 中主要用于活动 Torrent 做种。留空表示不限速;新值会立即应用,并在 Firelink 重启后恢复。',
|
||||
torrentOverallUploadLimitInvalid: '请输入有效的上传限制,例如 512K 或 2M。',
|
||||
torrentOverallUploadLimitUpdateFailed: '无法应用 Aria2 总上传限制:{{detail}}',
|
||||
identity: '身份',
|
||||
customUserAgent: '自定义 User-Agent',
|
||||
userAgentDescription: '应用于元数据获取和下载引擎。',
|
||||
|
||||
@@ -78,6 +78,7 @@ type CommandMap = {
|
||||
};
|
||||
get_torrent_peers: { args: { id: string }; result: TorrentPeerDiagnostics };
|
||||
set_torrent_max_open_files: { args: { max_open_files: number }; result: void };
|
||||
set_torrent_overall_upload_limit: { args: { limit: string | null }; result: void };
|
||||
set_global_speed_limit: { args: { limit: string | null }; result: void };
|
||||
request_automation_permission: { args: undefined; result: void };
|
||||
check_automation_permission: { args: undefined; result: void };
|
||||
|
||||
@@ -155,6 +155,53 @@ describe('useSettingsStore global speed limit persistence', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSettingsStore Torrent overall upload limit persistence', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useSettingsStore.setState({ torrentOverallUploadLimit: '2M' });
|
||||
});
|
||||
|
||||
it('applies a normalized limit before updating local state', async () => {
|
||||
await useSettingsStore.getState().setTorrentOverallUploadLimit('1.5 MB/s');
|
||||
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith('set_torrent_overall_upload_limit', {
|
||||
limit: '1.5M'
|
||||
});
|
||||
expect(useSettingsStore.getState().torrentOverallUploadLimit).toBe('1.5M');
|
||||
});
|
||||
|
||||
it('keeps the saved value when the native global option rejects an update', async () => {
|
||||
vi.mocked(ipc.invokeCommand).mockRejectedValueOnce(new Error('aria2 unavailable'));
|
||||
|
||||
await expect(
|
||||
useSettingsStore.getState().setTorrentOverallUploadLimit('3M')
|
||||
).rejects.toThrow('aria2 unavailable');
|
||||
|
||||
expect(useSettingsStore.getState().torrentOverallUploadLimit).toBe('2M');
|
||||
});
|
||||
|
||||
it('uses null to restore Aria2 unlimited upload', async () => {
|
||||
await useSettingsStore.getState().setTorrentOverallUploadLimit('');
|
||||
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith('set_torrent_overall_upload_limit', {
|
||||
limit: null
|
||||
});
|
||||
expect(useSettingsStore.getState().torrentOverallUploadLimit).toBe('');
|
||||
});
|
||||
|
||||
it('rejects malformed limits without clearing the saved value', async () => {
|
||||
await expect(
|
||||
useSettingsStore.getState().setTorrentOverallUploadLimit('not-a-rate')
|
||||
).rejects.toThrow('Torrent overall upload limit is invalid');
|
||||
|
||||
expect(ipc.invokeCommand).not.toHaveBeenCalledWith(
|
||||
'set_torrent_overall_upload_limit',
|
||||
expect.anything()
|
||||
);
|
||||
expect(useSettingsStore.getState().torrentOverallUploadLimit).toBe('2M');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSettingsStore dock badge synchronization', () => {
|
||||
it('increments the badge sync version for every toggle without issuing out-of-band clears', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
|
||||
let settingsQueue: Promise<void> = Promise.resolve();
|
||||
let torrentMaxOpenFilesQueue: Promise<void> = Promise.resolve();
|
||||
let torrentOverallUploadLimitQueue: Promise<void> = Promise.resolve();
|
||||
let pairingTokenHydrationRequest: Promise<PairingTokenHydration> | null = null;
|
||||
const settingsPersistenceErrorListeners = new Set<() => void>();
|
||||
let settingsPersistenceFailed = false;
|
||||
@@ -212,6 +213,7 @@ export interface SettingsState {
|
||||
approvedDownloadRoots: string[];
|
||||
maxConcurrentDownloads: number;
|
||||
globalSpeedLimit: string;
|
||||
torrentOverallUploadLimit: string;
|
||||
speedLimitPresetValues: number[];
|
||||
logsEnabled: boolean;
|
||||
isSidebarVisible: boolean;
|
||||
@@ -279,6 +281,7 @@ export interface SettingsState {
|
||||
approveDownloadRoot: (path: string) => Promise<string>;
|
||||
setMaxConcurrentDownloads: (count: number) => void;
|
||||
setGlobalSpeedLimit: (limit: string) => Promise<void>;
|
||||
setTorrentOverallUploadLimit: (limit: string) => Promise<void>;
|
||||
setSpeedLimitPresetValues: (values: number[]) => void;
|
||||
setLogsEnabled: (enabled: boolean) => void;
|
||||
setSidebarPosition: (position: SidebarPosition) => void;
|
||||
@@ -358,6 +361,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
approvedDownloadRoots: [],
|
||||
maxConcurrentDownloads: 3,
|
||||
globalSpeedLimit: '',
|
||||
torrentOverallUploadLimit: '',
|
||||
speedLimitPresetValues: DEFAULT_SPEED_LIMIT_PRESET_VALUES,
|
||||
logsEnabled: false,
|
||||
activeView: 'downloads',
|
||||
@@ -464,6 +468,23 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
info('Settings updated: globalSpeedLimit');
|
||||
set({ globalSpeedLimit: limit });
|
||||
},
|
||||
setTorrentOverallUploadLimit: (limit) => {
|
||||
const normalizedLimit = normalizeSpeedLimitForBackend(limit);
|
||||
if (limit.trim() && !normalizedLimit) {
|
||||
return Promise.reject(new Error('Torrent overall upload limit is invalid'));
|
||||
}
|
||||
const normalized = normalizedLimit ?? '';
|
||||
const apply = async () => {
|
||||
await invoke('set_torrent_overall_upload_limit', {
|
||||
limit: normalized || null
|
||||
});
|
||||
info('Settings updated: torrentOverallUploadLimit');
|
||||
set({ torrentOverallUploadLimit: normalized });
|
||||
};
|
||||
const result = torrentOverallUploadLimitQueue.then(apply, apply);
|
||||
torrentOverallUploadLimitQueue = result.then(() => undefined, () => undefined);
|
||||
return result;
|
||||
},
|
||||
setSpeedLimitPresetValues: (speedLimitPresetValues) => set({ speedLimitPresetValues }),
|
||||
setLogsEnabled: (logsEnabled) => set({ logsEnabled }),
|
||||
setSidebarPosition: (sidebarPosition) => set({ sidebarPosition }),
|
||||
@@ -688,6 +709,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
approvedDownloadRoots: state.approvedDownloadRoots,
|
||||
maxConcurrentDownloads: state.maxConcurrentDownloads,
|
||||
globalSpeedLimit: state.globalSpeedLimit,
|
||||
torrentOverallUploadLimit: state.torrentOverallUploadLimit,
|
||||
speedLimitPresetValues: state.speedLimitPresetValues,
|
||||
logsEnabled: state.logsEnabled,
|
||||
isSidebarVisible: state.isSidebarVisible,
|
||||
@@ -859,6 +881,9 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
12,
|
||||
currentState.maxConcurrentDownloads
|
||||
),
|
||||
torrentOverallUploadLimit: typeof persisted.torrentOverallUploadLimit === 'string'
|
||||
? normalizeSpeedLimitForBackend(persisted.torrentOverallUploadLimit) ?? ''
|
||||
: currentState.torrentOverallUploadLimit,
|
||||
perServerConnections: clampSettingInteger(
|
||||
persisted.perServerConnections,
|
||||
1,
|
||||
|
||||
Reference in New Issue
Block a user