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 { 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user