import { useEffect } from "react"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useBasePathsStore, useDownloaderPageStatesStore, useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; 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 { cn } from "@/lib/utils"; import { Slider } from "@/components/ui/slider"; import { Input } from "@/components/ui/input"; import { open } from '@tauri-apps/plugin-dialog'; import { useSettings } from "@/helpers/use-settings"; import { useYtDlpUpdater } from "@/helpers/use-ytdlp-updater"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import * as fs from "@tauri-apps/plugin-fs"; import { join } from "@tauri-apps/api/path"; import { formatSpeed, generateID } from "@/utils"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification'; import { NeoDlpLogo } from "@/components/icons/neodlp"; import clsx from "clsx"; import { Badge } from "@/components/ui/badge"; import { config } from "@/config"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; const proxyUrlSchema = z.object({ url: z.url({ error: (issue) => issue.input === undefined || issue.input === null || issue.input === "" ? "Proxy URL is required" : "Invalid URL format" }) }); const rateLimitSchema = z.object({ rate_limit: z.coerce.number({ error: (issue) => issue.input === undefined || issue.input === null || issue.input === "" ? "Rate Limit is required" : "Rate Limit must be a valid number" }).int({ message: "Rate Limit must be an integer" }).min(1024, { message: "Rate Limit must be at least 1024 bytes/s (1 KB/s)" }).max(104857600, { message: "Rate Limit must be at most 104857600 bytes/s (100 MB/s)" }), }); const addCustomCommandSchema = z.object({ label: z.string().min(1, { message: "Label is required" }), args: z.string().min(1, { message: "Arguments are required" }), }); const filenameTemplateShcema = z.object({ template: z.string().min(1, { message: "Filename Template is required" }), }); function AppGeneralSettings() { const { saveSettingsKey } = useSettings(); const maxParallelDownloads = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads); const maxRetries = useSettingsPageStatesStore(state => state.settings.max_retries); const preferVideoOverPlaylist = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist); const strictDownloadabilityCheck = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check); const useAria2 = useSettingsPageStatesStore(state => state.settings.use_aria2); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); return ( <>

Max Parallel Downloads

Set maximum number of allowed parallel downloads

saveSettingsKey('max_parallel_downloads', value[0])} />

Prefer Video Over Playlist

Prefer only the video, if the URL refers to a video and a playlist

saveSettingsKey('prefer_video_over_playlist', checked)} />

Strict Downloadablity Check

Only show streams that are actualy downloadable, also check formats before downloading (high quality results, takes longer time to search)

saveSettingsKey('strict_downloadablity_check', checked)} />

Max Retries

Set maximum number of retries for a download before giving up

saveSettingsKey('max_retries', value[0])} />

Aria2

Use aria2c as external downloader (recommended only if you are experiancing too slow download speeds with native downloader, you need to install aria2 via homebrew if you are on macos to use this feature)

