Animate advanced options panels (#47)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user