Phase 5-9: Matching-Engine, Podcast-Support, Web-Interface + Player
Backend: - Matching-Orchestrator mit deutschen Serien-Patterns (drei ???, TKKG, ...) - Vollständige MusicBrainz-Integration (Tracklist → Kapitel, Cover Art Archive) - OpenLibrary + Google Books als Fallback-Quellen - Auto-Accept (≥0.75) vs zu_prüfen (0.5-0.75) vs kein Match - Manuelles Matching: GET /api/items/:id/match/search, POST apply - RSS-Feed-Manager: feedparser, iTunes Search, periodisches Update - APScheduler für Podcast-Feed-Updates (konfigurierbares Intervall) - Podcast-Router: Feed-URL setzen, Episoden, Feed-Suche - HLS: FFmpeg läuft als Background-Task, wartet auf ersten Segment - main.py: APScheduler + neue Router eingebunden Frontend (React + Vite + Tailwind + HLS.js): - Login-Seite mit Fehlerbehandlung - Library-Seite: Grid/Listen-Ansicht, Suche, Tag-Filter, Pagination, Scan - BookCard: Cover, Fortschrittsbalken, zu_prüfen Badge, Quick-Play - BookDetail: Metadaten, Matching-Panel, Kapitel-Liste, Lesezeichen - AudioPlayer: HLS.js, Kapitel-Marker auf Fortschrittsbalken, Speed, Sleep-Timer, Lesezeichen, Keyboard-Shortcuts (Space/Arrows) - MiniPlayer: persistent an Fußzeile, expandierbar - PodcastDetail: Feed-URL, iTunes-Suche, Episoden-Liste - Admin-Panel: Benutzer/Bibliotheken/Einstellungen verwalten - App.tsx: React Router, Auth-Guard, Player-Overlay Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
53
frontend/src/components/player/MiniPlayer.tsx
Normal file
53
frontend/src/components/player/MiniPlayer.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import { Play, Pause, X, ChevronUp } from 'lucide-react'
|
||||
import { usePlayerStore } from '../../store/playerStore'
|
||||
import CoverImage from '../common/CoverImage'
|
||||
import { coverUrl } from '../../api/items'
|
||||
|
||||
export default function MiniPlayer() {
|
||||
const { item, currentTime, duration, isPlaying, setPlaying, stop, setExpanded } = usePlayerStore()
|
||||
if (!item) return null
|
||||
|
||||
const meta = item.media?.metadata || {}
|
||||
const title = meta.title || item.relPath || ''
|
||||
const author = meta.authors?.[0]?.name || ''
|
||||
const pct = duration > 0 ? (currentTime / duration) * 100 : 0
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-surface border-t border-white/10 z-50">
|
||||
{/* Progress bar */}
|
||||
<div className="h-0.5 bg-white/10">
|
||||
<div className="h-full bg-primary transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<div
|
||||
className="cursor-pointer flex items-center gap-3 flex-1 min-w-0"
|
||||
onClick={() => setExpanded(true)}
|
||||
>
|
||||
<CoverImage
|
||||
src={item.media?.coverPath ? coverUrl(item.id) : null}
|
||||
alt={title}
|
||||
className="w-10 h-10 rounded flex-shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{title}</p>
|
||||
{author && <p className="text-xs text-gray-400 truncate">{author}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button
|
||||
className="w-9 h-9 rounded-full bg-primary text-black flex items-center justify-center"
|
||||
onClick={() => setPlaying(!isPlaying)}
|
||||
>
|
||||
{isPlaying ? <Pause size={16} fill="currentColor" /> : <Play size={16} fill="currentColor" />}
|
||||
</button>
|
||||
<button className="text-gray-400 hover:text-white p-1" onClick={stop}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user