(refactor): improved settings categorization and layout

This commit is contained in:
2025-07-09 21:51:51 +05:30
parent 1670a757ca
commit c133b18b49

View File

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