import Heading from "@/components/heading"; 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 { ArrowDownToLine, ArrowRight, BrushCleaning, Bug, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, 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, generateID } from "@/utils"; import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup"; import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 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)" }), }); 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" }), }); 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 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 customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands); const filenameTemplate = useSettingsPageStatesStore(state => state.settings.filename_template); const debugMode = useSettingsPageStatesStore(state => state.settings.debug_mode); const logVerbose = useSettingsPageStatesStore(state => state.settings.log_verbose); const logWarning = useSettingsPageStatesStore(state => state.settings.log_warning); const logProgress = useSettingsPageStatesStore(state => state.settings.log_progress); 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 setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey); const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration); 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.", }); } } 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.", }); } } 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.", }); } } 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() proxyUrlForm.reset(); rateLimitForm.reset(); addCustomCommandForm.reset(); filenameTemplateForm.reset(); websocketPortForm.reset(); } }>Reset

YT-DLP

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

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

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

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

Filename Template

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

( )} />

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

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 in Audio

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

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

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

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

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

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