Animate advanced options panels (#47)

This commit is contained in:
Nexmoe
2025-12-21 13:32:46 +08:00
committed by GitHub
parent 3bc9d76f6e
commit f5c26f5f02
2 changed files with 125 additions and 75 deletions

View File

@@ -20,6 +20,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'
import { popularSites } from '@renderer/data/popularSites' import { popularSites } from '@renderer/data/popularSites'
import { cn } from '@renderer/lib/utils'
import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types' import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types'
import { useAtom, useSetAtom } from 'jotai' import { useAtom, useSetAtom } from 'jotai'
@@ -1042,23 +1043,41 @@ export function DownloadDialog({ onOpenSupportedSites, onOpenSettings }: Downloa
)} )}
{/* Advanced Options Content - Single Video */} {/* Advanced Options Content - Single Video */}
{videoInfo && !loading && singleVideoAdvancedOptionsOpen && ( {videoInfo && !loading && (
<div className="w-full pt-4 mt-4 border-t"> <div
<AdvancedOptions data-state={singleVideoAdvancedOptionsOpen ? 'open' : 'closed'}
startTime={videoInfoCardState.startTime} className={cn(
endTime={videoInfoCardState.endTime} 'grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-out',
downloadSubs={videoInfoCardState.downloadSubs} singleVideoAdvancedOptionsOpen
onStartTimeChange={(value) => ? 'grid-rows-[1fr] opacity-100'
setVideoInfoCardState((prev) => ({ ...prev, startTime: value })) : 'grid-rows-[0fr] opacity-0'
} )}
onEndTimeChange={(value) => aria-hidden={!singleVideoAdvancedOptionsOpen}
setVideoInfoCardState((prev) => ({ ...prev, endTime: value })) >
} <div
onDownloadSubsChange={(value) => className={cn(
setVideoInfoCardState((prev) => ({ ...prev, downloadSubs: value })) 'min-h-0',
} !singleVideoAdvancedOptionsOpen && 'pointer-events-none'
showAccordion={false} )}
/> >
<div className="w-full pt-4 mt-4 border-t">
<AdvancedOptions
startTime={videoInfoCardState.startTime}
endTime={videoInfoCardState.endTime}
downloadSubs={videoInfoCardState.downloadSubs}
onStartTimeChange={(value) =>
setVideoInfoCardState((prev) => ({ ...prev, startTime: value }))
}
onEndTimeChange={(value) =>
setVideoInfoCardState((prev) => ({ ...prev, endTime: value }))
}
onDownloadSubsChange={(value) =>
setVideoInfoCardState((prev) => ({ ...prev, downloadSubs: value }))
}
showAccordion={false}
/>
</div>
</div>
</div> </div>
)} )}
</div> </div>
@@ -1191,51 +1210,62 @@ export function DownloadDialog({ onOpenSupportedSites, onOpenSettings }: Downloa
)} )}
{/* Advanced Options Content - Playlist */} {/* Advanced Options Content - Playlist */}
{advancedOptionsOpen && ( <div
<div className="w-full pt-4 mt-4 border-t"> data-state={advancedOptionsOpen ? 'open' : 'closed'}
<div className="space-y-4"> className={cn(
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 'grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-out',
<div className="space-y-2"> advancedOptionsOpen
<Label htmlFor={downloadTypeId}>{t('playlist.downloadType')}</Label> ? 'grid-rows-[1fr] opacity-100'
<Select : 'grid-rows-[0fr] opacity-0'
value={downloadType} )}
onValueChange={(v) => setDownloadType(v as 'video' | 'audio')} aria-hidden={!advancedOptionsOpen}
disabled={playlistBusy} >
> <div className={cn('min-h-0', !advancedOptionsOpen && 'pointer-events-none')}>
<SelectTrigger id={downloadTypeId}> <div className="w-full pt-4 mt-4 border-t">
<SelectValue /> <div className="space-y-4">
</SelectTrigger> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectContent> <div className="space-y-2">
<SelectItem value="video">{t('download.video')}</SelectItem> <Label htmlFor={downloadTypeId}>{t('playlist.downloadType')}</Label>
<SelectItem value="audio">{t('download.audio')}</SelectItem> <Select
</SelectContent> value={downloadType}
</Select> onValueChange={(v) => setDownloadType(v as 'video' | 'audio')}
</div> disabled={playlistBusy}
>
<SelectTrigger id={downloadTypeId}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="video">{t('download.video')}</SelectItem>
<SelectItem value="audio">{t('download.audio')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label>{t('playlist.range')}</Label> <Label>{t('playlist.range')}</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
placeholder="1" placeholder="1"
value={startIndex} value={startIndex}
onChange={(e) => setStartIndex(e.target.value)} onChange={(e) => setStartIndex(e.target.value)}
className="text-center" className="text-center"
disabled={playlistBusy} disabled={playlistBusy}
/> />
<span className="text-muted-foreground text-xs">-</span> <span className="text-muted-foreground text-xs">-</span>
<Input <Input
placeholder={playlistInfo?.entryCount.toString() || 'End'} placeholder={playlistInfo?.entryCount.toString() || 'End'}
value={endIndex} value={endIndex}
onChange={(e) => setEndIndex(e.target.value)} onChange={(e) => setEndIndex(e.target.value)}
className="text-center" className="text-center"
disabled={playlistBusy} disabled={playlistBusy}
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} </div>
</div> </div>
</TabsContent> </TabsContent>
</ScrollArea> </ScrollArea>

View File

@@ -1,10 +1,5 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
} from '@renderer/components/ui/accordion'
import { Button } from '@renderer/components/ui/button' import { Button } from '@renderer/components/ui/button'
import { Checkbox } from '@renderer/components/ui/checkbox'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -17,6 +12,7 @@ import { Input } from '@renderer/components/ui/input'
import { Label } from '@renderer/components/ui/label' import { Label } from '@renderer/components/ui/label'
import { Switch } from '@renderer/components/ui/switch' import { Switch } from '@renderer/components/ui/switch'
import { ipcServices } from '@renderer/lib/ipc' import { ipcServices } from '@renderer/lib/ipc'
import { cn } from '@renderer/lib/utils'
import { settingsAtom } from '@renderer/store/settings' import { settingsAtom } from '@renderer/store/settings'
import { resolveFeedAtom } from '@renderer/store/subscriptions' import { resolveFeedAtom } from '@renderer/store/subscriptions'
import { DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE, type SubscriptionRule } from '@shared/types' import { DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE, type SubscriptionRule } from '@shared/types'
@@ -85,6 +81,8 @@ export function SubscriptionFormDialog({
const detectTimeout = useRef<NodeJS.Timeout | null>(null) const detectTimeout = useRef<NodeJS.Timeout | null>(null)
const prevDefaultPathRef = useRef(buildDefaultSubscriptionDirectory(settings.downloadPath)) const prevDefaultPathRef = useRef(buildDefaultSubscriptionDirectory(settings.downloadPath))
const urlInputId = useId() const urlInputId = useId()
const advancedOptionsId = useId()
const [advancedOptionsOpen, setAdvancedOptionsOpen] = useState(false)
// Initialize form values based on mode // Initialize form values based on mode
useEffect(() => { useEffect(() => {
@@ -92,6 +90,8 @@ export function SubscriptionFormDialog({
return return
} }
setAdvancedOptionsOpen(false)
if (mode === 'edit' && subscription) { if (mode === 'edit' && subscription) {
setUrl(subscription.feedUrl) setUrl(subscription.feedUrl)
setKeywords(subscription.keywords.join(', ')) setKeywords(subscription.keywords.join(', '))
@@ -274,10 +274,16 @@ export function SubscriptionFormDialog({
</Button> </Button>
</div> </div>
</div> </div>
<Accordion type="single" collapsible> <div
<AccordionItem value="advanced"> data-state={advancedOptionsOpen ? 'open' : 'closed'}
<AccordionTrigger>{t('advancedOptions.title')}</AccordionTrigger> className={cn(
<AccordionContent className="space-y-3"> 'grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-out',
advancedOptionsOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
)}
aria-hidden={!advancedOptionsOpen}
>
<div className={cn('min-h-0', !advancedOptionsOpen && 'pointer-events-none')}>
<div className="space-y-3 border-t pt-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t('subscriptions.fields.keywords')}</Label> <Label>{t('subscriptions.fields.keywords')}</Label>
<Input value={keywords} onChange={(event) => setKeywords(event.target.value)} /> <Input value={keywords} onChange={(event) => setKeywords(event.target.value)} />
@@ -299,17 +305,31 @@ export function SubscriptionFormDialog({
<p className="text-sm">{t('subscriptions.fields.onlyLatest')}</p> <p className="text-sm">{t('subscriptions.fields.onlyLatest')}</p>
<Switch checked={onlyLatest} onCheckedChange={setOnlyLatest} /> <Switch checked={onlyLatest} onCheckedChange={setOnlyLatest} />
</div> </div>
</AccordionContent> </div>
</AccordionItem> </div>
</Accordion> </div>
</div> </div>
<DialogFooter> <DialogFooter>
{mode === 'add' && ( <div className="flex items-center justify-between w-full gap-4">
<Button variant="outline" onClick={onClose}> <div className="flex items-center gap-2">
{t('download.cancel')} <Checkbox
</Button> id={advancedOptionsId}
)} checked={advancedOptionsOpen}
<Button onClick={() => void handleSave()}>{t(saveButtonKey)}</Button> onCheckedChange={(checked) => setAdvancedOptionsOpen(checked === true)}
/>
<Label htmlFor={advancedOptionsId} className="cursor-pointer">
{t('advancedOptions.title')}
</Label>
</div>
<div className="ml-auto flex gap-2">
{mode === 'add' && (
<Button variant="outline" onClick={onClose}>
{t('download.cancel')}
</Button>
)}
<Button onClick={() => void handleSave()}>{t(saveButtonKey)}</Button>
</div>
</div>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>