updated
This commit is contained in:
parent
d909d951a3
commit
e3387dd6fe
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
backend/web/dist/assets/index-ByYRHYVi.css
vendored
1
backend/web/dist/assets/index-ByYRHYVi.css
vendored
File diff suppressed because one or more lines are too long
1
backend/web/dist/assets/index-CAKbyWZn.css
vendored
Normal file
1
backend/web/dist/assets/index-CAKbyWZn.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
backend/web/dist/index.html
vendored
4
backend/web/dist/index.html
vendored
@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>App</title>
|
<title>App</title>
|
||||||
<script type="module" crossorigin src="/assets/index-IS5yelG1.js"></script>
|
<script type="module" crossorigin src="/assets/index-Czq-AJKF.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-ByYRHYVi.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CAKbyWZn.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -994,6 +994,26 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onHint = (ev: Event) => {
|
||||||
|
const e = ev as CustomEvent<{ delta?: number }>
|
||||||
|
const delta = Number(e.detail?.delta ?? 0)
|
||||||
|
if (!Number.isFinite(delta) || delta === 0) {
|
||||||
|
void refreshDoneNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Tabs sofort updaten (optimistisch)
|
||||||
|
setDoneCount((c) => Math.max(0, c + delta))
|
||||||
|
|
||||||
|
// ✅ danach einmal server-truth holen (Pagination + count 100% korrekt)
|
||||||
|
void refreshDoneNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('finished-downloads:count-hint', onHint as EventListener)
|
||||||
|
return () => window.removeEventListener('finished-downloads:count-hint', onHint as EventListener)
|
||||||
|
}, [refreshDoneNow])
|
||||||
|
|
||||||
// ---- Player model sync (wie bei dir) ----
|
// ---- Player model sync (wie bei dir) ----
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!playerJob) {
|
if (!playerJob) {
|
||||||
@ -1028,6 +1048,10 @@ export default function App() {
|
|||||||
setJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file))
|
setJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file))
|
||||||
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
||||||
}, 320)
|
}, 320)
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
void refreshDoneNow()
|
||||||
|
}, 350)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
window.dispatchEvent(new CustomEvent('finished-downloads:delete', { detail: { file, phase: 'error' as const } }))
|
window.dispatchEvent(new CustomEvent('finished-downloads:delete', { detail: { file, phase: 'error' as const } }))
|
||||||
notify.error('Löschen fehlgeschlagen', e?.message ?? String(e))
|
notify.error('Löschen fehlgeschlagen', e?.message ?? String(e))
|
||||||
@ -1052,7 +1076,7 @@ export default function App() {
|
|||||||
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
||||||
}, 320)
|
}, 320)
|
||||||
|
|
||||||
if (selectedTab !== 'finished') void refreshDoneNow()
|
void refreshDoneNow()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
window.dispatchEvent(new CustomEvent('finished-downloads:delete', { detail: { file, phase: 'error' as const } }))
|
window.dispatchEvent(new CustomEvent('finished-downloads:delete', { detail: { file, phase: 'error' as const } }))
|
||||||
notify.error('Keep fehlgeschlagen', e?.message ?? String(e))
|
notify.error('Keep fehlgeschlagen', e?.message ?? String(e))
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import {
|
|||||||
RectangleStackIcon,
|
RectangleStackIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
AdjustmentsHorizontalIcon,
|
AdjustmentsHorizontalIcon,
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
import { type SwipeCardHandle } from './SwipeCard'
|
import { type SwipeCardHandle } from './SwipeCard'
|
||||||
import { flushSync } from 'react-dom'
|
import { flushSync } from 'react-dom'
|
||||||
@ -30,6 +28,7 @@ import Button from './Button'
|
|||||||
import { useNotify } from './notify'
|
import { useNotify } from './notify'
|
||||||
import LazyMount from './LazyMount'
|
import LazyMount from './LazyMount'
|
||||||
import { isHotName, stripHotPrefix } from './hotName'
|
import { isHotName, stripHotPrefix } from './hotName'
|
||||||
|
import LabeledSwitch from './LabeledSwitch'
|
||||||
import Switch from './Switch'
|
import Switch from './Switch'
|
||||||
|
|
||||||
type SortMode =
|
type SortMode =
|
||||||
@ -388,9 +387,11 @@ export default function FinishedDownloads({
|
|||||||
// Wenn Filter aktiv: Overrides behalten (wir arbeiten mit all=1)
|
// Wenn Filter aktiv: Overrides behalten (wir arbeiten mit all=1)
|
||||||
if (globalFilterActive) return
|
if (globalFilterActive) return
|
||||||
|
|
||||||
|
// ✅ Overrides nur zurücksetzen, wenn sich die "Query" ändert,
|
||||||
|
// nicht wenn App optimistisch doneJobs filtert.
|
||||||
setOverrideDoneJobs(null)
|
setOverrideDoneJobs(null)
|
||||||
setOverrideDoneTotal(null)
|
setOverrideDoneTotal(null)
|
||||||
}, [doneJobs, doneTotal, globalFilterActive])
|
}, [page, pageSize, sortMode, includeKeep, globalFilterActive])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!includeKeep) {
|
if (!includeKeep) {
|
||||||
@ -604,6 +605,10 @@ export default function FinishedDownloads({
|
|||||||
}
|
}
|
||||||
|
|
||||||
animateRemove(key)
|
animateRemove(key)
|
||||||
|
|
||||||
|
// ✅ Tab-Count sofort korrigieren (App hört drauf)
|
||||||
|
window.dispatchEvent(new CustomEvent('finished-downloads:count-hint', { detail: { delta: -1 } }))
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notify.error('Löschen fehlgeschlagen', String(e?.message || e))
|
notify.error('Löschen fehlgeschlagen', String(e?.message || e))
|
||||||
@ -637,6 +642,10 @@ export default function FinishedDownloads({
|
|||||||
|
|
||||||
// ✅ aus UI entfernen (wie delete), aber "keep" ist kein delete -> trotzdem raus aus finished
|
// ✅ aus UI entfernen (wie delete), aber "keep" ist kein delete -> trotzdem raus aus finished
|
||||||
animateRemove(key)
|
animateRemove(key)
|
||||||
|
|
||||||
|
// ✅ Tab-Count sofort korrigieren (App hört drauf)
|
||||||
|
window.dispatchEvent(new CustomEvent('finished-downloads:count-hint', { detail: { delta: -1 } }))
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notify.error('Keep fehlgeschlagen', String(e?.message || e))
|
notify.error('Keep fehlgeschlagen', String(e?.message || e))
|
||||||
@ -656,13 +665,51 @@ export default function FinishedDownloads({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ HOT-Name berechnen (genau "HOT " Prefix)
|
||||||
|
const toggledName = (raw: string) => (isHotName(raw) ? stripHotPrefix(raw) : `HOT ${raw}`)
|
||||||
|
|
||||||
|
// ✅ UI-optimistisch umbenennen + Dauer-Key mitziehen
|
||||||
|
const applyOptimisticRename = (oldFile: string, newFile: string) => {
|
||||||
|
if (!newFile || newFile === oldFile) return
|
||||||
|
|
||||||
|
setRenamedFiles((prev) => ({ ...prev, [oldFile]: newFile }))
|
||||||
|
|
||||||
|
setDurations((prev) => {
|
||||||
|
const v = prev[oldFile]
|
||||||
|
if (typeof v !== 'number') return prev
|
||||||
|
const { [oldFile]: _omit, ...rest } = prev
|
||||||
|
return { ...rest, [newFile]: v }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ falls Backend andere Namen liefert: Server-Truth nachziehen
|
||||||
|
const applyServerTruth = (apiOld: string, apiNew: string, optimisticNew: string) => {
|
||||||
|
if (!apiNew) return
|
||||||
|
if (apiNew === optimisticNew && apiOld === file) return
|
||||||
|
|
||||||
|
setRenamedFiles((prev) => ({ ...prev, [apiOld]: apiNew }))
|
||||||
|
|
||||||
|
setDurations((prev) => {
|
||||||
|
const v = prev[apiOld] ?? prev[optimisticNew]
|
||||||
|
if (typeof v !== 'number') return prev
|
||||||
|
const { [apiOld]: _omit1, [optimisticNew]: _omit2, ...rest } = prev as any
|
||||||
|
return { ...rest, [apiNew]: v }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await releasePlayingFile(file, { close: true })
|
await releasePlayingFile(file, { close: true })
|
||||||
|
|
||||||
// ✅ Wenn du extern einen Handler hast, kannst du den nutzen
|
const oldFile = file
|
||||||
// (Wenn du KEINEN hast: läuft der Fallback unten)
|
const optimisticNew = toggledName(oldFile)
|
||||||
|
|
||||||
|
// ✅ Wichtig: Optimistik IMMER anwenden – auch wenn ein externer onToggleHot Handler existiert
|
||||||
|
applyOptimisticRename(oldFile, optimisticNew)
|
||||||
|
|
||||||
|
// ✅ Externer Handler (App) – danach Liste auffrischen
|
||||||
if (onToggleHot) {
|
if (onToggleHot) {
|
||||||
await onToggleHot(job)
|
await onToggleHot(job)
|
||||||
|
queueRefill()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,26 +721,17 @@ export default function FinishedDownloads({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = (await res.json().catch(() => null)) as any
|
const data = (await res.json().catch(() => null)) as any
|
||||||
const oldFile = typeof data?.oldFile === 'string' && data.oldFile ? data.oldFile : file
|
const apiOld = typeof data?.oldFile === 'string' && data.oldFile ? data.oldFile : file
|
||||||
const newFile = typeof data?.newFile === 'string' && data.newFile ? data.newFile : ''
|
const apiNew = typeof data?.newFile === 'string' && data.newFile ? data.newFile : ''
|
||||||
|
|
||||||
if (newFile) {
|
applyServerTruth(apiOld, apiNew, optimisticNew)
|
||||||
// Optimistisch umbenennen (nicht aufs nächste Polling warten)
|
|
||||||
setRenamedFiles((prev) => ({ ...prev, [oldFile]: newFile }))
|
|
||||||
|
|
||||||
// Dauer-Key mitziehen (optional)
|
queueRefill()
|
||||||
setDurations((prev) => {
|
|
||||||
const v = prev[oldFile]
|
|
||||||
if (typeof v !== 'number') return prev
|
|
||||||
const { [oldFile]: _omit, ...rest } = prev
|
|
||||||
return { ...rest, [newFile]: v }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notify.error('HOT umbenennen fehlgeschlagen', String(e?.message || e))
|
notify.error('HOT umbenennen fehlgeschlagen', String(e?.message || e))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[baseName, releasePlayingFile, onToggleHot, notify]
|
[baseName, releasePlayingFile, onToggleHot, notify, queueRefill]
|
||||||
)
|
)
|
||||||
|
|
||||||
const runtimeSecondsForSort = useCallback((job: RecordJob) => {
|
const runtimeSecondsForSort = useCallback((job: RecordJob) => {
|
||||||
@ -1255,9 +1293,9 @@ export default function FinishedDownloads({
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<div className="flex items-center justify-between gap-2 p-3">
|
<div className="flex items-center gap-3 p-3">
|
||||||
{/* Mobile title + count badge */}
|
{/* Left: Title + Count */}
|
||||||
<div className="sm:hidden flex items-center gap-2 min-w-0">
|
<div className="hidden sm:flex items-center gap-2 min-w-0">
|
||||||
<div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
|
<div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
Abgeschlossene Downloads
|
Abgeschlossene Downloads
|
||||||
</div>
|
</div>
|
||||||
@ -1266,18 +1304,51 @@ export default function FinishedDownloads({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop title */}
|
{/* Mobile title (bleibt wie gehabt, aber kompakter) */}
|
||||||
<div className="hidden sm:flex items-center gap-2 min-w-0">
|
<div className="sm:hidden flex items-center gap-2 min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
|
<div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
Abgeschlossene Downloads
|
Abgeschlossene Downloads
|
||||||
</div>
|
</div>
|
||||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
|
<span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
|
||||||
{totalItemsForPagination}
|
{totalItemsForPagination}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 flex items-center gap-2">
|
{/* Right: Controls */}
|
||||||
{/* Desktop: Sort rechts neben View-Buttons */}
|
<div className="flex items-center gap-2 ml-auto shrink-0">
|
||||||
|
{/* Desktop: Suche soll den Platz füllen */}
|
||||||
|
<div className="hidden sm:flex items-center gap-2 min-w-0 flex-1">
|
||||||
|
<input
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Suchen…"
|
||||||
|
className="
|
||||||
|
h-9 w-full max-w-[420px] rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
||||||
|
dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
{(searchQuery || '').trim() !== '' ? (
|
||||||
|
<Button size="sm" variant="soft" onClick={clearSearch}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop: Keep Toggle */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<LabeledSwitch
|
||||||
|
label="Behaltene Downloads anzeigen"
|
||||||
|
checked={includeKeep}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (page !== 1) onPageChange(1)
|
||||||
|
setIncludeKeep(checked)
|
||||||
|
queueRefill()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop: Sort (nur wenn nicht Tabelle) */}
|
||||||
{view !== 'table' && (
|
{view !== 'table' && (
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:block">
|
||||||
<label className="sr-only" htmlFor="finished-sort">
|
<label className="sr-only" htmlFor="finished-sort">
|
||||||
@ -1310,109 +1381,87 @@ export default function FinishedDownloads({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Keep toggle (Desktop) */}
|
|
||||||
<div className="hidden sm:flex items-center gap-2">
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-200">Behaltene Downloads anzeigen</span>
|
|
||||||
<Switch
|
|
||||||
checked={includeKeep}
|
|
||||||
onChange={(checked) => {
|
|
||||||
if (page !== 1) onPageChange(1)
|
|
||||||
setIncludeKeep(checked)
|
|
||||||
queueRefill()
|
|
||||||
}}
|
|
||||||
ariaLabel="Behaltene Downloads anzeigen"
|
|
||||||
size="short"
|
|
||||||
className=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop: Suche neben Sort */}
|
|
||||||
<div className="hidden sm:flex items-center gap-2 min-w-0">
|
|
||||||
<input
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder="Suchen…"
|
|
||||||
className="
|
|
||||||
h-9 w-56 md:w-72 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
||||||
dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
{(searchQuery || '').trim() !== '' ? (
|
|
||||||
<Button size="sm" variant="soft" onClick={clearSearch}>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile: Optionen ein/ausklappen */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sm:hidden inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-2.5 text-xs font-semibold text-gray-900 shadow-sm
|
|
||||||
hover:bg-gray-50 dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-white/10"
|
|
||||||
onClick={() => setMobileOptionsOpen((v) => !v)}
|
|
||||||
aria-expanded={mobileOptionsOpen}
|
|
||||||
aria-controls="finished-mobile-options"
|
|
||||||
>
|
|
||||||
<AdjustmentsHorizontalIcon className="size-4" />
|
|
||||||
Optionen
|
|
||||||
{mobileOptionsOpen ? (
|
|
||||||
<ChevronUpIcon className="size-4 opacity-80" />
|
|
||||||
) : (
|
|
||||||
<ChevronDownIcon className="size-4 opacity-80" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Views */}
|
{/* Views */}
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
value={view}
|
value={view}
|
||||||
onChange={(id) => setView(id as ViewMode)}
|
onChange={(id) => setView(id as ViewMode)}
|
||||||
size="md"
|
size={isSmall ? 'sm' : 'md'}
|
||||||
ariaLabel="Ansicht"
|
ariaLabel="Ansicht"
|
||||||
items={[
|
items={[
|
||||||
{
|
{ id: 'table', icon: <TableCellsIcon className={isSmall ? 'size-4' : 'size-5'} />, label: isSmall ? undefined : 'Tabelle', srLabel: 'Tabelle' },
|
||||||
id: 'table',
|
{ id: 'cards', icon: <RectangleStackIcon className={isSmall ? 'size-4' : 'size-5'} />, label: isSmall ? undefined : 'Cards', srLabel: 'Cards' },
|
||||||
icon: <TableCellsIcon className="size-5" />,
|
{ id: 'gallery', icon: <Squares2X2Icon className={isSmall ? 'size-4' : 'size-5'} />, label: isSmall ? undefined : 'Galerie', srLabel: 'Galerie' },
|
||||||
label: isSmall ? undefined : 'Tabelle',
|
|
||||||
srLabel: 'Tabelle',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cards',
|
|
||||||
icon: <RectangleStackIcon className="size-5" />,
|
|
||||||
label: isSmall ? undefined : 'Cards',
|
|
||||||
srLabel: 'Cards',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gallery',
|
|
||||||
icon: <Squares2X2Icon className="size-5" />,
|
|
||||||
label: isSmall ? undefined : 'Galerie',
|
|
||||||
srLabel: 'Galerie',
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Mobile: Optionen Button (unverändert) */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sm:hidden relative inline-flex items-center justify-center rounded-md border border-gray-200 bg-white p-2 shadow-sm
|
||||||
|
hover:bg-gray-50 dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-white/10"
|
||||||
|
onClick={() => setMobileOptionsOpen((v) => !v)}
|
||||||
|
aria-expanded={mobileOptionsOpen}
|
||||||
|
aria-controls="finished-mobile-options"
|
||||||
|
aria-label="Filter & Optionen"
|
||||||
|
>
|
||||||
|
<AdjustmentsHorizontalIcon className="size-5" />
|
||||||
|
{/* kleiner Punkt wenn Filter aktiv */}
|
||||||
|
{globalFilterActive || includeKeep ? (
|
||||||
|
<span className="absolute -top-1 -right-1 h-2.5 w-2.5 rounded-full bg-indigo-500 ring-2 ring-white dark:ring-gray-950" />
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop: aktive Tag-Filter anzeigen */}
|
||||||
|
{tagFilter.length > 0 ? (
|
||||||
|
<div className="hidden sm:flex items-center gap-2 border-t border-gray-200/60 dark:border-white/10 px-3 py-2">
|
||||||
|
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
Tag-Filter
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
{tagFilter.map((t) => (
|
||||||
|
<TagBadge key={t} tag={t} active={true} onClick={toggleTagFilter} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="
|
||||||
|
ml-1 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium
|
||||||
|
text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
onClick={clearTagFilter}
|
||||||
|
>
|
||||||
|
Zurücksetzen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Mobile Optionen (einklappbar): Suche + Keep + Sort */}
|
{/* Mobile Optionen (einklappbar): Suche + Keep + Sort */}
|
||||||
<div
|
<div
|
||||||
id="finished-mobile-options"
|
id="finished-mobile-options"
|
||||||
className={[
|
className={[
|
||||||
'sm:hidden overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out',
|
'sm:hidden overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out',
|
||||||
mobileOptionsOpen ? 'max-h-[520px] opacity-100' : 'max-h-0 opacity-0',
|
mobileOptionsOpen ? 'max-h-[720px] opacity-100' : 'max-h-0 opacity-0',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
|
{/* “Sheet”-Body */}
|
||||||
{/* Mobile Suche */}
|
<div className="border-t border-gray-200/60 dark:border-white/10 p-3">
|
||||||
<div className="sm:hidden border-t border-gray-200/60 dark:border-white/10 p-3 pt-2">
|
<div className="space-y-2">
|
||||||
|
{/* Suche */}
|
||||||
|
<div className="rounded-lg border border-gray-200/70 bg-white/70 p-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Suchen…"
|
placeholder="Suchen…"
|
||||||
className="
|
className="
|
||||||
w-full h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
|
h-10 w-full rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900
|
||||||
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
||||||
dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
|
dark:border-white/10 dark:bg-gray-950/60 dark:text-gray-100 dark:[color-scheme:dark]
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{(searchQuery || '').trim() !== '' ? (
|
{(searchQuery || '').trim() !== '' ? (
|
||||||
@ -1423,12 +1472,17 @@ export default function FinishedDownloads({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile: Keep Toggle */}
|
{/* Keep – als Setting Row */}
|
||||||
<div className="sm:hidden border-t border-gray-200/60 dark:border-white/10 p-3 pt-2">
|
<div className="rounded-lg border border-gray-200/70 bg-white/70 px-3 py-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<div className="min-w-0">
|
||||||
Behaltene Downloads anzeigen
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
</span>
|
Keep anzeigen
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
Behaltene Downloads in der Liste
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={includeKeep}
|
checked={includeKeep}
|
||||||
@ -1442,14 +1496,13 @@ export default function FinishedDownloads({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Sort (eigene Sektion, schöner Abstand automatisch durch Trenner/Padding) */}
|
{/* Sort */}
|
||||||
{view !== 'table' && (
|
{view !== 'table' ? (
|
||||||
<div className="sm:hidden border-t border-gray-200/60 dark:border-white/10 p-3 pt-2">
|
<div className="rounded-lg border border-gray-200/70 bg-white/70 px-3 py-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
|
||||||
<label className="sr-only" htmlFor="finished-sort-mobile">
|
<div className="text-xs font-medium text-gray-600 dark:text-gray-300 mb-1">
|
||||||
Sortierung
|
Sortierung
|
||||||
</label>
|
</div>
|
||||||
<select
|
<select
|
||||||
id="finished-sort-mobile"
|
id="finished-sort-mobile"
|
||||||
value={sortMode}
|
value={sortMode}
|
||||||
@ -1459,8 +1512,8 @@ export default function FinishedDownloads({
|
|||||||
if (page !== 1) onPageChange(1)
|
if (page !== 1) onPageChange(1)
|
||||||
}}
|
}}
|
||||||
className="
|
className="
|
||||||
w-full h-9 rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-900 shadow-sm
|
w-full h-10 rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-900 shadow-sm
|
||||||
dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
|
dark:border-white/10 dark:bg-gray-950/60 dark:text-gray-100 dark:[color-scheme:dark]
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<option value="completed_desc">Fertiggestellt am ↓</option>
|
<option value="completed_desc">Fertiggestellt am ↓</option>
|
||||||
@ -1475,9 +1528,11 @@ export default function FinishedDownloads({
|
|||||||
<option value="size_asc">Größe ↑</option>
|
<option value="size_asc">Größe ↑</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Tag-Filter (als Sektion IN der Card, nicht nochmal eine eigene Box-in-Box) */}
|
{/* Tag-Filter */}
|
||||||
{tagFilter.length > 0 ? (
|
{tagFilter.length > 0 ? (
|
||||||
<div className="border-t border-gray-200/60 dark:border-white/10 px-3 py-2">
|
<div className="border-t border-gray-200/60 dark:border-white/10 px-3 py-2">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@ -1493,8 +1548,8 @@ export default function FinishedDownloads({
|
|||||||
ml-1 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium
|
ml-1 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium
|
||||||
text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10
|
text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10
|
||||||
"
|
"
|
||||||
size='sm'
|
size="sm"
|
||||||
variant='soft'
|
variant="soft"
|
||||||
onClick={clearTagFilter}
|
onClick={clearTagFilter}
|
||||||
>
|
>
|
||||||
Zurücksetzen
|
Zurücksetzen
|
||||||
@ -1505,6 +1560,7 @@ export default function FinishedDownloads({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{emptyFolder ? (
|
{emptyFolder ? (
|
||||||
<Card grayBody>
|
<Card grayBody>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user