diff --git a/src/api/podcasts.ts b/src/api/podcasts.ts index c7454a6..b094aa9 100644 --- a/src/api/podcasts.ts +++ b/src/api/podcasts.ts @@ -1,5 +1,5 @@ import { api } from './client' -import type { PodcastEpisode } from '@/types/abs' +import type { LibraryItem, PodcastEpisode } from '@/types/abs' /** Check the podcast's RSS feed for new episodes. ABS: POST /api/podcasts/:id/checknew. */ export async function checkNewEpisodes(itemId: string): Promise { @@ -7,6 +7,30 @@ export async function checkNewEpisodes(itemId: string): Promise { + const res = await api.patch<{ libraryItem?: LibraryItem } | LibraryItem>( + `/api/podcasts/${itemId}/episode/${episodeId}`, + payload, + ) + return (res.data as { libraryItem?: LibraryItem }).libraryItem ?? (res.data as LibraryItem) +} + +/** Delete a podcast episode. ABS: DELETE /api/podcasts/:id/episode/:episodeId. */ +export async function deleteEpisode(itemId: string, episodeId: string): Promise { + await api.delete(`/api/podcasts/${itemId}/episode/${episodeId}`) +} + export interface FeedEpisodePreview { title: string description?: string diff --git a/src/components/detail/EpisodeList.tsx b/src/components/detail/EpisodeList.tsx index 535eb61..362dd26 100644 --- a/src/components/detail/EpisodeList.tsx +++ b/src/components/detail/EpisodeList.tsx @@ -1,13 +1,22 @@ -import { Play, Check } from 'lucide-react' +import { useState } from 'react' +import { Play, Check, Pencil } from 'lucide-react' import { formatDuration } from '@/lib/format' import { cn } from '@/lib/cn' import { useProgressStore, progressKey } from '@/store/progressStore' import { usePlayerStore } from '@/store/playerStore' +import { useCan } from '@/store/authStore' import { htmlToText } from '@/lib/html' +import { Modal } from '@/components/ui/Modal' +import { Input } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { updateEpisode } from '@/api/podcasts' +import { apiErrorMessage } from '@/api/client' +import { toast } from '@/store/toastStore' import type { LibraryItem, Podcast, PodcastEpisode } from '@/types/abs' interface Props { item: LibraryItem + onUpdated?: (item: LibraryItem) => void } function pubLabel(ep: PodcastEpisode): string { @@ -25,70 +34,138 @@ function pubLabel(ep: PodcastEpisode): string { return ep.pubDate ?? '' } -export function EpisodeList({ item }: Props) { +export function EpisodeList({ item, onUpdated }: Props) { const episodes = (item.media as Podcast).episodes ?? [] const byKey = useProgressStore((s) => s.byKey) const play = usePlayerStore((s) => s.play) const currentEpisodeId = usePlayerStore((s) => s.episodeId) + const canEdit = useCan('update') + + const [editing, setEditing] = useState(null) + const [title, setTitle] = useState('') + const [saving, setSaving] = useState(false) + + function openRename(ep: PodcastEpisode) { + setEditing(ep) + setTitle(ep.title ?? '') + } + + async function saveRename() { + if (!editing) return + const next = title.trim() + if (!next) { + toast.error('Titel darf nicht leer sein.') + return + } + setSaving(true) + try { + const updated = await updateEpisode(item.id, editing.id, { title: next }) + onUpdated?.(updated) + toast.success('Episode umbenannt.') + setEditing(null) + } catch (err) { + toast.error(apiErrorMessage(err, 'Umbenennen fehlgeschlagen.')) + } finally { + setSaving(false) + } + } if (!episodes.length) { return

Keine Episoden vorhanden.

} - // newest first const sorted = [...episodes].sort((a, b) => (b.publishedAt ?? 0) - (a.publishedAt ?? 0)) return ( -
- {sorted.map((ep) => { - const prog = byKey[progressKey(item.id, ep.id)] - const pct = prog && !prog.isFinished ? Math.round(prog.progress * 100) : 0 - const finished = prog?.isFinished ?? false - const isCurrent = currentEpisodeId === ep.id - const duration = ep.duration ?? ep.audioFile?.duration ?? 0 - const startTime = prog && !prog.isFinished ? prog.currentTime : 0 + <> +
+ {sorted.map((ep) => { + const prog = byKey[progressKey(item.id, ep.id)] + const pct = prog && !prog.isFinished ? Math.round(prog.progress * 100) : 0 + const finished = prog?.isFinished ?? false + const isCurrent = currentEpisodeId === ep.id + const duration = ep.duration ?? ep.audioFile?.duration ?? 0 + const startTime = prog && !prog.isFinished ? prog.currentTime : 0 - return ( -
- -
-
-

{ep.title}

- {finished && } -
-
- {pubLabel(ep) && {pubLabel(ep)}} - {duration > 0 && ( - <> - · - {formatDuration(duration)} - - )} - {pct > 0 && ( - <> - · - {pct}% - + +
+
+

{ep.title}

+ {finished && } + {canEdit && ( + + )} +
+
+ {pubLabel(ep) && {pubLabel(ep)}} + {duration > 0 && ( + <> + · + {formatDuration(duration)} + + )} + {pct > 0 && ( + <> + · + {pct}% + + )} +
+ {ep.description && ( +

+ {htmlToText(ep.description)} +

)}
- {ep.description && ( -

- {htmlToText(ep.description)} -

- )}
-
- ) - })} -
+ ) + })} +
+ + setEditing(null)} + title="Episode umbenennen" + size="sm" + footer={ + <> + + + + } + > + setTitle(e.target.value)} + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') saveRename() + }} + /> + + ) } diff --git a/src/pages/ItemDetail.tsx b/src/pages/ItemDetail.tsx index 859a019..4e730d6 100644 --- a/src/pages/ItemDetail.tsx +++ b/src/pages/ItemDetail.tsx @@ -208,7 +208,7 @@ export default function ItemDetail() { )} {isPodcast ? ( - + setItem(updated)} /> ) : chapters.length > 0 ? (

Kapitel