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 { useToast } from "@/hooks/use-toast"; import { ArrowDownToLine, ArrowRight, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, 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"; const websocketPortSchema = z.object({ port: z.coerce.number({ required_error: "Websocket Port is required", invalid_type_error: "Websocket Port must be a valid number", }).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.string().min(1, { message: "Proxy URL is required" }).url({ message: "Invalid URL format" }) }); export default function SettingsPage() { const { toast } = useToast(); 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 preferVideoOverPlaylist = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist); const showDownloadableStreamsOnly = useSettingsPageStatesStore(state => state.settings.show_downloadable_streams_only); const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy); const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url); 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 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 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 openLink = async (url: string, app: string | null) => { try { await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => { toast({ title: 'Opening Link', description: `Opening link with ${app ? app : 'default app'}.`, }) }); } catch (e) { console.error(e); toast({ title: 'Failed to open link', description: 'An error occurred while trying to open the link.', variant: "destructive" }) } } 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({ title: "Proxy URL updated", description: `Proxy URL changed to ${values.url}`, }); } catch (error) { console.error("Error changing proxy URL:", error); toast({ title: "Failed to change proxy URL", description: "Please try again.", variant: "destructive", }); } } 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({ title: "Websocket port updated", description: `Websocket port changed to ${values.port}`, }); } catch (error) { console.error("Error changing websocket port:", error); toast({ title: "Failed to change websocket port", description: "Please try again.", variant: "destructive", }); } 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

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

Show Downloadable Streams Only (Strict)

Check, filter-out and show the streams that are actualy downloadable (high quality results, takes longer time to search, start a download)

saveSettingsKey('show_downloadable_streams_only', checked)} />

Theme

Choose app interface theme

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

Download Folder

Set default download folder (directory)

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

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 befor clicking the link

Websocket Port

Change extension websocket server port

( )} />
) }