saveSettingsKey('use_aria2', checked)} disabled={useCustomCommands} />
); } function AppAppearanceSettings() { const { saveSettingsKey } = useSettings(); const appTheme = useSettingsPageStatesStore(state => state.settings.theme); const appColorScheme = useSettingsPageStatesStore(state => state.settings.color_scheme); const themeOptions: { value: string; icon: LucideIcon; label: string }[] = [ { value: 'light', icon: Sun, label: 'Light' }, { value: 'dark', icon: Moon, label: 'Dark' }, { value: 'system', icon: Monitor, label: 'System' }, ]; const colorSchemeOptions: { value: string; label: string }[] = [ { value: 'default', label: 'Default' }, { value: 'blue', label: 'Blue' }, { value: 'green', label: 'Green' }, { value: 'orange', label: 'Orange' }, { value: 'red', label: 'Red' }, { value: 'rose', label: 'Rose' }, { value: 'violet', label: 'Violet' }, { value: 'yellow', label: 'Yellow' }, ]; return ( <>

Theme

Choose app interface theme

{themeOptions.map(({ value, icon: Icon, label }) => ( ))}

Color Scheme

Choose app interface color scheme

saveSettingsKey('color_scheme', value)} >
{colorSchemeOptions.map(({ value, label }) => ( { } {label} ))}
); } function AppFolderSettings() { const { saveSettingsKey } = useSettings(); const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger); const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset); const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath); const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath); const setPath = useBasePathsStore((state) => state.setPath); const filenameTemplate = useSettingsPageStatesStore(state => state.settings.filename_template); const downloadStates = useDownloadStatesStore(state => state.downloadStates); const ongoingDownloads = downloadStates.filter(state => ['starting', 'downloading', 'queued'].includes(state.download_status) ); const cleanTemporaryDownloads = async () => { const tempFiles = await fs.readDir(tempDownloadDirPath ?? ''); if (tempFiles.length > 0) { try { for (const file of tempFiles) { if (file.isFile) { const filePath = await join(tempDownloadDirPath ?? '', file.name); await fs.remove(filePath); } } toast.success("Temporary Downloads Cleaned", { description: "All temporary downloads have been successfully cleaned up.", }); } catch (e) { toast.error("Temporary Downloads Cleanup Failed", { description: "An error occurred while trying to clean up temporary downloads. Please try again.", }); } } else { toast.info("No Temporary Downloads", { description: "There are no temporary downloads to clean up.", }); } } const filenameTemplateForm = useForm>({ resolver: zodResolver(filenameTemplateShcema), defaultValues: { template: filenameTemplate, }, mode: "onChange", }); const watchedFilenameTemplate = filenameTemplateForm.watch("template"); const { errors: filenameTemplateFormErrors } = filenameTemplateForm.formState; function handleFilenameTemplateSubmit(values: z.infer) { try { saveSettingsKey('filename_template', values.template); toast.success("Filename Template updated", { description: `Filename Template changed to ${values.template}`, }); } catch (error) { console.error("Error changing filename template:", error); toast.error("Failed to change filename template", { description: "An error occurred while trying to change the filename template. Please try again.", }); } } useEffect(() => { if (formResetTrigger > 0) { filenameTemplateForm.reset(); acknowledgeFormReset(); } }, [formResetTrigger]); return ( <>

Download Folder

Set default download folder (directory)

Temporary Download Folder

Clean up temporary downloads (broken, cancelled, paused downloads)

Clean up all temporary downloads? Are you sure you want to clean up all temporary downloads? This will remove all broken, cancelled and paused downloads from the temporary folder. Paused downloads will re-start from the begining. This action cannot be undone! Cancel cleanTemporaryDownloads()} >Clean

Filename Template

Set the template for naming downloaded files (download id, file extension and playlist index will be auto-appended, changing template may cause paused downloads to re-start from begining)

( )} />
); } function AppFormatSettings() { const { saveSettingsKey } = useSettings(); const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format); const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format); const alwaysReencodeVideo = useSettingsPageStatesStore(state => state.settings.always_reencode_video); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); return ( <>

Video Format

Choose in which format the final video file will be saved

saveSettingsKey('video_format', value)} disabled={useCustomCommands} >

Audio Format

Choose in which format the final audio file will be saved

saveSettingsKey('audio_format', value)} disabled={useCustomCommands} >

Always Re-Encode Video

Instead of remuxing (simple container change) always re-encode the video to the target format with best compatible codecs (better compatibility, takes longer processing time)

saveSettingsKey('always_reencode_video', checked)} disabled={useCustomCommands} />
); } function AppEmbeddingSettings() { const { saveSettingsKey } = useSettings(); const embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata); const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata); const embedVideoThumbnail = useSettingsPageStatesStore(state => state.settings.embed_video_thumbnail); const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); return ( <>

Embed Metadata

Wheather to embed metadata in video/audio files (info, chapters)

saveSettingsKey('embed_video_metadata', checked)} disabled={useCustomCommands} />
saveSettingsKey('embed_audio_metadata', checked)} disabled={useCustomCommands} />

Embed Thumbnail

Wheather to embed thumbnail in video/audio files (as cover art)

