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 { popularSites } from '@renderer/data/popularSites'
import { cn } from '@renderer/lib/utils'
import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types'
import { useAtom, useSetAtom } from 'jotai'
@@ -1042,23 +1043,41 @@ export function DownloadDialog({ onOpenSupportedSites, onOpenSettings }: Downloa
)}
{/* Advanced Options Content - Single Video */}
{videoInfo && !loading && singleVideoAdvancedOptionsOpen && (
<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}
/>
{videoInfo && !loading && (
<div
data-state={singleVideoAdvancedOptionsOpen ? 'open' : 'closed'}
className={cn(
'grid overflow-hidden transition-[grid-template-rows,opacity] duration-200 ease-out',
singleVideoAdvancedOptionsOpen
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0'
)}
aria-hidden={!singleVideoAdvancedOptionsOpen}
>
<div
className={cn(
'min-h-0',
!singleVideoAdvancedOptionsOpen && 'pointer-events-none'
)}
>
<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>
@@ -1191,51 +1210,62 @@ export function DownloadDialog({ onOpenSupportedSites, onOpenSettings }: Downloa
)}
{/* Advanced Options Content - Playlist */}
{advancedOptionsOpen && (
<div className="w-full pt-4 mt-4 border-t">
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={downloadTypeId}>{t('playlist.downloadType')}</Label>
<Select
value={downloadType}
onValueChange={(v) => setDownloadType(v as 'video' | 'audio')}
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
data-state={advancedOptionsOpen ? 'open' : 'closed'}
className={cn(
'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="w-full pt-4 mt-4 border-t">
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor={downloadTypeId}>{t('playlist.downloadType')}</Label>
<Select
value={downloadType}
onValueChange={(v) => setDownloadType(v as 'video' | 'audio')}
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">
<Label>{t('playlist.range')}</Label>
<div className="flex items-center gap-2">
<Input
placeholder="1"
value={startIndex}
onChange={(e) => setStartIndex(e.target.value)}
className="text-center"
disabled={playlistBusy}
/>
<span className="text-muted-foreground text-xs">-</span>
<Input
placeholder={playlistInfo?.entryCount.toString() || 'End'}
value={endIndex}
onChange={(e) => setEndIndex(e.target.value)}
className="text-center"
disabled={playlistBusy}
/>
<div className="space-y-2">
<Label>{t('playlist.range')}</Label>
<div className="flex items-center gap-2">
<Input
placeholder="1"
value={startIndex}
onChange={(e) => setStartIndex(e.target.value)}
className="text-center"
disabled={playlistBusy}
/>
<span className="text-muted-foreground text-xs">-</span>
<Input
placeholder={playlistInfo?.entryCount.toString() || 'End'}
value={endIndex}
onChange={(e) => setEndIndex(e.target.value)}
className="text-center"
disabled={playlistBusy}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</TabsContent>
</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 { Checkbox } from '@renderer/components/ui/checkbox'
import {
Dialog,
DialogContent,
@@ -17,6 +12,7 @@ import { Input } from '@renderer/components/ui/input'
import { Label } from '@renderer/components/ui/label'
import { Switch } from '@renderer/components/ui/switch'
import { ipcServices } from '@renderer/lib/ipc'
import { cn } from '@renderer/lib/utils'
import { settingsAtom } from '@renderer/store/settings'
import { resolveFeedAtom } from '@renderer/store/subscriptions'
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 prevDefaultPathRef = useRef(buildDefaultSubscriptionDirectory(settings.downloadPath))
const urlInputId = useId()
const advancedOptionsId = useId()
const [advancedOptionsOpen, setAdvancedOptionsOpen] = useState(false)
// Initialize form values based on mode
useEffect(() => {
@@ -92,6 +90,8 @@ export function SubscriptionFormDialog({
return
}
setAdvancedOptionsOpen(false)
if (mode === 'edit' && subscription) {
setUrl(subscription.feedUrl)
setKeywords(subscription.keywords.join(', '))
@@ -274,10 +274,16 @@ export function SubscriptionFormDialog({
</Button>
</div>
</div>
<Accordion type="single" collapsible>
<AccordionItem value="advanced">
<AccordionTrigger>{t('advancedOptions.title')}</AccordionTrigger>
<AccordionContent className="space-y-3">
<div
data-state={advancedOptionsOpen ? 'open' : 'closed'}
className={cn(
'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">
<Label>{t('subscriptions.fields.keywords')}</Label>
<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>
<Switch checked={onlyLatest} onCheckedChange={setOnlyLatest} />
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>
</div>
<DialogFooter>
{mode === 'add' && (
<Button variant="outline" onClick={onClose}>
{t('download.cancel')}
</Button>
)}
<Button onClick={() => void handleSave()}>{t(saveButtonKey)}</Button>
<div className="flex items-center justify-between w-full gap-4">
<div className="flex items-center gap-2">
<Checkbox
id={advancedOptionsId}
checked={advancedOptionsOpen}
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>
</DialogContent>
</Dialog>