diff --git a/frontend/index.html b/frontend/index.html index 605c17e..9c7d8de 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,9 @@ Audiolib + + +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e51523e..4c9735a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -50,7 +50,7 @@ function AppRoutes() { - :
Keine Bibliothek. Im Admin-Bereich anlegen.
+ :
Keine Bibliothek. Im Admin-Bereich anlegen.
} /> } /> } /> @@ -64,7 +64,6 @@ function AppRoutes() { /> - {/* Vollbild-Player als Overlay */} {expanded && (
@@ -77,7 +76,6 @@ function AppRoutes() { function PodcastList() { const { libraries } = useAuthStore() const [podcasts, setPodcasts] = React.useState([]) - const podcastLib = libraries.find((l: any) => l.mediaType === 'podcast') React.useEffect(() => { import('./api/client').then(({ default: api }) => { @@ -86,22 +84,22 @@ function PodcastList() { }, []) return ( -
-

Podcasts

+
+

Podcasts

{podcasts.length === 0 ? ( -

Noch keine Podcasts. Library vom Typ „Podcast" scannen.

+

Noch keine Podcasts. Library vom Typ „Podcast" scannen.

) : ( -
+
{podcasts.map((p: any) => ( - diff --git a/frontend/src/components/common/CoverImage.tsx b/frontend/src/components/common/CoverImage.tsx index 98447f0..62c19c8 100644 --- a/frontend/src/components/common/CoverImage.tsx +++ b/frontend/src/components/common/CoverImage.tsx @@ -1,5 +1,18 @@ import React, { useState } from 'react' -import { BookOpen } from 'lucide-react' + +const COVER_COLORS = ['#1a2e1c', '#1c1a2e', '#2e1a1a', '#1a2a2e', '#2a2e1a', '#2e2218'] + +function hashStr(s: string): number { + let h = 0 + for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0 + return Math.abs(h) +} + +function initials(text: string): string { + const words = text.trim().split(/\s+/).filter(Boolean) + if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase() + return (text.slice(0, 2) || '?').toUpperCase() +} interface Props { src?: string | null @@ -7,13 +20,19 @@ interface Props { className?: string } -export default function CoverImage({ src, alt, className = '' }: Props) { +export default function CoverImage({ src, alt = '', className = '' }: Props) { const [error, setError] = useState(false) if (!src || error) { + const bg = COVER_COLORS[hashStr(alt || 'X') % COVER_COLORS.length] return ( -
- +
+ + {initials(alt || '?')} +
) } @@ -21,7 +40,7 @@ export default function CoverImage({ src, alt, className = '' }: Props) { return ( {alt setError(true)} /> diff --git a/frontend/src/components/common/FileBrowser.tsx b/frontend/src/components/common/FileBrowser.tsx index 222397e..943079e 100644 --- a/frontend/src/components/common/FileBrowser.tsx +++ b/frontend/src/components/common/FileBrowser.tsx @@ -37,36 +37,37 @@ export default function FileBrowser({ initialPath = '/', onSelect, onClose }: Pr return (
e.stopPropagation()} > {/* Header */} -
-

Ordner auswählen

-
{/* Quick access */} -
+
{['/audiofiles', '/data', '/media', '/'].map((p) => ( ))}
{/* Current path */} -
+
{parent && ( - )} -

{path}

+

{path}

{/* Entry list */} @@ -76,25 +77,27 @@ export default function FileBrowser({ initialPath = '/', onSelect, onClose }: Pr
) : error ? ( -

{error}

+

{error}

) : entries.length === 0 ? ( -

Ordner ist leer

+

Ordner ist leer

) : ( entries.map((e) => e.isDir ? ( ) : (
{e.name} @@ -105,13 +108,14 @@ export default function FileBrowser({ initialPath = '/', onSelect, onClose }: Pr
{/* Footer */} -
-
- +
) } diff --git a/frontend/src/components/library/BookCard.tsx b/frontend/src/components/library/BookCard.tsx index e1cf903..aa58762 100644 --- a/frontend/src/components/library/BookCard.tsx +++ b/frontend/src/components/library/BookCard.tsx @@ -21,52 +21,62 @@ export default function BookCard({ item }: Props) { const isPlaying = currentItem?.id === item.id const hasCover = item.media?.coverPath + const pct = progress && progress.duration > 0 + ? Math.min((progress.currentTime / progress.duration) * 100, 100) + : 0 + return (
navigate(`/book/${item.id}`)} > -
+ {/* Cover */} +
+ {isPlaying && (
)} + + + {progress && !progress.isFinished && pct > 0 && ( +
+
+
+ )}
-
+ {/* Text */} +
{series && ( -

+

{series.name}{series.sequence ? ` #${series.sequence}` : ''}

)} -

{title}

- {author &&

{author}

} - - {progress && !progress.isFinished && ( -
-
-
+

{title}

+ {author && ( +

{author}

)} - {tags.includes('zu_prüfen') && ( -
- - zu prüfen +
+ + zu prüfen
)}
diff --git a/frontend/src/components/player/AudioPlayer.tsx b/frontend/src/components/player/AudioPlayer.tsx index fe445ab..e317f36 100644 --- a/frontend/src/components/player/AudioPlayer.tsx +++ b/frontend/src/components/player/AudioPlayer.tsx @@ -29,7 +29,6 @@ export default function AudioPlayer() { const title = meta.title || item?.relPath || '' const author = meta.authors?.[0]?.name || '' - // HLS laden sobald sich die Session ändert useEffect(() => { if (!session || !audioRef.current) return const hlsUrl = session.audioTracks?.[0]?.contentUrl @@ -57,7 +56,6 @@ export default function AudioPlayer() { } }, [session?.id]) - // isPlaying <-> audio useEffect(() => { if (!audioRef.current) return if (isPlaying) audioRef.current.play().catch(() => {}) @@ -72,7 +70,6 @@ export default function AudioPlayer() { if (audioRef.current) audioRef.current.volume = muted ? 0 : volume }, [volume, muted]) - // Keyboard shortcuts useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement) return @@ -126,7 +123,6 @@ export default function AudioPlayer() { const SPEEDS = [0.75, 1, 1.25, 1.5, 1.75, 2] const SLEEP_OPTIONS = [15 * 60, 30 * 60, 45 * 60, 60 * 60] - // Chapter progress markers const chapterMarkers = chapters.map((c: any) => ({ pct: duration > 0 ? (c.start / duration) * 100 : 0, })) @@ -142,11 +138,11 @@ export default function AudioPlayer() { {/* Header */}
- - Jetzt läuft -
@@ -156,31 +152,31 @@ export default function AudioPlayer() {
{/* Title */}
{currentChapter && ( -

{currentChapter.title}

+

{currentChapter.title}

)} -

{title}

- {author &&

{author}

} +

{title}

+ {author &&

{author}

}
- {/* Progress bar with chapter markers */} -
-
+ {/* Progress bar */} +
+
{chapterMarkers.map((m: any, i: number) => (
))}
0 ? (currentTime / duration) * 100 : 0}%` }} />
-
+
{fmtTime(currentTime)} -{fmtTime(duration - currentTime)}
@@ -202,19 +198,26 @@ export default function AudioPlayer() { {/* Controls */}
{showSpeedMenu && ( -
+
{SPEEDS.map((s) => (
{/* Bookmark */} - {/* Sleep Timer */}
{sleepTimerActive && sleepTimer !== null && ( - {Math.floor(sleepTimer / 60)}m + {Math.floor(sleepTimer / 60)}m )} {showSleepMenu && ( -
+
{SLEEP_OPTIONS.map((s) => ( {/* Volume */} -
diff --git a/frontend/src/components/player/ChapterList.tsx b/frontend/src/components/player/ChapterList.tsx index aef3200..5d53700 100644 --- a/frontend/src/components/player/ChapterList.tsx +++ b/frontend/src/components/player/ChapterList.tsx @@ -23,18 +23,18 @@ export default function ChapterList({ chapters, currentTime, onSeek }: Props) { } return ( -
-

Kapitel

+
+

Kapitel

{chapters.map((ch, i) => ( ))}
diff --git a/frontend/src/components/player/MiniPlayer.tsx b/frontend/src/components/player/MiniPlayer.tsx index 6acc285..830af60 100644 --- a/frontend/src/components/player/MiniPlayer.tsx +++ b/frontend/src/components/player/MiniPlayer.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Play, Pause, X, ChevronUp } from 'lucide-react' +import { Play, Pause, X } from 'lucide-react' import { usePlayerStore } from '../../store/playerStore' import CoverImage from '../common/CoverImage' import { coverUrl } from '../../api/items' @@ -14,36 +14,65 @@ export default function MiniPlayer() { const pct = duration > 0 ? (currentTime / duration) * 100 : 0 return ( -
- {/* Progress bar */} -
-
+
+ {/* Progress track */} +
+
-
+ {/* 3-column grid */} +
+ {/* Left: cover + title */}
setExpanded(true)} > - +
+ +
-

{title}

- {author &&

{author}

} +

{title}

+ {author &&

{author}

}
-
+ {/* Center: play/pause */} + + + {/* Right: close */} +
-
diff --git a/frontend/src/index.css b/frontend/src/index.css index 6631cba..372a11d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,6 +3,20 @@ @tailwind utilities; body { - @apply bg-background text-white; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #0a0d0b; + color: #e4ede5; + font-family: 'Inter', sans-serif; + font-size: 14px; +} + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #2e3f30; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #3d5440; } + +input[type=range] { + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; } diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index 35b7c80..d95fb85 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -10,16 +10,17 @@ export default function Admin() { const [tab, setTab] = useState('users') return ( -
-

Administration

-
+
+

Administration

+
{(['users', 'libraries', 'settings'] as Tab[]).map((t) => ( @@ -58,38 +59,40 @@ function UsersPanel() { return (
-

{users.length} Benutzer

+

{users.length} Benutzer

{showCreate && ( -
-

Neuer Benutzer

+
+

Neuer Benutzer

setForm({ ...form, username: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary" + className="w-full bg-card border border-divider rounded-lg px-3 py-2 text-sm text-ink placeholder-muted focus:outline-none focus:ring-1 focus:ring-primary transition-colors" /> setForm({ ...form, password: e.target.value })} - className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary" + className="w-full bg-card border border-divider rounded-lg px-3 py-2 text-sm text-ink placeholder-muted focus:outline-none focus:ring-1 focus:ring-primary transition-colors" /> -