mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-19 23:39:33 +05:30
(refactor): improved settings categorization and layout
This commit is contained in:
@@ -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 { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { ExternalLink, FolderOpen, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, Sun, Terminal } from "lucide-react";
|
import { ExternalLink, Folder, FolderOpen, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTheme } from "@/providers/themeProvider";
|
import { useTheme } from "@/providers/themeProvider";
|
||||||
@@ -157,11 +157,40 @@ export default function SettingsPage() {
|
|||||||
<div className="container mx-auto p-4 space-y-4 min-h-screen">
|
<div className="container mx-auto p-4 space-y-4 min-h-screen">
|
||||||
<Heading title="Settings" description="Manage your preferences and app settings" />
|
<Heading title="Settings" description="Manage your preferences and app settings" />
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<TabsList>
|
<div className="w-full flex items-center justify-between">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsList>
|
||||||
<TabsTrigger value="extension">Extension</TabsTrigger>
|
<TabsTrigger value="app">Application</TabsTrigger>
|
||||||
</TabsList>
|
<TabsTrigger value="extension">Extension</TabsTrigger>
|
||||||
<TabsContent value="general">
|
</TabsList>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={isUsingDefaultSettings}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Reset settings to default?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to reset all settings to their default values? This action cannot be undone!
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={
|
||||||
|
() => resetSettings()
|
||||||
|
}>Reset</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
<TabsContent value="app">
|
||||||
<Card className="p-4 space-y-4 my-4">
|
<Card className="p-4 space-y-4 my-4">
|
||||||
<div className="w-full flex gap-4 items-center justify-between">
|
<div className="w-full flex gap-4 items-center justify-between">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
@@ -220,124 +249,160 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex flex-col w-[50%] gap-4">
|
<Tabs
|
||||||
<div className="app-theme">
|
orientation="vertical"
|
||||||
<h3 className="font-semibold">Theme</h3>
|
defaultValue="general"
|
||||||
<p className="text-sm text-muted-foreground mb-3">Choose app interface theme</p>
|
className="w-full flex flex-row items-start gap-4 mt-10"
|
||||||
<div className={cn('inline-flex gap-1 rounded-lg p-1 bg-muted')}>
|
>
|
||||||
{themeOptions.map(({ value, icon: Icon, label }) => (
|
<TabsList className="shrink-0 grid grid-cols-1 gap-1 p-0 bg-background min-w-45">
|
||||||
<button
|
<TabsTrigger
|
||||||
key={value}
|
key="general"
|
||||||
onClick={() => saveSettingsKey('theme', value)}
|
value="general"
|
||||||
className={cn(
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
|
><Wrench className="size-4" /> General</TabsTrigger>
|
||||||
appTheme === value
|
<TabsTrigger
|
||||||
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
|
key="appearance"
|
||||||
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
|
value="appearance"
|
||||||
)}
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
>
|
><WandSparkles className="size-4" /> Appearance</TabsTrigger>
|
||||||
<Icon className="-ml-1 h-4 w-4" />
|
<TabsTrigger
|
||||||
<span className="ml-1.5 text-sm">{label}</span>
|
key="folders"
|
||||||
</button>
|
value="folders"
|
||||||
))}
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
</div>
|
><Folder className="size-4" /> Folders</TabsTrigger>
|
||||||
</div>
|
<TabsTrigger
|
||||||
<div className="download-dir">
|
key="network"
|
||||||
<h3 className="font-semibold">Download Directory</h3>
|
value="network"
|
||||||
<p className="text-sm text-muted-foreground mb-3">Set default download directory</p>
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
<div className="flex items-center gap-4">
|
><Wifi className="size-4" /> Network</TabsTrigger>
|
||||||
<Input className="focus-visible:ring-0" type="text" placeholder="Select download directory" value={downloadDirPath ?? 'Unknown'} readOnly/>
|
</TabsList>
|
||||||
<Button
|
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
||||||
variant="outline"
|
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[150px]">
|
||||||
onClick={async () => {
|
<div className="max-parallel-downloads">
|
||||||
try {
|
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
||||||
const folder = await open({
|
<p className="text-xs text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
||||||
multiple: false,
|
<Slider
|
||||||
directory: true,
|
id="max-parallel-downloads"
|
||||||
});
|
className="w-[350px] mb-2"
|
||||||
if (folder) {
|
value={[maxParallelDownloads]}
|
||||||
saveSettingsKey('download_dir', folder);
|
min={1}
|
||||||
setPath('downloadDirPath', folder);
|
max={5}
|
||||||
}
|
onValueChange={(value) => saveSettingsKey('max_parallel_downloads', value[0])}
|
||||||
} catch (error) {
|
/>
|
||||||
console.error("Error selecting folder:", error);
|
<Label htmlFor="max-parallel-downloads" className="text-xs text-muted-foreground">(Current: {maxParallelDownloads}) (Default: 2, Maximum: 5)</Label>
|
||||||
toast({
|
</div>
|
||||||
title: "Failed to select folder",
|
<div className="prefer-video-over-playlist">
|
||||||
description: "Please try again.",
|
<h3 className="font-semibold">Prefer Video Over Playlist</h3>
|
||||||
variant: "destructive",
|
<p className="text-xs text-muted-foreground mb-3">Prefer only the video, if the URL refers to a video and a playlist</p>
|
||||||
});
|
<Switch
|
||||||
}
|
id="prefer-video-over-playlist"
|
||||||
}}
|
checked={preferVideoOverPlaylist}
|
||||||
>
|
onCheckedChange={(checked) => saveSettingsKey('prefer_video_over_playlist', checked)}
|
||||||
<FolderOpen className="w-4 h-4" /> Browse
|
/>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</div>
|
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[150px]">
|
||||||
<div className="max-parallel-downloads">
|
<div className="app-theme">
|
||||||
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
<h3 className="font-semibold">Theme</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
||||||
<Slider
|
<div className={cn('inline-flex gap-1 rounded-lg p-1 bg-muted')}>
|
||||||
id="max-parallel-downloads"
|
{themeOptions.map(({ value, icon: Icon, label }) => (
|
||||||
className="w-[350px] mb-2"
|
<button
|
||||||
value={[maxParallelDownloads]}
|
key={value}
|
||||||
min={1}
|
onClick={() => saveSettingsKey('theme', value)}
|
||||||
max={5}
|
className={cn(
|
||||||
onValueChange={(value) => saveSettingsKey('max_parallel_downloads', value[0])}
|
'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
|
||||||
/>
|
appTheme === value
|
||||||
<Label htmlFor="max-parallel-downloads" className="text-xs text-muted-foreground">(Current: {maxParallelDownloads}) (Default: 2, Maximum: 5)</Label>
|
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
|
||||||
</div>
|
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
|
||||||
<div className="prefer-video-over-playlist">
|
)}
|
||||||
<h3 className="font-semibold">Prefer Video Over Playlist</h3>
|
>
|
||||||
<p className="text-sm text-muted-foreground mb-3">Prefer only the video, if the URL refers to a video and a playlist</p>
|
<Icon className="-ml-1 h-4 w-4" />
|
||||||
<Switch
|
<span className="ml-1.5 text-sm">{label}</span>
|
||||||
id="prefer-video-over-playlist"
|
</button>
|
||||||
checked={preferVideoOverPlaylist}
|
))}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('prefer_video_over_playlist', checked)}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
<div className="proxy">
|
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[150px]">
|
||||||
<h3 className="font-semibold">Proxy</h3>
|
<div className="download-dir">
|
||||||
<p className="text-sm text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (Download speed may affect, Some sites may not work)</p>
|
<h3 className="font-semibold">Download Folder</h3>
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<p className="text-xs text-muted-foreground mb-3">Set default download folder (directory)</p>
|
||||||
<Switch
|
<div className="flex items-center gap-4">
|
||||||
id="use-proxy"
|
<Input className="focus-visible:ring-0" type="text" placeholder="Select download directory" value={downloadDirPath ?? 'Unknown'} readOnly/>
|
||||||
checked={useProxy}
|
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_proxy', checked)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Form {...proxyUrlForm}>
|
|
||||||
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
|
||||||
<FormField
|
|
||||||
control={proxyUrlForm.control}
|
|
||||||
name="url"
|
|
||||||
disabled={!useProxy}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
className="focus-visible:ring-0"
|
|
||||||
placeholder="Enter proxy URL"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy ? 'Enabled' : 'Disabled'})</Label>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
variant="outline"
|
||||||
disabled={!watchedProxyUrl || watchedProxyUrl === proxyUrl || Object.keys(proxyUrlFormErrors).length > 0 || !useProxy}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const folder = await open({
|
||||||
|
multiple: false,
|
||||||
|
directory: true,
|
||||||
|
});
|
||||||
|
if (folder) {
|
||||||
|
saveSettingsKey('download_dir', folder);
|
||||||
|
setPath('downloadDirPath', folder);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error selecting folder:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to select folder",
|
||||||
|
description: "Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Save
|
<FolderOpen className="w-4 h-4" /> Browse
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</div>
|
||||||
</Form>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
|
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[150px]">
|
||||||
|
<div className="proxy">
|
||||||
|
<h3 className="font-semibold">Proxy</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (Download speed may affect, Some sites may not work)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="use-proxy"
|
||||||
|
checked={useProxy}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_proxy', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Form {...proxyUrlForm}>
|
||||||
|
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={proxyUrlForm.control}
|
||||||
|
name="url"
|
||||||
|
disabled={!useProxy}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="focus-visible:ring-0"
|
||||||
|
placeholder="Enter proxy URL"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy ? 'Enabled' : 'Disabled'})</Label>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!watchedProxyUrl || watchedProxyUrl === proxyUrl || Object.keys(proxyUrlFormErrors).length > 0 || !useProxy}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tabs>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="extension">
|
<TabsContent value="extension">
|
||||||
<Card className="p-4 space-y-4 my-4">
|
<Card className="p-4 space-y-4 my-4">
|
||||||
@@ -389,81 +454,66 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex flex-col w-[50%] gap-4">
|
<Tabs
|
||||||
<div className="websocket-port">
|
orientation="vertical"
|
||||||
<h3 className="font-semibold">Websocket Port</h3>
|
defaultValue="general"
|
||||||
<p className="text-sm text-muted-foreground mb-3">Change extension websocket server port</p>
|
className="w-full flex flex-row items-start gap-4 mt-10"
|
||||||
<div className="flex items-center gap-4">
|
>
|
||||||
<Form {...websocketPortForm}>
|
<TabsList className="shrink-0 grid grid-cols-1 gap-1 p-0 bg-background min-w-45">
|
||||||
<form onSubmit={websocketPortForm.handleSubmit(handleWebsocketPortSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
<TabsTrigger
|
||||||
<FormField
|
key="general"
|
||||||
control={websocketPortForm.control}
|
value="general"
|
||||||
name="port"
|
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||||
disabled={isChangingWebSocketPort}
|
><Wrench className="size-4" /> General</TabsTrigger>
|
||||||
render={({ field }) => (
|
</TabsList>
|
||||||
<FormItem className="w-full">
|
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
||||||
<FormControl>
|
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[150px]">
|
||||||
<Input
|
<div className="websocket-port">
|
||||||
className="focus-visible:ring-0"
|
<h3 className="font-semibold">Websocket Port</h3>
|
||||||
placeholder="Enter port number"
|
<p className="text-xs text-muted-foreground mb-3">Change extension websocket server port</p>
|
||||||
{...field}
|
<div className="flex items-center gap-4">
|
||||||
/>
|
<Form {...websocketPortForm}>
|
||||||
</FormControl>
|
<form onSubmit={websocketPortForm.handleSubmit(handleWebsocketPortSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
<Label htmlFor="port" className="text-xs text-muted-foreground">(Current: {websocketPort}) (Default: 53511, Range: 50000-60000)</Label>
|
<FormField
|
||||||
<FormMessage />
|
control={websocketPortForm.control}
|
||||||
</FormItem>
|
name="port"
|
||||||
)}
|
disabled={isChangingWebSocketPort}
|
||||||
/>
|
render={({ field }) => (
|
||||||
<Button
|
<FormItem className="w-full">
|
||||||
type="submit"
|
<FormControl>
|
||||||
disabled={!watchedPort || Number(watchedPort) === websocketPort || Object.keys(websocketPortFormErrors).length > 0 || isChangingWebSocketPort || isRestartingWebSocketServer}
|
<Input
|
||||||
>
|
className="focus-visible:ring-0"
|
||||||
{isChangingWebSocketPort ? (
|
placeholder="Enter port number"
|
||||||
<>
|
{...field}
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
/>
|
||||||
Changing
|
</FormControl>
|
||||||
</>
|
<Label htmlFor="port" className="text-xs text-muted-foreground">(Current: {websocketPort}) (Default: 53511, Range: 50000-60000)</Label>
|
||||||
) : (
|
<FormMessage />
|
||||||
'Change'
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
</Button>
|
/>
|
||||||
</form>
|
<Button
|
||||||
</Form>
|
type="submit"
|
||||||
</div>
|
disabled={!watchedPort || Number(watchedPort) === websocketPort || Object.keys(websocketPortFormErrors).length > 0 || isChangingWebSocketPort || isRestartingWebSocketServer}
|
||||||
|
>
|
||||||
|
{isChangingWebSocketPort ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Changing
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Change'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tabs>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div className="flex flex-col">
|
|
||||||
<h3 className="font-semibold">Reset Settings</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-3">Reset all setting to default</p>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="w-fit"
|
|
||||||
variant="destructive"
|
|
||||||
disabled={isUsingDefaultSettings}
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-4 w-4" />
|
|
||||||
Reset Default
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone! it will permanently reset all settings to default.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={
|
|
||||||
() => resetSettings()
|
|
||||||
}>Reset</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user