mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-25 01:57:06 +00:00
refactor(frontend): replace manual react queue state throttling with native tokio concurrency
This commit is contained in:
@@ -984,6 +984,18 @@ async fn set_concurrent_limit(state: tauri::State<'_, AppState>, limit: usize) -
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn set_global_speed_limit(state: tauri::State<'_, AppState>, limit: Option<String>) -> Result<(), String> {
|
||||||
|
let limit_str = limit.unwrap_or_else(|| "0".to_string());
|
||||||
|
let _ = rpc_call(
|
||||||
|
state.aria2_port,
|
||||||
|
&state.aria2_secret,
|
||||||
|
"aria2.changeGlobalOption",
|
||||||
|
serde_json::json!([{"max-overall-download-limit": limit_str}])
|
||||||
|
).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn request_automation_permission() -> Result<(), String> {
|
fn request_automation_permission() -> Result<(), String> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -1383,7 +1395,7 @@ pub fn run() {
|
|||||||
request_automation_permission, open_automation_settings,
|
request_automation_permission, open_automation_settings,
|
||||||
set_keychain_password, get_keychain_password, delete_keychain_password,
|
set_keychain_password, get_keychain_password, delete_keychain_password,
|
||||||
check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
|
check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
|
||||||
set_extension_frontend_ready, set_concurrent_limit, remove_download,
|
set_extension_frontend_ready, set_concurrent_limit, set_global_speed_limit, remove_download,
|
||||||
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media
|
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -93,12 +93,24 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousSpeedLimit.current === globalSpeedLimit) return;
|
if (previousSpeedLimit.current === globalSpeedLimit) return;
|
||||||
previousSpeedLimit.current = globalSpeedLimit;
|
previousSpeedLimit.current = globalSpeedLimit;
|
||||||
const timeout = window.setTimeout(() => {
|
|
||||||
useDownloadStore.getState().restartActiveDownloads().catch(error => {
|
// Convert to aria2 format (e.g. "1M", "500K")
|
||||||
console.error('Failed to apply global speed limit:', error);
|
let formattedLimit = null;
|
||||||
});
|
if (globalSpeedLimit) {
|
||||||
}, 500);
|
const match = globalSpeedLimit.trim().match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?(?:\/s)?$/i);
|
||||||
return () => window.clearTimeout(timeout);
|
if (match) {
|
||||||
|
const amount = Number(match[1]);
|
||||||
|
if (Number.isFinite(amount) && amount > 0) {
|
||||||
|
const multipliers: Record<string, number> = { '': 1, k: 1024, m: 1048576, g: 1073741824 };
|
||||||
|
const bytes = Math.round(amount * multipliers[match[2].toLowerCase()]);
|
||||||
|
formattedLimit = `${bytes}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke('set_global_speed_limit', { limit: formattedLimit }).catch(error => {
|
||||||
|
console.error('Failed to apply global speed limit:', error);
|
||||||
|
});
|
||||||
}, [globalSpeedLimit]);
|
}, [globalSpeedLimit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -192,7 +204,13 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlistenProgress = listen('download-progress', (event: any) => {
|
const unlistenProgress = listen('download-progress', (event: any) => {
|
||||||
const { id, fraction, speed, eta } = event.payload;
|
const { id, fraction, speed, eta } = event.payload;
|
||||||
updateDownload(id, { fraction, speed, eta });
|
const state = useDownloadStore.getState();
|
||||||
|
const current = state.downloads.find(d => d.id === id);
|
||||||
|
if (current && current.status === 'queued') {
|
||||||
|
updateDownload(id, { status: 'downloading', fraction, speed, eta });
|
||||||
|
} else {
|
||||||
|
updateDownload(id, { fraction, speed, eta });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlistenComplete = listen('download-complete', (event: any) => {
|
const unlistenComplete = listen('download-complete', (event: any) => {
|
||||||
|
|||||||
@@ -48,41 +48,7 @@ const syncSystemIntegrations = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const speedLimitToKiB = (value?: string | null): number | null => {
|
// Legacy manual speed limit math removed
|
||||||
if (!value) return null;
|
|
||||||
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?(?:\/s)?$/i);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
const amount = Number(match[1]);
|
|
||||||
if (!Number.isFinite(amount) || amount <= 0) return null;
|
|
||||||
|
|
||||||
const multipliers: Record<string, number> = {
|
|
||||||
'': 1,
|
|
||||||
k: 1,
|
|
||||||
m: 1024,
|
|
||||||
g: 1024 * 1024,
|
|
||||||
t: 1024 * 1024 * 1024
|
|
||||||
};
|
|
||||||
return Math.max(1, Math.round(amount * multipliers[match[2].toLowerCase()]));
|
|
||||||
};
|
|
||||||
|
|
||||||
const effectiveSpeedLimit = (
|
|
||||||
itemLimit: string | null | undefined,
|
|
||||||
globalLimit: string,
|
|
||||||
maxConcurrentDownloads: number
|
|
||||||
): string | null => {
|
|
||||||
const itemKiB = speedLimitToKiB(itemLimit);
|
|
||||||
const globalKiB = speedLimitToKiB(globalLimit);
|
|
||||||
const perSlotGlobalKiB = globalKiB
|
|
||||||
? Math.max(1, Math.floor(globalKiB / Math.max(maxConcurrentDownloads, 1)))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const effectiveKiB = itemKiB && perSlotGlobalKiB
|
|
||||||
? Math.min(itemKiB, perSlotGlobalKiB)
|
|
||||||
: itemKiB ?? perSlotGlobalKiB;
|
|
||||||
|
|
||||||
return effectiveKiB ? `${effectiveKiB}K` : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DownloadStatus = 'downloading' | 'paused' | 'completed' | 'failed' | 'queued';
|
export type DownloadStatus = 'downloading' | 'paused' | 'completed' | 'failed' | 'queued';
|
||||||
export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
|
export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
|
||||||
@@ -117,6 +83,7 @@ export interface DownloadItem {
|
|||||||
isMedia?: boolean;
|
isMedia?: boolean;
|
||||||
mediaFormatSelector?: string;
|
mediaFormatSelector?: string;
|
||||||
queueId: string;
|
queueId: string;
|
||||||
|
_dispatched?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionDownloadRequest {
|
export interface ExtensionDownloadRequest {
|
||||||
@@ -149,7 +116,6 @@ interface DownloadState {
|
|||||||
addQueue: (name: string) => void;
|
addQueue: (name: string) => void;
|
||||||
renameQueue: (id: string, name: string) => void;
|
renameQueue: (id: string, name: string) => void;
|
||||||
removeQueue: (id: string) => void;
|
removeQueue: (id: string) => void;
|
||||||
restartActiveDownloads: () => Promise<number>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDownloadStore = create<DownloadState>((set, get) => ({
|
export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||||
@@ -265,7 +231,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.map(d =>
|
downloads: state.downloads.map(d =>
|
||||||
d.id === id
|
d.id === id
|
||||||
? { ...d, status: 'queued', fraction: 0, speed: '-', eta: '-' }
|
? { ...d, status: 'queued', _dispatched: false, fraction: 0, speed: '-', eta: '-' }
|
||||||
: d
|
: d
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -281,7 +247,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.map(item =>
|
downloads: state.downloads.map(item =>
|
||||||
runnableIds.includes(item.id)
|
runnableIds.includes(item.id)
|
||||||
? { ...item, status: 'queued', speed: '-', eta: '-' }
|
? { ...item, status: 'queued', _dispatched: false, speed: '-', eta: '-' }
|
||||||
: item
|
: item
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -320,47 +286,15 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
downloads: state.downloads.map(d => d.queueId === id ? { ...d, queueId: MAIN_QUEUE_ID } : d)
|
downloads: state.downloads.map(d => d.queueId === id ? { ...d, queueId: MAIN_QUEUE_ID } : d)
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
restartActiveDownloads: async () => {
|
|
||||||
const activeIds = get().downloads
|
|
||||||
.filter(item => item.status === 'downloading')
|
|
||||||
.map(item => item.id);
|
|
||||||
|
|
||||||
if (activeIds.length === 0) return 0;
|
|
||||||
|
|
||||||
set((state) => ({
|
|
||||||
downloads: state.downloads.map(item =>
|
|
||||||
activeIds.includes(item.id)
|
|
||||||
? { ...item, status: 'paused', speed: '-', eta: '-' }
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
await Promise.all(activeIds.map(id => invoke('pause_download', { id }).catch(() => {})));
|
|
||||||
await new Promise(resolve => window.setTimeout(resolve, 350));
|
|
||||||
set((state) => ({
|
|
||||||
downloads: state.downloads.map(item =>
|
|
||||||
activeIds.includes(item.id)
|
|
||||||
? { ...item, status: 'queued' }
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
await get().processQueue();
|
|
||||||
return activeIds.length;
|
|
||||||
},
|
|
||||||
processQueue: async () => {
|
processQueue: async () => {
|
||||||
const { downloads, updateDownload } = get();
|
const { downloads, updateDownload } = get();
|
||||||
const settingsSnapshot = useSettingsStore.getState();
|
|
||||||
const { maxConcurrentDownloads } = settingsSnapshot;
|
|
||||||
|
|
||||||
const activeCount = downloads.filter(d => d.status === 'downloading').length;
|
// Find all queued items that haven't been dispatched to the backend yet
|
||||||
if (activeCount >= maxConcurrentDownloads) return;
|
const itemsToStart = downloads.filter(d => d.status === 'queued' && !d._dispatched);
|
||||||
|
|
||||||
const queuedItems = downloads.filter(d => d.status === 'queued');
|
|
||||||
const slotsAvailable = maxConcurrentDownloads - activeCount;
|
|
||||||
|
|
||||||
const itemsToStart = queuedItems.slice(0, slotsAvailable);
|
|
||||||
|
|
||||||
for (const item of itemsToStart) {
|
for (const item of itemsToStart) {
|
||||||
updateDownload(item.id, { status: 'downloading' });
|
// Mark as dispatched so we don't send it again on the next pass
|
||||||
|
updateDownload(item.id, { _dispatched: true });
|
||||||
try {
|
try {
|
||||||
const settings = useSettingsStore.getState();
|
const settings = useSettingsStore.getState();
|
||||||
const login = getSiteLogin(item.url, settings);
|
const login = getSiteLogin(item.url, settings);
|
||||||
@@ -379,11 +313,6 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
'~/Downloads';
|
'~/Downloads';
|
||||||
|
|
||||||
if (item.isMedia) {
|
if (item.isMedia) {
|
||||||
const speedLimit = effectiveSpeedLimit(
|
|
||||||
item.speedLimit,
|
|
||||||
settings.globalSpeedLimit,
|
|
||||||
settings.maxConcurrentDownloads
|
|
||||||
);
|
|
||||||
await invoke('start_media_download', {
|
await invoke('start_media_download', {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
@@ -391,7 +320,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
filename: item.fileName,
|
filename: item.fileName,
|
||||||
formatSelector: item.mediaFormatSelector || null,
|
formatSelector: item.mediaFormatSelector || null,
|
||||||
cookieSource: settings.mediaCookieSource !== 'none' ? settings.mediaCookieSource : null,
|
cookieSource: settings.mediaCookieSource !== 'none' ? settings.mediaCookieSource : null,
|
||||||
speedLimit,
|
speedLimit: item.speedLimit || null,
|
||||||
username: item.username || (login ? login.username : null),
|
username: item.username || (login ? login.username : null),
|
||||||
password: item.password || keychainPassword,
|
password: item.password || keychainPassword,
|
||||||
headers: item.headers || null,
|
headers: item.headers || null,
|
||||||
@@ -400,18 +329,13 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
maxTries: settings.maxAutomaticRetries
|
maxTries: settings.maxAutomaticRetries
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const speedLimit = effectiveSpeedLimit(
|
|
||||||
item.speedLimit,
|
|
||||||
settings.globalSpeedLimit,
|
|
||||||
settings.maxConcurrentDownloads
|
|
||||||
);
|
|
||||||
await invoke('start_download', {
|
await invoke('start_download', {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
destination: destPath,
|
destination: destPath,
|
||||||
filename: item.fileName,
|
filename: item.fileName,
|
||||||
connections: item.connections || settings.perServerConnections || null,
|
connections: item.connections || settings.perServerConnections || null,
|
||||||
speedLimit,
|
speedLimit: item.speedLimit || null,
|
||||||
username: item.username || (login ? login.username : null),
|
username: item.username || (login ? login.username : null),
|
||||||
password: item.password || keychainPassword,
|
password: item.password || keychainPassword,
|
||||||
headers: item.headers || null,
|
headers: item.headers || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user