import Heading from "@/components/heading"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useBasePathsStore, 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 { ArrowDownToLine, ArrowRight, BrushCleaning, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react"; import { cn } from "@/lib/utils"; import { useEffect } from "react"; import { useTheme } from "@/providers/themeProvider"; 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 { invoke } from "@tauri-apps/api/core"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { SlidingButton } from "@/components/custom/slidingButton"; 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 } from "@/utils"; import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup"; const websocketPortSchema = z.object({ port: z.coerce.number({ error: (issue) => issue.input === undefined || issue.input === null || issue.input === "" ? "Websocket Port is required" : "Websocket Port must be a valid number" }).int({ message: "Websocket Port must be an integer" }).min(50000, { message: "Websocket Port must be at least 50000" }).max(60000, { message: "Websocket Port must be at most 60000" }), }) 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)" }), }); export default function SettingsPage() { const { setTheme } = useTheme(); const activeTab = useSettingsPageStatesStore(state => state.activeTab); const activeSubAppTab = useSettingsPageStatesStore(state => state.activeSubAppTab); const activeSubExtTab = useSettingsPageStatesStore(state => state.activeSubExtTab); const setActiveTab = useSettingsPageStatesStore(state => state.setActiveTab); const setActiveSubAppTab = useSettingsPageStatesStore(state => state.setActiveSubAppTab); const setActiveSubExtTab = useSettingsPageStatesStore(state => state.setActiveSubExtTab); const isUsingDefaultSettings = useSettingsPageStatesStore(state => state.isUsingDefaultSettings); const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion); const isFetchingYtDlpVersion = useSettingsPageStatesStore(state => state.isFetchingYtDlpVersion); const isUpdatingYtDlp = useSettingsPageStatesStore(state => state.isUpdatingYtDlp); const ytDlpUpdateChannel = useSettingsPageStatesStore(state => state.settings.ytdlp_update_channel); const ytDlpAutoUpdate = useSettingsPageStatesStore(state => state.settings.ytdlp_auto_update); const appTheme = useSettingsPageStatesStore(state => state.settings.theme); 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 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 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 embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata); const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata); const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail); 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 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 useAria2 = useSettingsPageStatesStore(state => state.settings.use_aria2); const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port); const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort); const setIsChangingWebSocketPort = useSettingsPageStatesStore(state => state.setIsChangingWebSocketPort); const isRestartingWebSocketServer = useSettingsPageStatesStore(state => state.isRestartingWebSocketServer); const setIsRestartingWebSocketServer = useSettingsPageStatesStore(state => state.setIsRestartingWebSocketServer); const downloadStates = useDownloadStatesStore(state => state.downloadStates); const ongoingDownloads = downloadStates.filter(state => ['starting', 'downloading', 'queued'].includes(state.download_status) ); const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath); const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath); const setPath = useBasePathsStore((state) => state.setPath); const { saveSettingsKey, resetSettings } = useSettings(); const { updateYtDlp } = useYtDlpUpdater(); 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 sponsorblockCategories = [ { 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' }, ]; const openLink = async (url: string, app: string | null) => { try { await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => { toast.info("Opening link", { description: `Opening link with ${app ? app : 'default app'}.`, }) }); } catch (e) { console.error(e); toast.error("Failed to open link", { description: "An error occurred while trying to open the link.", }) } } 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 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.", }); } } interface Config { port: number; } const websocketPortForm = useForm>({ resolver: zodResolver(websocketPortSchema), defaultValues: { port: websocketPort, }, mode: "onChange", }); const watchedPort = websocketPortForm.watch("port"); const { errors: websocketPortFormErrors } = websocketPortForm.formState; async function handleWebsocketPortSubmit(values: z.infer) { setIsChangingWebSocketPort(true); try { // const port = parseInt(values.port, 10); const updatedConfig: Config = await invoke("update_config", { newConfig: { port: values.port, } }); saveSettingsKey('websocket_port', updatedConfig.port); toast.success("Websocket port updated", { description: `Websocket port changed to ${values.port}`, }); } catch (error) { console.error("Error changing websocket port:", error); toast.error("Failed to change websocket port", { description: "An error occurred while trying to change the websocket port. Please try again.", }); } finally { setIsChangingWebSocketPort(false); } } useEffect(() => { const updateTheme = async () => { setTheme(appTheme); } updateTheme().catch(console.error); }, [appTheme]); return (
Application Extension Reset settings to default? Are you sure you want to reset all settings to their default values? This action cannot be undone! Cancel resetSettings() }>Reset

YT-DLP

Version: {isFetchingYtDlpVersion ? 'Loading...' : ytDlpVersion ?? 'unknown'}

saveSettingsKey('ytdlp_auto_update', checked)} />
General Appearance Folders Formats Metadata Network Cookies Sponsorblock

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 for large files and unstable connections, resuming is not supported)

saveSettingsKey('use_aria2', checked)} />

Theme

Choose app interface theme

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

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

Video Format

Choose in which format the final video file will be saved

saveSettingsKey('video_format', value)} >

Audio Format

Choose in which format the final audio file will be saved

saveSettingsKey('audio_format', value)} >

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)} />

Embed Metadata

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

saveSettingsKey('embed_video_metadata', checked)} />
saveSettingsKey('embed_audio_metadata', checked)} />

Embed Thumbnail in Audio

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

saveSettingsKey('embed_audio_thumbnail', checked)} />

Proxy

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

saveSettingsKey('use_proxy', checked)} />
( )} />

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)} />
( )} />

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)} />
saveSettingsKey('import_cookies_from', value)} disabled={!useCookies} >

Sponsor Block

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

saveSettingsKey('use_sponsorblock', checked)} />
saveSettingsKey('sponsorblock_mode', value)} disabled={!useSponsorblock} >
saveSettingsKey('sponsorblock_remove', value)} disabled={!useSponsorblock || sponsorblockMode !== "remove"} >
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"} >
{sponsorblockCategories.map((category) => ( category.code !== "poi_highlight" && ( {category.label} ) ))}
saveSettingsKey('sponsorblock_mark', value)} disabled={!useSponsorblock || sponsorblockMode !== "mark"} >
cat.code) : sponsorblockMark === "all" ? sponsorblockCategories.map((cat) => cat.code) : []} onValueChange={(value) => saveSettingsKey('sponsorblock_mark_categories', value)} disabled={!useSponsorblock || sponsorblockMode !== "mark" || sponsorblockMark !== "custom"} >
{sponsorblockCategories.map((category) => ( {category.label} ))}

Extension Websocket Server

{isChangingWebSocketPort || isRestartingWebSocketServer ? ( <>
Restarting... ) : ( <>
Running )}
Install Port

NeoDLP Extension

Integrate NeoDLP with your favourite browser

Get Now
} onClick={() => openLink('https://chromewebstore.google.com/detail/neo-downloader-plus/mehopeailfjmiloiiohgicphlcgpompf', 'chrome')} > Get Chrome Extension from Chrome Web Store Get Now
} onClick={() => openLink('https://addons.mozilla.org/en-US/firefox/addon/neo-downloader-plus', 'firefox')} > Get Firefox Extension from Mozilla Addons Store

* These links opens with coresponding browsers only. Make sure the browser is installed before clicking the link

Websocket Port

Change extension websocket server port

( )} />
) }