saveSettingsKey('embed_video_thumbnail', checked)} disabled={useCustomCommands} />
saveSettingsKey('embed_audio_thumbnail', checked)} disabled={useCustomCommands} />
); } function AppNetworkSettings() { const { saveSettingsKey } = useSettings(); const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger); const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset); const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy); const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url); const useRateLimit = useSettingsPageStatesStore(state => state.settings.use_rate_limit); const rateLimit = useSettingsPageStatesStore(state => state.settings.rate_limit); const useForceInternetProtocol = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol); const forceInternetProtocol = useSettingsPageStatesStore(state => state.settings.force_internet_protocol); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); const proxyUrlForm = useForm>({ resolver: zodResolver(proxyUrlSchema), defaultValues: { url: proxyUrl, }, mode: "onChange", }); const watchedProxyUrl = proxyUrlForm.watch("url"); const { errors: proxyUrlFormErrors } = proxyUrlForm.formState; function handleProxyUrlSubmit(values: z.infer) { try { saveSettingsKey('proxy_url', values.url); toast.success("Proxy URL updated", { description: `Proxy URL changed to ${values.url}`, }); } catch (error) { console.error("Error changing proxy URL:", error); toast.error("Failed to change proxy URL", { description: "An error occurred while trying to change the proxy URL. Please try again.", }); } } const rateLimitForm = useForm>({ resolver: zodResolver(rateLimitSchema), defaultValues: { rate_limit: rateLimit, }, mode: "onChange", }); const watchedRateLimit = rateLimitForm.watch("rate_limit"); const { errors: rateLimitFormErrors } = rateLimitForm.formState; function handleRateLimitSubmit(values: z.infer) { try { saveSettingsKey('rate_limit', values.rate_limit); toast.success("Rate Limit updated", { description: `Rate Limit changed to ${values.rate_limit} bytes/s`, }); } catch (error) { console.error("Error changing rate limit:", error); toast.error("Failed to change rate limit", { description: "An error occurred while trying to change the rate limit. Please try again.", }); } } useEffect(() => { if (formResetTrigger > 0) { proxyUrlForm.reset(); rateLimitForm.reset(); acknowledgeFormReset(); } }, [formResetTrigger]); return ( <>

Proxy

Use proxy for downloads, Unblocks blocked sites in your region (download speed may affect, some sites may not work)

saveSettingsKey('use_proxy', checked)} disabled={useCustomCommands} />
( )} />

Rate Limit

Limit download speed to prevent network congestion. Rate limit is applied per-download basis (not in the whole app)

saveSettingsKey('use_rate_limit', checked)} disabled={useCustomCommands} />
( )} />

Force Internet Protocol

Force use a specific internet protocol (ipv4/ipv6) for all downloads, useful if your network supports only one (some sites may not work)

saveSettingsKey('use_force_internet_protocol', checked)} disabled={useCustomCommands} />
saveSettingsKey('force_internet_protocol', value)} disabled={!useForceInternetProtocol || useCustomCommands} >
); } function AppCookiesSettings() { const { saveSettingsKey } = useSettings(); const useCookies = useSettingsPageStatesStore(state => state.settings.use_cookies); const importCookiesFrom = useSettingsPageStatesStore(state => state.settings.import_cookies_from); const cookiesBrowser = useSettingsPageStatesStore(state => state.settings.cookies_browser); const cookiesFile = useSettingsPageStatesStore(state => state.settings.cookies_file); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); return ( <>

Cookies

Use cookies to access exclusive/private (login-protected) contents from sites (use wisely, over-use can even block/ban your account)

saveSettingsKey('use_cookies', checked)} disabled={useCustomCommands} />
saveSettingsKey('import_cookies_from', value)} disabled={!useCookies || useCustomCommands} >
); } function AppSponsorblockSettings() { const { saveSettingsKey } = useSettings(); const useSponsorblock = useSettingsPageStatesStore(state => state.settings.use_sponsorblock); const sponsorblockMode = useSettingsPageStatesStore(state => state.settings.sponsorblock_mode); const sponsorblockRemove = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove); const sponsorblockMark = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark); const sponsorblockRemoveCategories = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove_categories); const sponsorblockMarkCategories = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark_categories); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); const sponsorblockCategories: { code: string; label: string }[] = [ { code: 'sponsor', label: 'Sponsorship' }, { code: 'intro', label: 'Intro' }, { code: 'outro', label: 'Outro' }, { code: 'interaction', label: 'Interaction' }, { code: 'selfpromo', label: 'Self Promotion' }, { code: 'music_offtopic', label: 'Music Offtopic' }, { code: 'preview', label: 'Preview' }, { code: 'filler', label: 'Filler' }, { code: 'poi_highlight', label: 'Point of Interest' }, { code: 'chapter', label: 'Chapter' }, { code: 'hook', label: 'Hook' }, ]; return ( <>

Sponsor Block

Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)

