mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2026-03-22 22:55:48 +05:30
feat: added delay configuration settings #12
This commit is contained in:
110
src/components/custom/numberInput.tsx
Normal file
110
src/components/custom/numberInput.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Minus, Plus } from "lucide-react"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface NumberInputProps
|
||||||
|
extends Omit<React.ComponentProps<"input">, "type" | "onChange" | "value"> {
|
||||||
|
value?: number
|
||||||
|
defaultValue?: number
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
step?: number
|
||||||
|
onChange?: (value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
value: controlledValue,
|
||||||
|
defaultValue = 0,
|
||||||
|
min = -Infinity,
|
||||||
|
max = Infinity,
|
||||||
|
step = 1,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
readOnly,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [internalValue, setInternalValue] = React.useState(defaultValue)
|
||||||
|
const isControlled = controlledValue !== undefined
|
||||||
|
const currentValue = isControlled ? controlledValue : internalValue
|
||||||
|
|
||||||
|
const updateValue = (newValue: number) => {
|
||||||
|
const clamped = Math.min(max, Math.max(min, newValue))
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalValue(clamped)
|
||||||
|
}
|
||||||
|
onChange?.(clamped)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIncrement = () => {
|
||||||
|
if (!disabled && !readOnly) {
|
||||||
|
updateValue(currentValue + step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDecrement = () => {
|
||||||
|
if (!disabled && !readOnly) {
|
||||||
|
updateValue(currentValue - step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const parsed = parseFloat(e.target.value)
|
||||||
|
if (!isNaN(parsed)) {
|
||||||
|
updateValue(parsed)
|
||||||
|
} else if (e.target.value === "" || e.target.value === "-") {
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalValue(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative flex items-center", className)}>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
ref={ref}
|
||||||
|
value={currentValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
disabled={disabled}
|
||||||
|
readOnly={readOnly}
|
||||||
|
className="pr-16 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield] focus-visible:ring-0"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-0 flex h-full items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDecrement}
|
||||||
|
disabled={disabled || readOnly || currentValue <= min}
|
||||||
|
className="flex h-full items-center justify-center px-2 text-muted-foreground hover:text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50 transition-colors border-x"
|
||||||
|
aria-label="Decrement"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<Minus className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleIncrement}
|
||||||
|
disabled={disabled || readOnly || currentValue >= max}
|
||||||
|
className="flex h-full items-center justify-center px-2 text-muted-foreground hover:text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50 transition-colors rounded-r-md"
|
||||||
|
aria-label="Increment"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
NumberInput.displayName = "NumberInput"
|
||||||
|
|
||||||
|
export { NumberInput }
|
||||||
@@ -352,7 +352,7 @@ export function CompletedDownloads({ downloads }: CompletedDownloadsProps) {
|
|||||||
<Empty className="mt-10">
|
<Empty className="mt-10">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<CircleArrowDown />
|
<CircleArrowDown className="stroke-primary" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No Completed Downloads</EmptyTitle>
|
<EmptyTitle>No Completed Downloads</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export function IncompleteDownloads({ downloads }: IncompleteDownloadsProps) {
|
|||||||
<Empty className="mt-10">
|
<Empty className="mt-10">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyMedia variant="icon">
|
<EmptyMedia variant="icon">
|
||||||
<CircleCheck />
|
<CircleCheck className="stroke-primary" />
|
||||||
</EmptyMedia>
|
</EmptyMedia>
|
||||||
<EmptyTitle>No Incomplete Downloads</EmptyTitle>
|
<EmptyTitle>No Incomplete Downloads</EmptyTitle>
|
||||||
<EmptyDescription>
|
<EmptyDescription>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Timer, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -34,6 +34,7 @@ import { config } from "@/config";
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg";
|
import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { NumberInput } from "@/components/custom/numberInput";
|
||||||
|
|
||||||
const proxyUrlSchema = z.object({
|
const proxyUrlSchema = z.object({
|
||||||
url: z.url({
|
url: z.url({
|
||||||
@@ -66,6 +67,46 @@ const filenameTemplateShcema = z.object({
|
|||||||
template: z.string().min(1, { message: "Filename Template is required" }),
|
template: z.string().min(1, { message: "Filename Template is required" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const minMaxSleepIntervalSchema = z.object({
|
||||||
|
min_sleep_interval: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Minimum Sleep Interval is required"
|
||||||
|
: "Minimum Sleep Interval must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Minimum Sleep Interval must be an integer"
|
||||||
|
}).min(1, {
|
||||||
|
message: "Minimum Sleep Interval must be at least 1 second"
|
||||||
|
}).max(3600, {
|
||||||
|
message: "Minimum Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||||
|
}),
|
||||||
|
max_sleep_interval: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Maximum Sleep Interval is required"
|
||||||
|
: "Maximum Sleep Interval must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Maximum Sleep Interval must be an integer"
|
||||||
|
}).min(1, {
|
||||||
|
message: "Maximum Sleep Interval must be at least 1 second"
|
||||||
|
}).max(3600, {
|
||||||
|
message: "Maximum Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestSleepIntervalSchema = z.object({
|
||||||
|
request_sleep_interval: z.coerce.number<number>({
|
||||||
|
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
|
||||||
|
? "Request Sleep Interval is required"
|
||||||
|
: "Request Sleep Interval must be a valid number"
|
||||||
|
}).int({
|
||||||
|
message: "Request Sleep Interval must be an integer"
|
||||||
|
}).min(1, {
|
||||||
|
message: "Request Sleep Interval must be at least 1 second"
|
||||||
|
}).max(3600, {
|
||||||
|
message: "Request Sleep Interval must be at most 3600 seconds (1 hour)"
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
function AppGeneralSettings() {
|
function AppGeneralSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
@@ -668,9 +709,10 @@ function AppNetworkSettings() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput
|
||||||
className="focus-visible:ring-0"
|
className="w-full"
|
||||||
placeholder="Enter rate limit in bytes/s"
|
placeholder="Enter rate limit in bytes/s"
|
||||||
|
min={0}
|
||||||
readOnly={useCustomCommands}
|
readOnly={useCustomCommands}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -853,7 +895,7 @@ function AppSponsorblockSettings() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="sponsorblock">
|
<div className="sponsorblock">
|
||||||
<h3 className="font-semibold">Sponsor Block</h3>
|
<h3 className="font-semibold">Sponsorblock</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<Switch
|
<Switch
|
||||||
@@ -978,6 +1020,220 @@ function AppSponsorblockSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppDelaySettings() {
|
||||||
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
|
const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger);
|
||||||
|
const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset);
|
||||||
|
|
||||||
|
const useDelay = useSettingsPageStatesStore(state => state.settings.use_delay);
|
||||||
|
const useSearchDelay = useSettingsPageStatesStore(state => state.settings.use_search_delay);
|
||||||
|
const delayMode = useSettingsPageStatesStore(state => state.settings.delay_mode);
|
||||||
|
const minSleepInterval = useSettingsPageStatesStore(state => state.settings.min_sleep_interval);
|
||||||
|
const maxSleepInterval = useSettingsPageStatesStore(state => state.settings.max_sleep_interval);
|
||||||
|
const requestSleepInterval = useSettingsPageStatesStore(state => state.settings.request_sleep_interval);
|
||||||
|
const delayPlaylistOnly = useSettingsPageStatesStore(state => state.settings.delay_playlist_only);
|
||||||
|
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||||
|
|
||||||
|
const minMaxSleepIntervalForm = useForm<z.infer<typeof minMaxSleepIntervalSchema>>({
|
||||||
|
resolver: zodResolver(minMaxSleepIntervalSchema),
|
||||||
|
defaultValues: {
|
||||||
|
min_sleep_interval: minSleepInterval,
|
||||||
|
max_sleep_interval: maxSleepInterval,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedMinSleepInterval = minMaxSleepIntervalForm.watch("min_sleep_interval");
|
||||||
|
const watchedMaxSleepInterval = minMaxSleepIntervalForm.watch("max_sleep_interval");
|
||||||
|
const { errors: minMaxSleepIntervalFormErrors } = minMaxSleepIntervalForm.formState;
|
||||||
|
|
||||||
|
function handleMinMaxSleepIntervalSubmit(values: z.infer<typeof minMaxSleepIntervalSchema>) {
|
||||||
|
try {
|
||||||
|
saveSettingsKey('min_sleep_interval', values.min_sleep_interval);
|
||||||
|
saveSettingsKey('max_sleep_interval', values.max_sleep_interval);
|
||||||
|
toast.success("Sleep Intervals updated", {
|
||||||
|
description: `Minimum Sleep Interval changed to ${values.min_sleep_interval} seconds, Maximum Sleep Interval changed to ${values.max_sleep_interval} seconds`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing sleep intervals:", error);
|
||||||
|
toast.error("Failed to change sleep intervals", {
|
||||||
|
description: "An error occurred while trying to change the sleep intervals. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestSleepIntervalForm = useForm<z.infer<typeof requestSleepIntervalSchema>>({
|
||||||
|
resolver: zodResolver(requestSleepIntervalSchema),
|
||||||
|
defaultValues: {
|
||||||
|
request_sleep_interval: requestSleepInterval,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedRequestSleepInterval = requestSleepIntervalForm.watch("request_sleep_interval");
|
||||||
|
const { errors: requestSleepIntervalFormErrors } = requestSleepIntervalForm.formState;
|
||||||
|
|
||||||
|
function handleRequestSleepIntervalSubmit(values: z.infer<typeof requestSleepIntervalSchema>) {
|
||||||
|
try {
|
||||||
|
saveSettingsKey('request_sleep_interval', values.request_sleep_interval);
|
||||||
|
toast.success("Request Sleep Interval updated", {
|
||||||
|
description: `Request Sleep Interval changed to ${values.request_sleep_interval} seconds`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing request sleep interval:", error);
|
||||||
|
toast.error("Failed to change request sleep interval", {
|
||||||
|
description: "An error occurred while trying to change the request sleep interval. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formResetTrigger > 0) {
|
||||||
|
minMaxSleepIntervalForm.reset();
|
||||||
|
requestSleepIntervalForm.reset();
|
||||||
|
acknowledgeFormReset();
|
||||||
|
}
|
||||||
|
}, [formResetTrigger]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="delay">
|
||||||
|
<h3 className="font-semibold">Delay</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Use delay to prevent potential issues with some sites (bypass rate-limit, temporary ban, etc.)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Switch
|
||||||
|
id="use-delay"
|
||||||
|
checked={useDelay}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_delay', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-delay">Use Delay in Downloads</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="use-search-delay"
|
||||||
|
checked={useSearchDelay}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_search_delay', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-search-delay">Use Delay in Search</Label>
|
||||||
|
</div>
|
||||||
|
<RadioGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
className="flex items-center gap-4"
|
||||||
|
value={delayMode}
|
||||||
|
onValueChange={(value) => saveSettingsKey('delay_mode', value)}
|
||||||
|
disabled={(!useDelay && !useSearchDelay) || useCustomCommands}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="auto" id="delay-auto" />
|
||||||
|
<Label htmlFor="delay-auto">Auto (Default)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="custom" id="delay-custom" />
|
||||||
|
<Label htmlFor="delay-custom">Custom</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<div className="flex flex-col gap-2 mt-5">
|
||||||
|
<Label className="text-xs mb-1">Minimum, Maximum Sleep Interval (in Seconds)</Label>
|
||||||
|
<Form {...minMaxSleepIntervalForm}>
|
||||||
|
<form onSubmit={minMaxSleepIntervalForm.handleSubmit(handleMinMaxSleepIntervalSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={minMaxSleepIntervalForm.control}
|
||||||
|
name="min_sleep_interval"
|
||||||
|
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Min sleep"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={minMaxSleepIntervalForm.control}
|
||||||
|
name="max_sleep_interval"
|
||||||
|
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Max sleep"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={(!watchedMinSleepInterval || Number(watchedMinSleepInterval) === minSleepInterval) && (!watchedMaxSleepInterval || Number(watchedMaxSleepInterval) === maxSleepInterval) || Object.keys(minMaxSleepIntervalFormErrors).length > 0 || delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 mt-4 mb-2">
|
||||||
|
<Label className="text-xs mb-1">Request Sleep Interval (in Seconds)</Label>
|
||||||
|
<Form {...requestSleepIntervalForm}>
|
||||||
|
<form onSubmit={requestSleepIntervalForm.handleSubmit(handleRequestSleepIntervalSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={requestSleepIntervalForm.control}
|
||||||
|
name="request_sleep_interval"
|
||||||
|
disabled={delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Request sleep"
|
||||||
|
min={0}
|
||||||
|
readOnly={useCustomCommands}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!watchedRequestSleepInterval || Number(watchedRequestSleepInterval) === requestSleepInterval || Object.keys(requestSleepIntervalFormErrors).length > 0 || delayMode !== "custom" || (!useDelay && !useSearchDelay)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<Label className="text-xs text-muted-foreground">(Configured: {minSleepInterval}s - {maxSleepInterval}s & {requestSleepInterval}s, Mode: {delayMode === 'auto' ? 'Auto' : 'Custom'}, Status: {useDelay && delayPlaylistOnly ? 'Playlist Only' : useDelay ? 'Downloads' : ''}{useDelay && useSearchDelay ? ', Search' : useSearchDelay ? 'Search' : !useDelay && !useSearchDelay ? 'Disabled' : ''}) (Default: 10s - 20s & 1s, Range: 1s - 3600s)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="delay-playlist-only">
|
||||||
|
<h3 className="font-semibold">Delay Playlist Only</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Only apply delay for playlist/batch downloads, single video downloads will not be affected (recommended)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="delay-playlist-only"
|
||||||
|
checked={delayPlaylistOnly}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('delay_playlist_only', checked)}
|
||||||
|
disabled={!useDelay || useCustomCommands}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AppNotificationSettings() {
|
function AppNotificationSettings() {
|
||||||
const { saveSettingsKey } = useSettings();
|
const { saveSettingsKey } = useSettings();
|
||||||
|
|
||||||
@@ -1468,6 +1724,7 @@ export function ApplicationSettings() {
|
|||||||
{ key: 'network', label: 'Network', icon: Wifi, component: <AppNetworkSettings /> },
|
{ key: 'network', label: 'Network', icon: Wifi, component: <AppNetworkSettings /> },
|
||||||
{ key: 'cookies', label: 'Cookies', icon: Cookie, component: <AppCookiesSettings /> },
|
{ key: 'cookies', label: 'Cookies', icon: Cookie, component: <AppCookiesSettings /> },
|
||||||
{ key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> },
|
{ key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: <AppSponsorblockSettings /> },
|
||||||
|
{ key: 'delay', label: 'Delay', icon: Timer, component: <AppDelaySettings /> },
|
||||||
{ key: 'notifications', label: 'Notifications', icon: BellRing, component: <AppNotificationSettings /> },
|
{ key: 'notifications', label: 'Notifications', icon: BellRing, component: <AppNotificationSettings /> },
|
||||||
{ key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> },
|
{ key: 'commands', label: 'Commands', icon: SquareTerminal, component: <AppCommandSettings /> },
|
||||||
{ key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> },
|
{ key: 'debug', label: 'Debug', icon: Bug, component: <AppDebugSettings /> },
|
||||||
@@ -1553,7 +1810,7 @@ export function ApplicationSettings() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
<div className="min-h-full flex flex-col w-full border-l border-border pl-4">
|
<div className="min-h-full flex flex-col w-full border-l border-border pl-4">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-108.75", tab.key === "info" ? "max-w-[80%]" : "max-w-[70%]")}>
|
<TabsContent key={tab.key} value={tab.key} className={clsx("flex flex-col gap-4 min-h-120", tab.key === "info" ? "max-w-[80%]" : "max-w-[70%]")}>
|
||||||
{tab.component}
|
{tab.component}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ArrowDownToLine, ArrowRight, EthernetPort, Loader2, Radio, RotateCw } from "lucide-react";
|
import { ArrowDownToLine, ArrowRight, EthernetPort, Loader2, Radio, RotateCw } from "lucide-react";
|
||||||
import { useSettings } from "@/helpers/use-settings";
|
import { useSettings } from "@/helpers/use-settings";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
@@ -15,6 +14,7 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from "@/component
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { SlidingButton } from "@/components/custom/slidingButton";
|
import { SlidingButton } from "@/components/custom/slidingButton";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { NumberInput } from "@/components/custom/numberInput";
|
||||||
|
|
||||||
const websocketPortSchema = z.object({
|
const websocketPortSchema = z.object({
|
||||||
port: z.coerce.number<number>({
|
port: z.coerce.number<number>({
|
||||||
@@ -167,9 +167,10 @@ function ExtPortSettings() {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInput
|
||||||
className="focus-visible:ring-0"
|
className="w-full"
|
||||||
placeholder="Enter port number"
|
placeholder="Enter port number"
|
||||||
|
min={0}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -61,7 +61,14 @@ export default function useDownloader() {
|
|||||||
log_verbose: LOG_VERBOSE,
|
log_verbose: LOG_VERBOSE,
|
||||||
log_progress: LOG_PROGRESS,
|
log_progress: LOG_PROGRESS,
|
||||||
enable_notifications: ENABLE_NOTIFICATIONS,
|
enable_notifications: ENABLE_NOTIFICATIONS,
|
||||||
download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION
|
download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION,
|
||||||
|
use_delay: USE_DELAY,
|
||||||
|
use_search_delay: USE_SEARCH_DELAY,
|
||||||
|
delay_mode: DELAY_MODE,
|
||||||
|
min_sleep_interval: MIN_SLEEP_INTERVAL,
|
||||||
|
max_sleep_interval: MAX_SLEEP_INTERVAL,
|
||||||
|
request_sleep_interval: REQUEST_SLEEP_INTERVAL,
|
||||||
|
delay_playlist_only: DELAY_PLAYLIST_ONLY,
|
||||||
} = useSettingsPageStatesStore(state => state.settings);
|
} = useSettingsPageStatesStore(state => state.settings);
|
||||||
|
|
||||||
const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
|
const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
|
||||||
@@ -167,6 +174,14 @@ export default function useDownloader() {
|
|||||||
args.push('--sponsorblock-mark', sponsorblockMark);
|
args.push('--sponsorblock-mark', sponsorblockMark);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_SEARCH_DELAY) {
|
||||||
|
if (DELAY_MODE === 'auto') {
|
||||||
|
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||||
|
} else if (DELAY_MODE === 'custom') {
|
||||||
|
args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||||
|
|
||||||
let jsonOutput = '';
|
let jsonOutput = '';
|
||||||
@@ -319,14 +334,41 @@ export default function useDownloader() {
|
|||||||
args.push('--output', `${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`);
|
args.push('--output', `${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_DELAY) {
|
||||||
|
if (!DELAY_PLAYLIST_ONLY) {
|
||||||
|
if (DELAY_MODE === 'auto') {
|
||||||
if (isMultiplePlaylistItems) {
|
if (isMultiplePlaylistItems) {
|
||||||
const playlistLength = playlistIndices.split(',').length;
|
const playlistLength = playlistIndices.split(',').length;
|
||||||
if (playlistLength > 5 && playlistLength < 100) {
|
if (playlistLength <= 5) {
|
||||||
args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '15');
|
args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '10');
|
||||||
|
} else if (playlistLength > 5 && playlistLength < 100) {
|
||||||
|
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||||
} else if (playlistLength >= 100 && playlistLength < 500) {
|
} else if (playlistLength >= 100 && playlistLength < 500) {
|
||||||
args.push('--sleep-requests', '1.5', '--sleep-interval', '10', '--max-sleep-interval', '40');
|
args.push('--sleep-requests', '2', '--sleep-interval', '20', '--max-sleep-interval', '40');
|
||||||
} else if (playlistLength >= 500) {
|
} else if (playlistLength >= 500) {
|
||||||
args.push('--sleep-requests', '2.5', '--sleep-interval', '20', '--max-sleep-interval', '60');
|
args.push('--sleep-requests', '2', '--sleep-interval', '40', '--max-sleep-interval', '60');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||||
|
}
|
||||||
|
} else if (DELAY_MODE === 'custom') {
|
||||||
|
args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString());
|
||||||
|
}
|
||||||
|
} else if (DELAY_PLAYLIST_ONLY && isMultiplePlaylistItems) {
|
||||||
|
if (DELAY_MODE === 'auto') {
|
||||||
|
const playlistLength = playlistIndices.split(',').length;
|
||||||
|
if (playlistLength <= 5) {
|
||||||
|
args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '10');
|
||||||
|
} else if (playlistLength > 5 && playlistLength < 100) {
|
||||||
|
args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20');
|
||||||
|
} else if (playlistLength >= 100 && playlistLength < 500) {
|
||||||
|
args.push('--sleep-requests', '2', '--sleep-interval', '20', '--max-sleep-interval', '40');
|
||||||
|
} else if (playlistLength >= 500) {
|
||||||
|
args.push('--sleep-requests', '2', '--sleep-interval', '40', '--max-sleep-interval', '60');
|
||||||
|
}
|
||||||
|
} else if (DELAY_MODE === 'custom') {
|
||||||
|
args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,13 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
enable_notifications: false,
|
enable_notifications: false,
|
||||||
update_notification: true,
|
update_notification: true,
|
||||||
download_completion_notification: false,
|
download_completion_notification: false,
|
||||||
|
use_delay: true,
|
||||||
|
use_search_delay: false,
|
||||||
|
delay_mode: 'auto',
|
||||||
|
min_sleep_interval: 10,
|
||||||
|
max_sleep_interval: 20,
|
||||||
|
request_sleep_interval: 1,
|
||||||
|
delay_playlist_only: true,
|
||||||
// extension settings
|
// extension settings
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
@@ -282,6 +289,13 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
enable_notifications: false,
|
enable_notifications: false,
|
||||||
update_notification: true,
|
update_notification: true,
|
||||||
download_completion_notification: false,
|
download_completion_notification: false,
|
||||||
|
use_delay: true,
|
||||||
|
use_search_delay: false,
|
||||||
|
delay_mode: 'auto',
|
||||||
|
min_sleep_interval: 10,
|
||||||
|
max_sleep_interval: 20,
|
||||||
|
request_sleep_interval: 1,
|
||||||
|
delay_playlist_only: true,
|
||||||
// extension settings
|
// extension settings
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ export interface Settings {
|
|||||||
enable_notifications: boolean;
|
enable_notifications: boolean;
|
||||||
update_notification: boolean;
|
update_notification: boolean;
|
||||||
download_completion_notification: boolean;
|
download_completion_notification: boolean;
|
||||||
|
use_delay: boolean;
|
||||||
|
use_search_delay: boolean;
|
||||||
|
delay_mode: string;
|
||||||
|
min_sleep_interval: number;
|
||||||
|
max_sleep_interval: number;
|
||||||
|
request_sleep_interval: number;
|
||||||
|
delay_playlist_only: boolean;
|
||||||
// extension settings
|
// extension settings
|
||||||
websocket_port: number;
|
websocket_port: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export const determineFileType = (
|
|||||||
const audioCodec = (acodec || '').toLowerCase();
|
const audioCodec = (acodec || '').toLowerCase();
|
||||||
|
|
||||||
const isNone = (str: string): boolean => {
|
const isNone = (str: string): boolean => {
|
||||||
return ['none', 'n/a', '-', ''].includes(str);
|
return ['none', 'auto', 'n/a', '-', ''].includes(str);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasVideo = !isNone(videoCodec);
|
const hasVideo = !isNone(videoCodec);
|
||||||
@@ -591,7 +591,7 @@ export const getMergedBestFormat = (
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...baseFormat,
|
...baseFormat,
|
||||||
format: 'Best Video (Automatic)',
|
format: 'Best Quality (Auto)',
|
||||||
format_id: 'best',
|
format_id: 'best',
|
||||||
format_note: 'auto',
|
format_note: 'auto',
|
||||||
ext: 'auto',
|
ext: 'auto',
|
||||||
|
|||||||
Reference in New Issue
Block a user