feat(download): add select all and clear history (#136)
* feat(download): add select all and clear history * fix(download): remove clear all history action * feat(download): clear selection on escape * feat(download): select full playlist groups * style(download): adjust playlist group spacing
This commit is contained in:
@@ -78,11 +78,11 @@ export function PlaylistDownloadGroup({
|
||||
const aggregatePercent = totalCount > 0 ? Math.min((totalProgress / totalCount) * 100, 100) : 0
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-md bg-muted/30 px-2.5 py-2 mx-6">
|
||||
<div className="rounded-md bg-muted/30 mx-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-2 rounded-md p-1.5 transition-colors hover:bg-muted/40 active:bg-muted/60 -ml-1.5 -mr-1.5"
|
||||
className="px-3 flex min-w-0 flex-1 items-center gap-2 rounded-md p-1.5 transition-colors hover:bg-muted/40 active:bg-muted/60"
|
||||
onClick={() => setIsExpanded((prev) => !prev)}
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={toggleLabel}
|
||||
@@ -118,7 +118,7 @@ export function PlaylistDownloadGroup({
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<div className="flex shrink-0 items-center gap-1 pr-3">
|
||||
{canDeletePlaylist && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -153,17 +153,15 @@ export function PlaylistDownloadGroup({
|
||||
}}
|
||||
>
|
||||
<div className="min-h-0">
|
||||
<div className="space-y-3 pt-1">
|
||||
{records.map((record) => (
|
||||
<div key={`${groupId}:${record.entryType}:${record.id}`}>
|
||||
<DownloadItem
|
||||
download={record}
|
||||
isSelected={selectedIds?.has(record.id) ?? false}
|
||||
onToggleSelect={onToggleSelect}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{records.map((record) => (
|
||||
<div key={`${groupId}:${record.entryType}:${record.id}`}>
|
||||
<DownloadItem
|
||||
download={record}
|
||||
isSelected={selectedIds?.has(record.id) ?? false}
|
||||
onToggleSelect={onToggleSelect}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,6 +113,17 @@ const resolveDownloadExtension = (download: DownloadRecord): string => {
|
||||
return download.type === 'audio' ? 'mp3' : 'mp4'
|
||||
}
|
||||
|
||||
const isEditableTarget = (target: EventTarget | null): boolean => {
|
||||
if (!target || !(target instanceof HTMLElement)) {
|
||||
return false
|
||||
}
|
||||
if (target.isContentEditable) {
|
||||
return true
|
||||
}
|
||||
const tagName = target.tagName
|
||||
return tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT'
|
||||
}
|
||||
|
||||
interface UnifiedDownloadHistoryProps {
|
||||
onOpenSupportedSites?: () => void
|
||||
onOpenSettings?: () => void
|
||||
@@ -163,6 +174,12 @@ export function UnifiedDownloadHistory({
|
||||
})
|
||||
}, [allRecords, statusFilter])
|
||||
|
||||
const visibleHistoryIds = useMemo(
|
||||
() =>
|
||||
filteredRecords.filter((record) => record.entryType === 'history').map((record) => record.id),
|
||||
[filteredRecords]
|
||||
)
|
||||
|
||||
const filters: Array<{ key: StatusFilter; label: string; count: number }> = [
|
||||
{ key: 'all', label: t('download.all'), count: downloadStats.total },
|
||||
{ key: 'active', label: t('download.active'), count: downloadStats.active },
|
||||
@@ -170,16 +187,34 @@ export function UnifiedDownloadHistory({
|
||||
{ key: 'error', label: t('download.error'), count: downloadStats.error }
|
||||
]
|
||||
|
||||
const selectableIds = useMemo(
|
||||
() =>
|
||||
filteredRecords.filter((record) => record.entryType === 'history').map((record) => record.id),
|
||||
[filteredRecords]
|
||||
)
|
||||
const selectableIds = useMemo(() => {
|
||||
if (visibleHistoryIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
const ids = new Set(visibleHistoryIds)
|
||||
const playlistIds = new Set(
|
||||
filteredRecords
|
||||
.filter((record) => record.entryType === 'history' && record.playlistId)
|
||||
.map((record) => record.playlistId as string)
|
||||
)
|
||||
if (playlistIds.size === 0) {
|
||||
return Array.from(ids)
|
||||
}
|
||||
for (const record of historyRecords) {
|
||||
if (record.playlistId && playlistIds.has(record.playlistId)) {
|
||||
ids.add(record.id)
|
||||
}
|
||||
}
|
||||
return Array.from(ids)
|
||||
}, [filteredRecords, historyRecords, visibleHistoryIds])
|
||||
const selectableCount = selectableIds.length
|
||||
const visibleSelectableCount = visibleHistoryIds.length
|
||||
const selectionSummary =
|
||||
selectableCount === 0
|
||||
? t('history.selectedCount', { count: selectedCount })
|
||||
: t('history.selectionSummary', { selected: selectedCount, total: selectableCount })
|
||||
: selectableCount > visibleSelectableCount
|
||||
? t('history.selectedCount', { count: selectedCount })
|
||||
: t('history.selectionSummary', { selected: selectedCount, total: selectableCount })
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIds.size === 0) {
|
||||
@@ -216,6 +251,13 @@ export function UnifiedDownloadHistory({
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectableIds.length === 0) {
|
||||
return
|
||||
}
|
||||
setSelectedIds(new Set(selectableIds))
|
||||
}
|
||||
|
||||
const handleRequestDeleteSelected = () => {
|
||||
if (selectedIds.size === 0) {
|
||||
return
|
||||
@@ -398,6 +440,41 @@ export function UnifiedDownloadHistory({
|
||||
return { order, groups }
|
||||
}, [filteredRecords])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
if (isEditableTarget(event.target)) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
if (confirmAction) {
|
||||
return
|
||||
}
|
||||
if (selectedIds.size === 0) {
|
||||
return
|
||||
}
|
||||
setSelectedIds(new Set())
|
||||
return
|
||||
}
|
||||
if (!(event.metaKey || event.ctrlKey)) {
|
||||
return
|
||||
}
|
||||
if (event.key.toLowerCase() !== 'a') {
|
||||
return
|
||||
}
|
||||
if (selectableIds.length === 0) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
setSelectedIds(new Set(selectableIds))
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [confirmAction, selectableIds, selectedIds])
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col h-full')}>
|
||||
<CardHeader className="gap-4 p-0 px-6 py-4 z-50 bg-background backdrop-blur">
|
||||
@@ -431,6 +508,15 @@ export function UnifiedDownloadHistory({
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 rounded-full px-3"
|
||||
onClick={handleSelectAll}
|
||||
disabled={selectableIds.length === 0}
|
||||
>
|
||||
{t('history.selectAll')}
|
||||
</Button>
|
||||
<DownloadDialog
|
||||
onOpenSupportedSites={onOpenSupportedSites}
|
||||
onOpenSettings={onOpenSettings}
|
||||
|
||||
Reference in New Issue
Block a user