saveSettingsKey('use_sponsorblock', checked)} disabled={useCustomCommands} />
saveSettingsKey('sponsorblock_mode', value)} disabled={!useSponsorblock || useCustomCommands} >
saveSettingsKey('sponsorblock_remove', value)} disabled={/*!useSponsorblock || sponsorblockMode !== "remove" ||*/ useCustomCommands} >
cat.code !== 'poi_highlight' && cat.code !== 'filler').map((cat) => cat.code) : sponsorblockRemove === "all" ? sponsorblockCategories.filter((cat) => cat.code !== 'poi_highlight').map((cat) => cat.code) : []} onValueChange={(value) => saveSettingsKey('sponsorblock_remove_categories', value)} disabled={/*!useSponsorblock || sponsorblockMode !== "remove" ||*/ sponsorblockRemove !== "custom" || useCustomCommands} >
{sponsorblockCategories.map((category) => ( category.code !== "poi_highlight" && ( {category.label} ) ))}
saveSettingsKey('sponsorblock_mark', value)} disabled={/*!useSponsorblock || sponsorblockMode !== "mark" ||*/ useCustomCommands} >
cat.code) : sponsorblockMark === "all" ? sponsorblockCategories.map((cat) => cat.code) : []} onValueChange={(value) => saveSettingsKey('sponsorblock_mark_categories', value)} disabled={/*!useSponsorblock || sponsorblockMode !== "mark" ||*/ sponsorblockMark !== "custom" || useCustomCommands} >
{sponsorblockCategories.map((category) => ( {category.label} ))}
); } function AppNotificationSettings() { const { saveSettingsKey } = useSettings(); const enableNotifications = useSettingsPageStatesStore(state => state.settings.enable_notifications); const updateNotification = useSettingsPageStatesStore(state => state.settings.update_notification); const downloadCompletionNotification = useSettingsPageStatesStore(state => state.settings.download_completion_notification); return ( <>

Desktop Notifications

Enable desktop notifications for app events (updates, download completions, etc.)

{ if (checked) { const granted = await isPermissionGranted(); if (!granted) { const permission = await requestPermission(); if (permission !== 'granted') { toast.error("Notification Permission Denied", { description: "You have denied the notification permission. Please enable it from your system settings to receive notifications.", }); return; } } } saveSettingsKey('enable_notifications', checked) }} />
saveSettingsKey('update_notification', checked)} disabled={!enableNotifications} />
saveSettingsKey('download_completion_notification', checked)} disabled={!enableNotifications} />
); } function AppCommandSettings() { const { saveSettingsKey } = useSettings(); const formResetTrigger = useSettingsPageStatesStore(state => state.formResetTrigger); const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands); const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey); const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration); const addCustomCommandForm = useForm>({ resolver: zodResolver(addCustomCommandSchema), defaultValues: { label: '', args: '', }, mode: "onChange", }); const watchedLabel = addCustomCommandForm.watch("label"); const watchedArgs = addCustomCommandForm.watch("args"); const { errors: addCustomCommandFormErrors } = addCustomCommandForm.formState; function handleAddCustomCommandSubmit(values: z.infer) { try { const newCommand = { id: generateID(), label: values.label, args: values.args, }; const updatedCommands = [...customCommands, newCommand]; saveSettingsKey('custom_commands', updatedCommands); toast.success("Custom Command added", { description: `Custom Command "${values.label}" added.`, }); addCustomCommandForm.reset(); } catch (error) { console.error("Error adding custom command:", error); toast.error("Failed to add custom command", { description: "An error occurred while trying to add the custom command. Please try again.", }); } } function handleRemoveCustomCommandSubmit(commandId: string) { try { const removedCommand = customCommands.find(command => command.id === commandId); const updatedCommands = customCommands.filter(command => command.id !== commandId); saveSettingsKey('custom_commands', updatedCommands); setDownloadConfigurationKey('custom_command', null); toast.success("Custom Command removed", { description: `Custom Command "${removedCommand?.label}" removed.`, }); } catch (error) { console.error("Error removing custom command:", error); toast.error("Failed to remove custom command", { description: "An error occurred while trying to remove the custom command. Please try again.", }); } } useEffect(() => { if (formResetTrigger > 0) { addCustomCommandForm.reset(); acknowledgeFormReset(); } }, [formResetTrigger]); return ( <>

Custom Commands

Run custom yt-dlp commands for your downloads

Most Settings will be Disabled! This feature is intended for advanced users only. Turning it on will disable most other settings in the app. Make sure you know what you are doing before using this feature, otherwise things could break easily.
{ saveSettingsKey('use_custom_commands', checked) resetDownloadConfiguration(); }} />
(