mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-28 04:49:39 +00:00
d720308f30
- Prevent UI hang by processing aria2 unpause actions asynchronously - Bypass internal aria2 limits to honor Firelink's max concurrent setting - Improve visibility of the 'Recheck engines' button in Settings - Refactor Speed Limiter checkbox into a toggle and fix text selection - Update Add window font and placeholder to match modern design standards
143 lines
5.9 KiB
TypeScript
143 lines
5.9 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Gauge, Save, Zap } from 'lucide-react';
|
|
import { useSettingsStore } from '../store/useSettingsStore';
|
|
import { WindowDragRegion } from './WindowDragRegion';
|
|
import { useToast } from '../contexts/ToastContext';
|
|
|
|
type SpeedUnit = 'KB/s' | 'MB/s';
|
|
|
|
function parseLimit(limit: string, fallback: number): { value: number; unit: SpeedUnit } {
|
|
const match = limit.trim().match(/^(\d+(?:\.\d+)?)\s*([km]?)b?(?:\/s)?$/i);
|
|
const valueKiB = match
|
|
? Math.max(1, Math.round(Number(match[1]) * (match[2].toLowerCase() === 'm' ? 1024 : 1)))
|
|
: fallback;
|
|
|
|
return valueKiB >= 1024 && valueKiB % 1024 === 0
|
|
? { value: valueKiB / 1024, unit: 'MB/s' }
|
|
: { value: valueKiB, unit: 'KB/s' };
|
|
}
|
|
|
|
export default function SpeedLimiterView() {
|
|
const globalSpeedLimit = useSettingsStore(state => state.globalSpeedLimit);
|
|
const lastCustomSpeedLimitKiB = useSettingsStore(state => state.lastCustomSpeedLimitKiB);
|
|
const setGlobalSpeedLimit = useSettingsStore(state => state.setGlobalSpeedLimit);
|
|
const setLastCustomSpeedLimitKiB = useSettingsStore(state => state.setLastCustomSpeedLimitKiB);
|
|
const initial = parseLimit(globalSpeedLimit, lastCustomSpeedLimitKiB);
|
|
const [enabled, setEnabled] = useState(Boolean(globalSpeedLimit));
|
|
const [value, setValue] = useState(initial.value);
|
|
const [unit, setUnit] = useState<SpeedUnit>(initial.unit);
|
|
const { addToast } = useToast();
|
|
|
|
useEffect(() => {
|
|
const parsed = parseLimit(globalSpeedLimit, lastCustomSpeedLimitKiB);
|
|
setEnabled(Boolean(globalSpeedLimit));
|
|
setValue(parsed.value);
|
|
setUnit(parsed.unit);
|
|
}, [globalSpeedLimit, lastCustomSpeedLimitKiB]);
|
|
|
|
|
|
const save = () => {
|
|
const numericValue = Math.max(1, Math.min(Number(value) || 1, unit === 'MB/s' ? 10240 : 10_485_760));
|
|
const valueKiB = Math.min(10_485_760, Math.round(unit === 'MB/s' ? numericValue * 1024 : numericValue));
|
|
setLastCustomSpeedLimitKiB(valueKiB);
|
|
setGlobalSpeedLimit(enabled ? `${valueKiB}K` : '');
|
|
addToast({
|
|
message: enabled ? `Global limit saved at ${numericValue} ${unit}` : 'Global speed limit disabled',
|
|
variant: 'success'
|
|
});
|
|
};
|
|
|
|
const preset = (presetValue: number) => {
|
|
setEnabled(true);
|
|
setValue(presetValue);
|
|
setUnit('MB/s');
|
|
};
|
|
|
|
return (
|
|
<div className="flex-1 flex h-full flex-col overflow-hidden bg-main-bg">
|
|
<WindowDragRegion />
|
|
|
|
<div className="flex items-center gap-3 border-b border-border-color px-6 pb-4">
|
|
<div className="flex items-center gap-3 text-[17px] font-semibold tracking-tight text-text-primary select-none">
|
|
<button
|
|
onClick={() => setEnabled(!enabled)}
|
|
className={`relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition-colors duration-200 ease-in-out focus:outline-none ${enabled ? 'bg-accent' : 'bg-item-hover'}`}
|
|
aria-checked={enabled}
|
|
role="switch"
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition duration-200 ease-in-out ${enabled ? 'translate-x-4' : 'translate-x-1'}`}
|
|
/>
|
|
</button>
|
|
Speed Limiter
|
|
</div>
|
|
<span className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${
|
|
enabled ? 'bg-accent/15 text-accent' : 'bg-item-hover text-text-muted'
|
|
}`}>
|
|
{enabled ? `${value} ${unit}` : 'Unlimited'}
|
|
</span>
|
|
<button onClick={save} className="app-button app-button-primary ml-auto px-3 text-[11px]">
|
|
<Save size={14} /> Save Limit
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<section className={`app-card max-w-[720px] p-5 ${enabled ? '' : 'opacity-50'}`}>
|
|
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
|
|
<Gauge size={18} className="text-accent" /> Global Speed Limit
|
|
</div>
|
|
<p className="max-w-xl text-[12px] leading-relaxed text-text-muted">
|
|
This cap is shared by transfers running through the core downloader. A lower per-download limit still takes precedence.
|
|
Saving updates the core downloader immediately; media extraction keeps its existing per-download options.
|
|
</p>
|
|
|
|
<div className="mt-6 flex items-center gap-3">
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={value}
|
|
disabled={!enabled}
|
|
onChange={event => setValue(Math.max(1, Number(event.target.value) || 1))}
|
|
className="app-control w-28 px-3 py-2 text-right font-mono"
|
|
/>
|
|
<div className="flex rounded-md border border-border-modal bg-bg-input p-1">
|
|
{(['KB/s', 'MB/s'] as SpeedUnit[]).map(option => (
|
|
<button
|
|
key={option}
|
|
type="button"
|
|
disabled={!enabled}
|
|
onClick={() => setUnit(option)}
|
|
className={`rounded px-3 py-1.5 text-[12px] font-medium ${
|
|
unit === option ? 'bg-accent text-white' : 'text-text-secondary hover:bg-item-hover'
|
|
}`}
|
|
>
|
|
{option}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="my-6 border-t border-border-color" />
|
|
<div className="mb-3 flex items-center gap-2 text-[12px] font-medium text-text-secondary">
|
|
<Zap size={14} /> Quick Presets
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[1, 5, 10].map(presetValue => (
|
|
<button
|
|
key={presetValue}
|
|
type="button"
|
|
disabled={!enabled}
|
|
onClick={() => preset(presetValue)}
|
|
className="app-button px-4 text-[12px] disabled:opacity-50"
|
|
>
|
|
{presetValue} MB/s
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|