mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-06 17:38:06 +00:00
fix(downloads): harden queue admission and live speed controls
This commit is contained in:
@@ -72,6 +72,8 @@ export const PropertiesModal = () => {
|
||||
|
||||
const [speedLimitEnabled, setSpeedLimitEnabled] = useState(false);
|
||||
const [speedLimitValue, setSpeedLimitValue] = useState('1024'); // KiB/s
|
||||
const [liveSpeedLimitValue, setLiveSpeedLimitValue] = useState('');
|
||||
const [isLiveSpeedLimitPending, setIsLiveSpeedLimitPending] = useState(false);
|
||||
|
||||
const [loginMode, setLoginMode] = useState<LoginMode>('matching');
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -143,6 +145,11 @@ export const PropertiesModal = () => {
|
||||
}
|
||||
}, [selectedPropertiesDownloadId, setSelectedPropertiesDownloadId]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeLimit = item?.speedLimit?.trim();
|
||||
setLiveSpeedLimitValue(activeLimit && activeLimit !== '0' ? activeLimit : '');
|
||||
}, [item?.speedLimit, selectedPropertiesDownloadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPropertiesDownloadId || connectionsDirty) return;
|
||||
const activeItem = useDownloadStore.getState().downloads.find(d => d.id === selectedPropertiesDownloadId);
|
||||
@@ -245,8 +252,27 @@ export const PropertiesModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLiveSpeedLimit = async (limit: string | null) => {
|
||||
if (isLiveSpeedLimitPending || item.isMedia || !['downloading', 'retrying'].includes(item.status)) return;
|
||||
|
||||
setErrorMessage('');
|
||||
setIsLiveSpeedLimitPending(true);
|
||||
try {
|
||||
await useDownloadStore.getState().setDownloadSpeedLimit(item.id, limit);
|
||||
if (limit === null) setLiveSpeedLimitValue('');
|
||||
} catch (error) {
|
||||
setErrorMessage(t($ => $.properties.liveSpeedLimitFailed, {
|
||||
detail: error instanceof Error ? error.message : String(error)
|
||||
}));
|
||||
} finally {
|
||||
setIsLiveSpeedLimitPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const identityLocked = getIdentityLocked(item.status);
|
||||
const transferLocked = getTransferLocked(item.status);
|
||||
const liveSpeedLimitAvailable = !item.isMedia && ['downloading', 'retrying'].includes(item.status);
|
||||
const liveSpeedLimitUnavailable = item.isMedia && ['downloading', 'processing', 'retrying'].includes(item.status);
|
||||
const configuredConnections = resolveDownloadConnections(item.connections, perServerConnections);
|
||||
const observedConnectionTotal = Math.max(
|
||||
1,
|
||||
@@ -458,6 +484,53 @@ export const PropertiesModal = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(liveSpeedLimitAvailable || liveSpeedLimitUnavailable) && (
|
||||
<div className="col-start-2 rounded-md border border-border-modal/60 bg-border-color/20 p-3 space-y-2">
|
||||
{liveSpeedLimitAvailable ? (
|
||||
<>
|
||||
<label htmlFor="live-speed-limit" className="block text-xs font-medium text-text-primary">
|
||||
{t($ => $.properties.liveSpeedLimit)}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="live-speed-limit"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={liveSpeedLimitValue}
|
||||
onChange={event => setLiveSpeedLimitValue(event.currentTarget.value)}
|
||||
placeholder={t($ => $.properties.liveSpeedLimitPlaceholder)}
|
||||
disabled={isLiveSpeedLimitPending}
|
||||
aria-describedby="live-speed-limit-hint"
|
||||
className="w-28 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleLiveSpeedLimit(liveSpeedLimitValue)}
|
||||
disabled={isLiveSpeedLimitPending}
|
||||
className="app-button app-button-primary px-3 text-xs disabled:opacity-50"
|
||||
>
|
||||
{t($ => $.properties.liveSpeedLimitApply)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleLiveSpeedLimit(null)}
|
||||
disabled={isLiveSpeedLimitPending || !liveSpeedLimitValue}
|
||||
className="app-button px-3 text-xs disabled:opacity-50"
|
||||
>
|
||||
{t($ => $.properties.liveSpeedLimitClear)}
|
||||
</button>
|
||||
</div>
|
||||
<p id="live-speed-limit-hint" className="text-[11px] text-text-muted">
|
||||
{t($ => $.properties.liveSpeedLimitHint)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-[11px] text-text-muted">
|
||||
{t($ => $.properties.liveSpeedLimitUnavailable)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ interface SidebarProps {
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
const { selectedFilter, onSelectFilter } = props;
|
||||
const { downloads, queues, addQueue, renameQueue, removeQueue, startQueue, pauseQueue } = useDownloadStore();
|
||||
const { downloads, queues, addQueue, renameQueue, removeQueue, startQueue, pauseQueue, setQueueConcurrency } = useDownloadStore();
|
||||
const { activeView, setActiveView, toggleSidebar } = useSettingsStore();
|
||||
const { addToast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
@@ -475,6 +475,45 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||
<Pause size={14} className="me-2 text-text-secondary" />
|
||||
{t($ => $.actions.pauseQueue)}
|
||||
</button>
|
||||
{(() => {
|
||||
const queue = queues.find(candidate => candidate.id === contextMenu.id);
|
||||
if (!queue) return null;
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="queue-concurrency-label"
|
||||
className="px-3 py-2 text-[12px] text-text-secondary"
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<label id="queue-concurrency-label" htmlFor="queue-concurrency-select" className="block mb-1">
|
||||
{t($ => $.sidebar.queueConcurrency)}
|
||||
</label>
|
||||
<select
|
||||
id="queue-concurrency-select"
|
||||
aria-label={t($ => $.sidebar.queueConcurrency)}
|
||||
value={queue.maxConcurrent?.toString() ?? ''}
|
||||
className="w-full rounded border border-border-color bg-bg-context-menu px-1.5 py-1 text-[12px] text-text-primary outline-none focus:border-accent"
|
||||
onChange={event => {
|
||||
const value = event.currentTarget.value === ''
|
||||
? null
|
||||
: Number(event.currentTarget.value);
|
||||
void setQueueConcurrency(queue.id, value).catch(error => {
|
||||
addToast({
|
||||
message: t($ => $.sidebar.queueConcurrencyFailed, { detail: String(error) }),
|
||||
variant: 'error',
|
||||
isActionable: true
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">{t($ => $.sidebar.queueConcurrencyGlobal)}</option>
|
||||
{Array.from({ length: 12 }, (_, index) => index + 1).map(value => (
|
||||
<option key={value} value={value}>{value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div className="h-px bg-border-color my-1 mx-2" />
|
||||
<button
|
||||
className="w-full text-start px-3 py-1.5 flex items-center hover:bg-item-hover"
|
||||
|
||||
Reference in New Issue
Block a user