feat(audit): L51 + L64 — daily-sync time picker + aria2c connection tuning
Two small self-contained features from the audit's deferred list: - L51: the Task Scheduler daily sync is no longer pinned to 09:00. New Settings.syncTime (24h HH:MM) with a native time input beside the Library "Daily sync" switch — persists on change, re-registers the task on blur (schtasks /Create /F overwrite is the time-change mechanism). The value reaches the schtasks /ST argv, so a shared isValidSyncTime guards it at both the settings write and in schedule.ts (unit-tested incl. injection-shaped input). Live-verified against the machine's real task. - L64: aria2c connection count (-x/-s) is user-tunable. New Settings.aria2cConnections (1-16, aria2c's own ceiling) exposed as a SpinButton in Settings -> Network while the aria2c toggle is on. ARIA2C_ARGS became the pure aria2cArgs(n?) with a defensive clamp; threaded through AccessOptions. The -k 1M split floor stays fixed — a free-text field would bypass the custom-command consent gate. typecheck + 309 tests + eslint + production build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -237,6 +237,7 @@ export function LibraryView(): React.JSX.Element {
|
||||
const syncing = useSources((s) => s.syncing)
|
||||
const downloadItems = useDownloads((s) => s.items)
|
||||
const autoDownloadNew = useSettings((s) => s.autoDownloadNew)
|
||||
const syncTime = useSettings((s) => s.syncTime)
|
||||
const updateSettings = useSettings((s) => s.update)
|
||||
|
||||
const [url, setUrl] = useState('')
|
||||
@@ -294,11 +295,26 @@ export function LibraryView(): React.JSX.Element {
|
||||
async function toggleScheduled(next: boolean): Promise<void> {
|
||||
setScheduled(next) // optimistic
|
||||
if (PREVIEW) return
|
||||
const res = await window.api.setScheduledSync(next)
|
||||
const res = await window.api.setScheduledSync(next, syncTime)
|
||||
setScheduled(res.enabled)
|
||||
if (res.error) setError(res.error)
|
||||
}
|
||||
|
||||
// The native time input emits a complete 'HH:MM' or '' (cleared/partial) — only
|
||||
// persist real times, so the picker can never store a malformed value (L51).
|
||||
function onSyncTimeChange(value: string): void {
|
||||
if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(value)) return
|
||||
updateSettings({ syncTime: value })
|
||||
}
|
||||
|
||||
// Re-register the task with the new time once editing settles (blur), so typing
|
||||
// through the segments doesn't spawn schtasks per keystroke (L51).
|
||||
async function onSyncTimeCommit(): Promise<void> {
|
||||
if (!scheduled || PREVIEW) return
|
||||
const res = await window.api.setScheduledSync(true, useSettings.getState().syncTime)
|
||||
if (res.error) setError(res.error)
|
||||
}
|
||||
|
||||
// Reset the selection (and any batch note) whenever the expanded source changes.
|
||||
useEffect(() => {
|
||||
setSelected(new Set())
|
||||
@@ -578,6 +594,16 @@ export function LibraryView(): React.JSX.Element {
|
||||
onChange={(_, d) => toggleScheduled(d.checked)}
|
||||
aria-label="Daily background sync"
|
||||
/>
|
||||
{scheduled && (
|
||||
<Input
|
||||
type="time"
|
||||
size="small"
|
||||
value={syncTime}
|
||||
onChange={(_, d) => onSyncTimeChange(d.value)}
|
||||
onBlur={onSyncTimeCommit}
|
||||
aria-label="Daily sync time"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import { Field, Input, Switch, Button, Card, Subtitle2, Spinner } from '@fluentui/react-components'
|
||||
import {
|
||||
Field,
|
||||
Input,
|
||||
Switch,
|
||||
SpinButton,
|
||||
Button,
|
||||
Card,
|
||||
Subtitle2,
|
||||
Spinner
|
||||
} from '@fluentui/react-components'
|
||||
import { GlobeRegular } from '@fluentui/react-icons'
|
||||
import { useSettings } from '../../store/settings'
|
||||
import { useSettingsStyles } from './settingsStyles'
|
||||
@@ -9,6 +18,7 @@ export function NetworkCard(): React.JSX.Element {
|
||||
const proxy = useSettings((s) => s.proxy)
|
||||
const rateLimit = useSettings((s) => s.rateLimit)
|
||||
const useAria2c = useSettings((s) => s.useAria2c)
|
||||
const aria2cConnections = useSettings((s) => s.aria2cConnections)
|
||||
const youtubePlayerClient = useSettings((s) => s.youtubePlayerClient)
|
||||
const youtubePoToken = useSettings((s) => s.youtubePoToken)
|
||||
const update = useSettings((s) => s.update)
|
||||
@@ -53,6 +63,25 @@ export function NetworkCard(): React.JSX.Element {
|
||||
<Switch checked={useAria2c} onChange={(_, d) => update({ useAria2c: d.checked })} />
|
||||
</Field>
|
||||
|
||||
{useAria2c && (
|
||||
<Field
|
||||
label="aria2c connections"
|
||||
hint="Parallel connections per download (1–16). More can be faster on throttled connections; fewer is gentler on servers."
|
||||
>
|
||||
<SpinButton
|
||||
value={aria2cConnections}
|
||||
min={1}
|
||||
max={16}
|
||||
onChange={(_, d) => {
|
||||
const v = d.value ?? Number(d.displayValue)
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
update({ aria2cConnections: Math.min(16, Math.max(1, Math.round(v))) })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="YouTube client (advanced)"
|
||||
hint="Override how AeroFetch identifies itself to YouTube when downloads start failing. Try web_safari, tv, or mweb. Leave blank for the automatic default."
|
||||
|
||||
Reference in New Issue
Block a user